shamu 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -9,160 +9,131 @@ module Shamu
9
9
 
10
10
  included do
11
11
  before_action do
12
- json_api error: "The 'include' parameter is not supported", status: :bad_request if params[:include]
12
+ render json: json_error( "The 'include' parameter is not supported" ), status: :bad_request if params[:include] # rubocop:disable Metrics/LineLength
13
13
  end
14
14
  end
15
15
 
16
- private
17
-
18
- # @!visibility public
19
- #
20
- # Writes a single resource as a well-formed JSON API response.
21
- #
22
- # @param [Object] resource to present as JSON.
23
- # @param [JsonApi::Presenter] presenter to use when building the
24
- # response. If not given, attempts to find a presenter. See
25
- # {#json_context}
26
- # @param (see #json_context)
27
- # @yield (response) write additional top-level links and meta
28
- # information.
29
- # @yieldparam [JsonApi::Response] response
30
- # @return [JsonApi::Response] the presented json response.
31
- def json_resource( resource, presenter = nil, **context, &block )
32
- context = json_context( **context )
33
- response = Shamu::JsonApi::Response.new( context )
34
- response.resource resource, presenter
35
- yield response if block_given?
36
- response
37
- end
16
+ def process_action( * )
17
+ # If no format has been specfied, default to json_api
18
+ request.parameters[:format] ||= "json_api"
19
+ super
20
+ end
38
21
 
39
- # @!visibility public
40
- #
41
- # Writes a single resource as a well-formed JSON API response.
42
- #
43
- # @param [Enumerabl<Object>] resources to present as a JSON array.
44
- # @param [JsonApi::Presenter] presenter to use when building the
45
- # response. If not given, attempts to find a presenter. See
46
- # {#json_context}
47
- # @param (see #json_context)
48
- # @yield (response) write additional top-level links and meta
49
- # information.
50
- # @yieldparam [JsonApi::Response] response
51
- # @return [JsonApi::Response] the presented json response.
52
- def json_collection( resources, presenter = nil, pagination: :auto, **context, &block )
53
- context = json_context( **context )
54
- response = Shamu::JsonApi::Response.new( context )
55
- response.collection resources, presenter
56
- json_paginate_resources response, resources, pagination
57
- yield response if block_given?
58
- response
59
- end
22
+ # Builds a well-formed JSON API response for a single resource.
23
+ #
24
+ # @param [Object] resource to present as JSON.
25
+ # @param [Class] presenter {Presenter} class to use when building the
26
+ # response for the given resource. If not given, attempts to find a
27
+ # presenter by calling {Context#find_presenter}.
28
+ # @param (see #json_context)
29
+ # @yield (response) write additional top-level links and meta
30
+ # information.
31
+ # @yieldparam [JsonApi::Response] response
32
+ # @return [JsonApi::Response] the presented JSON response.
33
+ def json_resource( resource, presenter = nil, **context, &block )
34
+ response = build_json_response( context )
35
+ response.resource resource, presenter
36
+ yield response if block_given?
37
+ response
38
+ end
60
39
 
61
- # @!visibility public
62
- #
63
- # Add page-based pagination links for the resources.
64
- #
65
- # @param [#current_page,#next_page,#previous_page] resources a collection that responds to `#current_page`
66
- # @param [JsonApi::BaseBuilder] builder to add links to.
67
- # @param [String] param the name of the page parameter to adjust for
68
- def json_paginate( resources, builder, param: "page[number]" )
69
- page = resources.current_page
70
-
71
- if resources.respond_to?( :next_page ) ? resources.next_page : true
72
- builder.link :next, url_for( params.reverse_merge( param => resources.current_page + 1 ) )
73
- end
40
+ # Builds a well-formed JSON API response for a collection of resources.
41
+ #
42
+ # @param [Enumerable<Object>] resources to present as a JSON array.
43
+ # @param [Class] presenter {Presenter} class to use when building the
44
+ # response for each of the resources. If not given, attempts to find
45
+ # a presenter by calling {Context#find_presenter}
46
+ # @param (see #json_context)
47
+ # @yield (response) write additional top-level links and meta
48
+ # information.
49
+ # @yieldparam [JsonApi::Response] response
50
+ # @return [JsonApi::Response] the presented JSON response.
51
+ def json_collection( resources, presenter = nil, pagination: :auto, **context, &block )
52
+ response = build_json_response( context )
53
+ response.collection resources, presenter
54
+ json_paginate_resources response, resources, pagination
55
+ yield response if block_given?
56
+ response
57
+ end
74
58
 
75
- if resources.respond_to?( :prev_page ) ? resources.prev_page : page > 1
76
- builder.link :prev, url_for( params.reverse_merge( param => resources.current_page - 1 ) )
77
- end
59
+ # Add page-based pagination links for the resources to the builder.
60
+ #
61
+ # @param [#current_page,#next_page,#previous_page] resources a collection that responds to `#current_page`
62
+ # @param [JsonApi::BaseBuilder] builder to add links to.
63
+ # @param [String] param the name of the page parameter to adjust for
64
+ # @return [void]
65
+ def json_paginate( resources, builder, param: "page[number]" )
66
+ page = resources.current_page
67
+
68
+ if resources.respond_to?( :next_page ) ? resources.next_page : true
69
+ builder.link :next, url_for( params.reverse_merge( param => resources.current_page + 1 ) )
78
70
  end
79
71
 
80
- # @!visiblity public
81
- #
82
- # Write an error response. See {Shamu::JsonApi::Response#error} for details.
83
- #
84
- # @param (see Shamu::JsonApi::Response#error)
85
- # @return [Shamu::JsonApi::Response]
86
- # @yield (builder)
87
- # @yieldparam [Shamu::JsonApi::ErrorBuilder] builder to customize the
88
- # error response.
89
- def json_error( exception = nil, http_status = :auto, **context, &block )
90
- context = json_context( **context )
91
- response = Shamu::JsonApi::Response.new( context )
92
-
93
- http_status = json_http_status_code_from_error( exception ) if http_status == :auto
94
- http_status = ::Rack::Utils.status_code( http_status ) if http_status
95
- response.error( exception, http_status, &block )
96
- response
72
+ if resources.respond_to?( :prev_page ) ? resources.prev_page : page > 1
73
+ builder.link :prev, url_for( params.reverse_merge( param => resources.current_page - 1 ) )
97
74
  end
75
+ end
98
76
 
99
- # @!visibility public
100
- #
101
- # Buid a {JsonApi::Context} for the current request and controller.
102
- #
103
- # @param [Hash<Symbol,Array>] fields to include in the response. If not
104
- # provided looks for a `fields` request argument and parses that.
105
- # See {JsonApi::Context#initialize}.
106
- # @param [Array<String>] namespaces to look for {Presenter presenters}.
107
- # If not provided automatically adds the controller name and it's
108
- # namespace.
109
- #
110
- # For example in the `Users::AccountController` it will add the
111
- # `Users::Accounts` and `Users` namespaces.
112
- #
113
- # See {JsonApi::Context#find_presenter}.
114
- # @param [Hash<Class,Class>] presenters a hash that maps resource classes
115
- # to the presenter class to use when building responses. See
116
- # {JsonApi::Context#find_presenter}.
117
- def json_context( fields: :not_set, namespaces: :not_set, presenters: :not_set )
118
- Shamu::JsonApi::Context.new fields: fields == :not_set ? json_context_fields : fields,
119
- namespaces: namespaces == :not_set ? json_context_namespaces : namespaces,
120
- presenters: presenters == :not_set ? json_context_presenters : presenters
77
+ # Write an error response. See {Shamu::JsonApi::Response#error} for details.
78
+ #
79
+ # @param (see Shamu::JsonApi::Response#error)
80
+ # @yield (builder)
81
+ # @yieldparam [Shamu::JsonApi::ErrorBuilder] builder to customize the
82
+ # error response.
83
+ # @return [JsonApi::Response] the presented JSON response.
84
+ def json_error( error = nil, **context, &block )
85
+ response = build_json_response( context )
86
+
87
+ response.error error do |builder|
88
+ builder.http_status json_http_status_code_from_error( error )
89
+ yield builder if block_given?
121
90
  end
122
91
 
123
- # rubocop:disable Metrics/PerceivedComplexity
124
-
125
- # @!visibility public
126
- #
127
- # Render a JSON API response for a resource, collection or error.
128
- #
129
- # @overload json_api( error:, status: :auto, **context, &block )
130
- # @param [Exception] error an error to report
131
- # @param [Symbol,Integer] status the HTTP status code to return. If
132
- # :auto, attempts to determine the proper response from the
133
- # exception and request type.
134
- # @param (see #json_context)
135
- # @overload json_api( resource:, status: :auto, presenter: nil, **context, &block )
136
- # @param [Object] resource the resource to render.
137
- # @param [Symbol,Integer] status the HTTP status code. If :auto
138
- # attempts to determine the proper response from the request type.
139
- # @param (see #json_resource)
140
- # @param [Shamu::JsonApi::Presenter] presenter to use when serializing
141
- # the resource.
142
- # @overload json_api( collection:, status: :ok, presenter: nil, **context, &block )
143
- # @param [Array<Object>] collection to render.
144
- # @param [Symbol,Integer] statis HTTP status code.
145
- # @param (see #json_collection)
146
- # @param [Shamu::JsonApi::Presenter] presenter to use when serializing
147
- # each of the resources.
148
- def json_api( error: nil, resource: nil, collection: nil, status: :auto, presenter: nil, pagination: :auto, **context, &block ) # rubocop:disable Metrics/LineLength
149
- options = { layout: nil }
150
-
151
- options[:json] =
152
- if error
153
- status = json_http_status_code_from_error( error ) if status == :auto
154
- json_error( error, status, **context, &block )
155
- elsif collection
156
- status = :ok if status == :auto
157
- json_collection( collection, presenter, pagination: pagination, **context, &block )
158
- else
159
- status = json_http_status_code_from_request if status == :auto
160
- json_resource( resource, presenter, **context, &block )
161
- end
92
+ response
93
+ end
162
94
 
163
- options[:status] = status if status
164
- render options.merge( context.except( :fields, :namespaces, :presenters ) )
165
- end
95
+ # Write all the validation errors from a record to the response.
96
+ #
97
+ # @param (see Shamu::JsonApi::Response#validation_errors)
98
+ # @yield (builder, attr, message)
99
+ # @yieldparam (see Shamu::JsonApi::Response#validation_errors)
100
+ # @return [JsonApi::Response] the presented JSON response.
101
+ def json_validation_errors( errors, **context, &block )
102
+ response = build_json_response( context )
103
+ response.validation_errors errors, &block
104
+
105
+ response
106
+ end
107
+
108
+ JSON_CONTEXT_KEYWORDS = [ :fields, :namespaces, :presenters ].freeze
109
+
110
+ # @!visibility public
111
+ #
112
+ # Build a {JsonApi::Context} for the current request and controller.
113
+ #
114
+ # @param [Hash<Symbol,Array>] fields to include in the response. If not
115
+ # provided looks for a `fields` request argument and parses that.
116
+ # See {JsonApi::Context#initialize}.
117
+ # @param [Array<String>] namespaces to look for {Presenter presenters}.
118
+ # If not provided automatically adds the controller name and it's
119
+ # namespace.
120
+ #
121
+ # For example in the `Users::AccountController` it will add the
122
+ # `Users::Accounts` and `Users` namespaces.
123
+ #
124
+ # See {JsonApi::Context#find_presenter}.
125
+ # @param [Hash<Class,Class>] presenters a hash that maps resource classes
126
+ # to the presenter class to use when building responses. See
127
+ # {JsonApi::Context#find_presenter}.
128
+ # @return [JsonApi::Context] the builder context honoring any filter
129
+ # parameters sent by the client.
130
+ def json_context( fields: :not_set, namespaces: :not_set, presenters: :not_set )
131
+ Shamu::JsonApi::Context.new fields: fields == :not_set ? json_context_fields : fields,
132
+ namespaces: namespaces == :not_set ? json_context_namespaces : namespaces,
133
+ presenters: presenters == :not_set ? json_context_presenters : presenters
134
+ end
135
+
136
+ private
166
137
 
167
138
  def json_context_fields
168
139
  params[:fields]
@@ -212,6 +183,10 @@ module Shamu
212
183
  else :ok
213
184
  end
214
185
  end
186
+
187
+ def build_json_response( context )
188
+ Shamu::JsonApi::Response.new( json_context( **context.slice( *JSON_CONTEXT_KEYWORDS ) ) )
189
+ end
215
190
  end
216
191
  end
217
192
  end
@@ -0,0 +1,53 @@
1
+ module Shamu
2
+ module Rails
3
+
4
+ # Support JSON API responses with the standard rails `#respond_with` method.
5
+ module JsonApiResponder
6
+
7
+ # Render the response as JSON
8
+ # @return [String]
9
+ def to_json
10
+ if has_errors?
11
+ display_errors
12
+ elsif get?
13
+ display resource
14
+ elsif put? || patch?
15
+ display resource, :location => api_location
16
+ elsif post?
17
+ display resource, :status => :created, :location => api_location
18
+ else
19
+ head :no_content
20
+ end
21
+ end
22
+ alias_method :to_json_api, :to_json
23
+
24
+ protected
25
+
26
+ # @visibility private
27
+ def display( resource, given_options = {} )
28
+ given_options.merge!( options )
29
+
30
+ json =
31
+ if resource.is_a?( Enumerable )
32
+ controller.json_collection resource, **given_options
33
+ else
34
+ controller.json_resource resource, **given_options
35
+ end
36
+
37
+ super json, given_options
38
+ end
39
+
40
+ # @visibility private
41
+ def display_errors
42
+ controller.render format => controller.json_validation_errors( resource_errors ), :status => :unprocessable_entity # rubocop:disable Metrics/LineLength
43
+ end
44
+
45
+ private
46
+
47
+ def validation_resource?( resource )
48
+ resource.respond_to?( :valid? ) && resource.respond_to?( :errors )
49
+ end
50
+
51
+ end
52
+ end
53
+ end
@@ -1,4 +1,5 @@
1
1
  require "shamu/rack"
2
+ require "shamu/json_api"
2
3
 
3
4
  module Shamu
4
5
  module Rails
@@ -16,7 +17,13 @@ module Shamu
16
17
  ::ActionController::Base.send :include, Shamu::Rails::Controller
17
18
  ::ActionController::Base.send :include, Shamu::Rails::Entity
18
19
  ::ActionController::Base.send :include, Shamu::Rails::Features
19
- ::ActionController::Base.send :include, Shamu::Rails::JsonApi
20
+
21
+ Mime::Type.register Shamu::JsonApi::MIME_TYPE, :json_api
22
+
23
+ ActionController::Renderers.add :json_api do |obj, _options|
24
+ self.content_type ||= Mime[:json_api]
25
+ obj
26
+ end
20
27
  end
21
28
  end
22
29
 
data/lib/shamu/rails.rb CHANGED
@@ -6,6 +6,7 @@ module Shamu
6
6
  require "shamu/rails/controller"
7
7
  require "shamu/rails/features"
8
8
  require "shamu/rails/json_api"
9
+ require "shamu/rails/json_api_responder"
9
10
  require "shamu/rails/railtie"
10
11
  end
11
12
  end
data/lib/shamu/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module Shamu
3
3
  # The primary version number
4
- VERSION_NUMBER = "0.0.4".freeze
4
+ VERSION_NUMBER = "0.0.5".freeze
5
5
 
6
6
  # Version suffix such as 'beta' or 'alpha'
7
7
  VERSION_SUFFIX = "".freeze
@@ -4,18 +4,6 @@ describe Shamu::JsonApi::BaseBuilder do
4
4
  let( :context ) { Shamu::JsonApi::Context.new }
5
5
  let( :builder ) { Shamu::JsonApi::BaseBuilder.new( context ) }
6
6
 
7
- before( :each ) do
8
- builder.identifier "example", 1
9
- end
10
-
11
- describe "#identifier" do
12
- it "writes type and id" do
13
- builder.identifier "spec", 5
14
-
15
- expect( builder.compile ).to include type: "spec", id: "5"
16
- end
17
- end
18
-
19
7
  describe "#link" do
20
8
  it "adds a link" do
21
9
  builder.link :self, "http://localhost"
@@ -32,11 +20,4 @@ describe Shamu::JsonApi::BaseBuilder do
32
20
  end
33
21
  end
34
22
 
35
- describe "#compile" do
36
- it "fails if identifier has not been specified" do
37
- expect do
38
- Shamu::JsonApi::BaseBuilder.new( context ).compile
39
- end.to raise_error Shamu::JsonApi::IncompleteResourceError
40
- end
41
- end
42
23
  end
@@ -43,18 +43,18 @@ describe Shamu::JsonApi::Context do
43
43
  klass = Class.new( Shamu::JsonApi::Presenter )
44
44
 
45
45
  context = Shamu::JsonApi::Context.new presenters: { String => klass }
46
- expect( context.find_presenter( "Ms. Piggy" ) ).to be_a klass
46
+ expect( context.find_presenter( "Ms. Piggy" ) ).to eq klass
47
47
  end
48
48
 
49
49
  it "finds implicitly named presenter in namespaces" do
50
50
  context = Shamu::JsonApi::Context.new namespaces: [ "JsonApiContextSpec" ]
51
- expect( context.find_presenter( :symbols ) ).to be_a JsonApiContextSpec::SymbolPresenter
51
+ expect( context.find_presenter( :symbols ) ).to eq JsonApiContextSpec::SymbolPresenter
52
52
  end
53
53
 
54
54
  it "finds implicitly named model_name presenter in namespaces" do
55
55
  resource = double model_name: ActiveModel::Name.new( Class, nil, "JsonApiContextSpec::Symbol" )
56
56
  context = Shamu::JsonApi::Context.new namespaces: [ "JsonApiContextSpec" ]
57
- expect( context.find_presenter( resource ) ).to be_a JsonApiContextSpec::SymbolPresenter
57
+ expect( context.find_presenter( resource ) ).to eq JsonApiContextSpec::SymbolPresenter
58
58
  end
59
59
 
60
60
  it "raises if no presenter can be found" do
@@ -3,14 +3,6 @@ require "spec_helper"
3
3
  describe Shamu::JsonApi::ErrorBuilder do
4
4
  let( :builder ) { Shamu::JsonApi::ErrorBuilder.new }
5
5
 
6
- describe "#summary" do
7
- it "infers title" do
8
- builder.summary 422, :not_allowed
9
-
10
- expect( builder.compile ).to include title: "Not Allowed"
11
- end
12
- end
13
-
14
6
  describe "#exception" do
15
7
  before( :each ) do
16
8
  builder.exception NotImplementedError.new( "Nope, we haven't done that yet" )
@@ -1,5 +1,27 @@
1
1
  require "spec_helper"
2
2
 
3
3
  describe Shamu::JsonApi::RelationshipBuilder do
4
+ let( :context ) { Shamu::JsonApi::Context.new }
5
+ let( :builder ) { Shamu::JsonApi::RelationshipBuilder.new( context ) }
6
+
7
+ before( :each ) do
8
+ builder.identifier "example", 1
9
+ end
10
+
11
+ describe "#compile" do
12
+ it "fails if identifier has not been specified" do
13
+ expect do
14
+ Shamu::JsonApi::RelationshipBuilder.new( context ).compile
15
+ end.to raise_error Shamu::JsonApi::IncompleteResourceError
16
+ end
17
+ end
18
+
19
+ describe "#identifier" do
20
+ it "writes type and id" do
21
+ builder.identifier "spec", 5
22
+
23
+ expect( builder.compile ).to include data: hash_including( type: "spec", id: "5" )
24
+ end
25
+ end
4
26
 
5
27
  end
@@ -4,10 +4,19 @@ describe Shamu::JsonApi::RelationshipBuilder do
4
4
  let( :context ) { Shamu::JsonApi::Context.new }
5
5
  let( :builder ) { Shamu::JsonApi::ResourceBuilder.new( context ) }
6
6
 
7
+
7
8
  before( :each ) do
8
9
  builder.identifier "example", 1
9
10
  end
10
11
 
12
+ describe "#identifier" do
13
+ it "writes type and id" do
14
+ builder.identifier "spec", 5
15
+
16
+ expect( builder.compile ).to include type: "spec", id: "5"
17
+ end
18
+ end
19
+
11
20
  describe "#attribute" do
12
21
  it "adds to the attributes node" do
13
22
  builder.attribute name: "Jim"
@@ -49,4 +58,12 @@ describe Shamu::JsonApi::RelationshipBuilder do
49
58
  end
50
59
  end
51
60
 
61
+ describe "#compile" do
62
+ it "fails if identifier has not been specified" do
63
+ expect do
64
+ Shamu::JsonApi::ResourceBuilder.new( context ).compile
65
+ end.to raise_error Shamu::JsonApi::IncompleteResourceError
66
+ end
67
+ end
68
+
52
69
  end
@@ -1,14 +1,21 @@
1
1
  require "spec_helper"
2
+ require "active_model"
2
3
 
3
4
  describe Shamu::JsonApi::Response do
4
5
  let( :context ) { Shamu::JsonApi::Context.new }
5
6
  let( :response ) { Shamu::JsonApi::Response.new context }
6
7
 
7
8
  it "uses presenter if given" do
8
- presenter = double Shamu::JsonApi::Presenter
9
- expect( presenter ).to receive( :present ) do |_, builder|
10
- builder.identifier :response, 9
11
- end.with( anything, kind_of( Shamu::JsonApi::ResourceBuilder ) )
9
+ presenter = double
10
+ expect( presenter ).to receive( :new ) do |resource, builder|
11
+ instance = Shamu::JsonApi::Presenter.new resource, builder
12
+
13
+ expect( instance ).to receive( :present ) do
14
+ builder.identifier :response, 9
15
+ end
16
+
17
+ instance
18
+ end
12
19
 
13
20
  response.resource double, presenter
14
21
  end
@@ -39,4 +46,27 @@ describe Shamu::JsonApi::Response do
39
46
 
40
47
  expect( response.compile ).to include errors: [ hash_including( code: "not_implemented" ) ]
41
48
  end
49
+
50
+ it "writes validation errors" do
51
+ klass = Class.new do
52
+ include ActiveModel::Validations
53
+
54
+ attr_reader :title
55
+ validates :title, presence: true
56
+
57
+ validate :general_problem
58
+
59
+ def general_problem
60
+ errors.add :base, "nope"
61
+ end
62
+ end
63
+
64
+ allow( klass ).to receive( :name ).and_return "Example"
65
+ record = klass.new
66
+ record.valid?
67
+
68
+ response.validation_errors record.errors
69
+ expect( response.compile ).to include errors: include( hash_including( source: { pointer: "/data/attributes/title" } ) ) # rubocop:disable Metrics/LineLength
70
+ expect( response.compile ).to include errors: include( hash_including( source: { pointer: "/data" } ) )
71
+ end
42
72
  end