Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement /api/v1/contacts REST API #1

Merged
merged 1 commit into from
Oct 18, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ gem 'jbuilder', '~> 2.5'
gem 'bootsnap', '>= 1.1.0', require: false

gem 'devise'
gem 'reform-rails', github: 'trailblazer/reform-rails'
gem 'reform', github: 'fernandes/reform', branch: 'feature/indexed_errors'
gem 'trailblazer-endpoint', github: 'trailblazer/trailblazer-endpoint'
gem 'trailblazer-rails'

group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
Expand Down
67 changes: 67 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
GIT
remote: https://github.com/fernandes/reform.git
revision: 51b9be20254ef5d810a6e790f799619e1fb1599b
branch: feature/indexed_errors
specs:
reform (2.3.0.rc1)
disposable (>= 0.4.2, < 0.5.0)
representable (>= 2.4.0, < 3.1.0)
uber (< 0.2.0)

GIT
remote: https://github.com/trailblazer/reform-rails.git
revision: 4ad30a8042310c1f5fee75e24c047ae776c054de
specs:
reform-rails (0.2.0.rc2)
activemodel (>= 3.2)
reform (>= 2.3.0.rc1, < 3.0.0)

GIT
remote: https://github.com/trailblazer/trailblazer-endpoint.git
revision: 0c80379a4ae977aa427092aade2aa07882f9381d
specs:
trailblazer-endpoint (0.0.1)
dry-matcher

GEM
remote: https://rubygems.org/
specs:
Expand Down Expand Up @@ -78,13 +103,24 @@ GEM
coffee-script-source (1.12.2)
concurrent-ruby (1.0.5)
crass (1.0.4)
declarative (0.0.10)
declarative-builder (0.1.0)
declarative-option (< 0.2.0)
declarative-option (0.1.0)
devise (4.5.0)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0, < 6.0)
responders
warden (~> 1.2.3)
disposable (0.4.4)
declarative (>= 0.0.9, < 1.0.0)
declarative-builder (< 0.2.0)
declarative-option (< 0.2.0)
representable (>= 2.4.0, <= 3.1.0)
uber (< 0.2.0)
docile (1.3.1)
dry-matcher (0.7.0)
erubi (1.7.1)
execjs (2.7.0)
ffi (1.9.25)
Expand Down Expand Up @@ -115,6 +151,7 @@ GEM
guard (~> 2.0)
guard-compat (~> 1.1)
spring
hirb (0.7.3)
i18n (1.1.1)
concurrent-ruby (~> 1.0)
io-like (0.3.0)
Expand Down Expand Up @@ -190,6 +227,10 @@ GEM
rb-fsevent (0.10.3)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
responders (2.4.0)
actionpack (>= 4.2.0, < 5.3)
railties (>= 4.2.0, < 5.3)
Expand Down Expand Up @@ -230,11 +271,33 @@ GEM
thor (0.20.0)
thread_safe (0.3.6)
tilt (2.0.8)
trailblazer (2.1.0.rc1)
declarative
trailblazer-macro (>= 2.1.0.rc1, < 2.2.0)
trailblazer-macro-contract (= 2.1.0.rc1)
trailblazer-operation
trailblazer-activity (0.7.1)
hirb
trailblazer-context
trailblazer-context (0.1.2)
trailblazer-loader (0.1.2)
trailblazer-macro (2.1.0.rc1)
trailblazer-macro-contract (2.1.0.rc1)
reform (>= 2.2.0, < 3.0.0)
trailblazer-operation (0.4.1)
trailblazer-activity (>= 0.7.1, < 0.8.0)
trailblazer-context (>= 0.1.1, < 0.3.0)
trailblazer-rails (2.1.5)
activesupport (>= 5.0.0)
reform-rails (>= 0.1.4, < 0.2.0)
trailblazer (>= 2.1.0.beta1, < 2.2.0)
trailblazer-loader (>= 0.1.0)
turbolinks (5.2.0)
turbolinks-source (~> 5.2)
turbolinks-source (5.2.0)
tzinfo (1.2.5)
thread_safe (~> 0.1)
uber (0.1.0)
uglifier (4.1.19)
execjs (>= 0.3.0, < 3)
warden (1.2.7)
Expand Down Expand Up @@ -276,11 +339,15 @@ DEPENDENCIES
pry-rails
puma (~> 3.11)
rails (~> 5.2.1)
reform!
reform-rails!
sass-rails (~> 5.0)
selenium-webdriver
simplecov
spring
spring-watcher-listen (~> 2.0.0)
trailblazer-endpoint!
trailblazer-rails
turbolinks (~> 5)
tzinfo-data
uglifier (>= 1.3.0)
Expand Down
18 changes: 18 additions & 0 deletions app/concepts/api/v1/contacts/contract/create.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module Api::V1::Contacts::Contract
class Create < Reform::Form
include Api::V1::Contacts::Representer::ContactModule

validates :first_name, presence: true, length: {minimum: 1}
validates :last_name, presence: true, length: {minimum: 1}

property :address, populate_if_empty: Address do
include Api::V1::Contacts::Representer::AddressModule
end

collection :telephones, populate_if_empty: Telephone do
include Api::V1::Contacts::Representer::TelephoneModule

validates :number, presence: true
end
end
end
8 changes: 8 additions & 0 deletions app/concepts/api/v1/contacts/operation/create.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class Api::V1::Contacts::Create < Trailblazer::Operation
step ->(options, params) { options["representer.render.class"] = Api::V1::Contacts::Representer::Create }
step ->(options, params) { options["representer.errors.class"] = ErrorsRepresenter }
step Model( Contact , :new )
step Contract::Build(constant: Api::V1::Contacts::Contract::Create)
step Contract::Validate()
step Contract::Persist()
end
4 changes: 4 additions & 0 deletions app/concepts/api/v1/contacts/operation/destroy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class Api::V1::Contacts::Destroy < Trailblazer::Operation
step Model( Contact , :find )
step ->(options, params) { options[:model].destroy! }
end
5 changes: 5 additions & 0 deletions app/concepts/api/v1/contacts/operation/index.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Api::V1::Contacts::Index < Trailblazer::Operation
step ->(options, params) { options["representer.render.class"] = Api::V1::Contacts::Representer::Index }
step ->(options, params) { options["representer.errors.class"] = ErrorsRepresenter }
step ->(options, params) { options[:model] = Contact.all }
end
15 changes: 15 additions & 0 deletions app/concepts/api/v1/contacts/operation/show.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class Api::V1::Contacts::Show < Trailblazer::Operation
step ->(options, params) { options["representer.render.class"] = Api::V1::Contacts::Representer::Create }
step ->(options, params) { options["representer.errors.class"] = ErrorsRepresenter }
step Rescue(ActiveRecord::RecordNotFound, handler: :not_found_message!) {
step Model( Contact , :find )
}

def not_found_message!(exception, options)
options[:error] = {
"message": "Not Found",
"documentation_url": "to be documented"
}
end

end
8 changes: 8 additions & 0 deletions app/concepts/api/v1/contacts/operation/update.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class Api::V1::Contacts::Update < Trailblazer::Operation
step ->(options, params) { options["representer.render.class"] = Api::V1::Contacts::Representer::Create }
step ->(options, params) { options["representer.errors.class"] = ErrorsRepresenter }
step Model( Contact , :find )
step Contract::Build(constant: Api::V1::Contacts::Contract::Create)
step Contract::Validate()
step Contract::Persist()
end
13 changes: 13 additions & 0 deletions app/concepts/api/v1/contacts/representer/address_module.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Api::V1::Contacts::Representer
module AddressModule
include Representable::JSON
include Reform::Form::Module

property :id, writeable: false
property :street_address
property :neighborhood
property :city
property :state
property :country
end
end
9 changes: 9 additions & 0 deletions app/concepts/api/v1/contacts/representer/contact_module.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Api::V1::Contacts::Representer
module ContactModule
include Reform::Form::Module

property :id, writeable: false
property :first_name
property :last_name
end
end
14 changes: 14 additions & 0 deletions app/concepts/api/v1/contacts/representer/create.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module Api::V1::Contacts::Representer
module Create
include Representable::JSON
include ContactModule

property :address, populate_if_empty: Address do
include AddressModule
end

collection :telephones, populate_if_empty: Telephone do
include TelephoneModule
end
end
end
18 changes: 18 additions & 0 deletions app/concepts/api/v1/contacts/representer/index.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module Api::V1::Contacts::Representer
module Index
include Representable::JSON::Collection

items class: Contact do
include Representable::JSON
include ContactModule

property :address, populate_if_empty: Address do
include AddressModule
end

collection :telephones, populate_if_empty: Telephone do
include TelephoneModule
end
end
end
end
10 changes: 10 additions & 0 deletions app/concepts/api/v1/contacts/representer/telephone_module.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module Api::V1::Contacts::Representer
module TelephoneModule
include Representable::JSON
include Reform::Form::Module

property :id, writeable: false
property :number
property :label
end
end
21 changes: 21 additions & 0 deletions app/controllers/api/v1/contacts_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class Api::V1::ContactsController < ApiController
def index
endpoint Api::V1::Contacts::Index
end

def create
endpoint Api::V1::Contacts::Create
end

def show
endpoint Api::V1::Contacts::Show
end

def update
endpoint Api::V1::Contacts::Update
end

def destroy
endpoint Api::V1::Contacts::Destroy
end
end
23 changes: 23 additions & 0 deletions app/controllers/api_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
require 'trailblazer/operation'

class ApiController < ActionController::Base
protect_from_forgery with: :null_session

protected

def default_handler
->(m) do
m.destroyed { |result| head :no_content }
# m.present { |result| render json: result[:model].extend(result['representer.render.class']).to_json }
m.created { |result| render json: result[:model].extend(result['representer.render.class']).to_json, status: :created }
m.success { |result| render json: result[:model].extend(result['representer.render.class']).to_json }
m.invalid { |result| render json: result["representer.errors.class"].new(result["result.contract.default"].errors.messages).to_json, status: :unprocessable_entity }
m.not_found { |result| render json: { error: result[:error] }, status: :not_found }
# m.unauthenticated { |result| render json: result[:model].extend(result['representer.render.class']).to_json }
end
end

def endpoint(operation_class, options={}, &block)
Api::Endpoint.(operation_class, default_handler, {params: params.to_unsafe_h}, &block)
end
end
34 changes: 34 additions & 0 deletions app/endpoints/api/endpoint.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module Api
class Endpoint < Trailblazer::Endpoint
# this is totally WIP as we need to find best practices.
# also, i want this to be easily extendable.
Matcher = Dry::Matcher.new(
destroyed: Dry::Matcher::Case.new( # DISCUSS: the "present" flag needs some discussion.
match: ->(result) { result.success? && result[:model].try(:destroyed?) },
resolve: ->(result) { result }),
# present: Dry::Matcher::Case.new( # DISCUSS: the "present" flag needs some discussion.
# match: ->(result) { result.success? && result["present"] },
# resolve: ->(result) { result }),
success: Dry::Matcher::Case.new(
match: ->(result) { result.success? },
resolve: ->(result) { result }),
created: Dry::Matcher::Case.new(
match: ->(result) { result.success? && result["model.action"] == :new }, # the "model.action" doesn't mean you need Model.
resolve: ->(result) { result }),
not_found: Dry::Matcher::Case.new(
match: ->(result) { result.failure? && result[:model].nil? },
resolve: ->(result) { result }),
# TODO: we could add unauthorized here.
# unauthenticated: Dry::Matcher::Case.new(
# match: ->(result) { result.failure? }, # FIXME: we might need a &. here ;)
# resolve: ->(result) { result }),
invalid: Dry::Matcher::Case.new(
match: ->(result) { result.failure? && result["result.contract.default"] && result["result.contract.default"].failure? },
resolve: ->(result) { result })
)

def matcher
Api::Endpoint::Matcher
end
end
end
2 changes: 2 additions & 0 deletions app/helpers/api/v1/contacts_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module Api::V1::ContactsHelper
end
2 changes: 2 additions & 0 deletions app/models/contact.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@
#

class Contact < ApplicationRecord
has_one :address, dependent: :destroy
has_many :telephones, dependent: :destroy
end
6 changes: 6 additions & 0 deletions app/representers/errors_representer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
require 'representable/json/hash'

class ErrorsRepresenter < Representable::Decorator
include Representable::JSON::Hash
self.representation_wrap = :errors
end
5 changes: 5 additions & 0 deletions config/initializers/reform.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Use reform indexed errors
Reform::Contract::Result::Errors.index_errors = true

# Set errors wrapper in initialization
ErrorsRepresenter.representation_wrap = :errors
5 changes: 5 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :contacts
end
end
get 'home/show'
devise_for :users
root to: 'home#show'
Expand Down
Loading