shamu 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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