Skip to content

Commit

Permalink
Merge pull request #1 from fernandes/feature/contacts_rest_api
Browse files Browse the repository at this point in the history
Implement /api/v1/contacts REST API
  • Loading branch information
fernandes authored Oct 18, 2018
2 parents 2c8dfe0 + 437ffb4 commit f44e874
Show file tree
Hide file tree
Showing 27 changed files with 546 additions and 28 deletions.
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

0 comments on commit f44e874

Please sign in to comment.