shamu 0.0.3 → 0.0.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 027f0ea99b484c02d9986999b39f3025dee4b4a3
4
- data.tar.gz: 0eb00f3878a9718c979e511e8930cf81d8d708e8
3
+ metadata.gz: 7dbbdb9cc8883493b91766988eafe575fdc6d7b4
4
+ data.tar.gz: c4c2e75d33b28dd7759d656d60c5b1dd444a3dc7
5
5
  SHA512:
6
- metadata.gz: 75946cc4efced52d7a3cf58bb673c75cdd2d9a7aee479b91f3fff4c96d485b74319f07c22345c616075a598948ee861eecd816046aefd37f9761321981c3d11e
7
- data.tar.gz: 63784d0e8b20db671ba1563c19146ecea4e2fa3728098fc1a79db42e8ef2ebf57a7e68716c9aa349676dff841b6c92f4c73fc81f660df3bbe3fca4d1a8b953ba
6
+ metadata.gz: ff33cde446d56d6a4d0930f0cd74d00b9a893af315bf2dcdcfb70f0289f33d53cdcfa93c229df4672c54947d1ec409cd8fb195913607be34b8432d972fc49b8d
7
+ data.tar.gz: e47d92497b0485d0cb356550d932aacce2f2a8a47177b2f3e51f7ff960ed2d990301f00a176751ca4b4ff9861edbcaa784de6da7bacad103b4f92a1be907de12
@@ -135,6 +135,11 @@ module Shamu
135
135
  @attributes ||= {}
136
136
  end
137
137
 
138
+ # @return [Hash] of all association {.attributes} defined on the class.
139
+ def associations
140
+ attributes.select { |_, v| v[:association] }
141
+ end
142
+
138
143
  def inherited( subclass )
139
144
  # Clone the base class's attributes into the subclass
140
145
  subclass.instance_variable_set :@attributes, attributes.dup
@@ -143,8 +148,8 @@ module Shamu
143
148
 
144
149
  # Define a new attribute for the class.
145
150
  #
146
- # @overload attribute(name, on:, default:, build: )
147
- # @overload attribute(name, build, on:, default:)
151
+ # @overload attribute(name, on:, default:, build:, &block )
152
+ # @overload attribute(name, build, on:, default:, &block)
148
153
  #
149
154
  # @param [Symbol] name of the attribute.
150
155
  # @param [Symbol] as an alias of the attribute.
@@ -174,10 +179,21 @@ module Shamu
174
179
  private :"fetch_#{ name }"
175
180
  private :"assign_#{ name }"
176
181
 
177
-
178
182
  self
179
183
  end
180
184
 
185
+ # Define an {.attribute} that defines an association to another resource
186
+ # that also has it's own attributes.
187
+ #
188
+ # @param (see .attribute)
189
+ # @yieldreturn (see .attribute)
190
+ # @return [self]
191
+ def association( name, *args, **options, &block )
192
+ options[:association] = true
193
+
194
+ attribute( name, *args, **options, &block )
195
+ end
196
+
181
197
  private
182
198
 
183
199
  # @return [Array<Symbol>] keys used by the {.attribute} method options
@@ -4,24 +4,30 @@ module Shamu
4
4
  # Used by a {Serilaizer} to write fields and relationships
5
5
  class BaseBuilder
6
6
 
7
+ # ============================================================================
8
+ # @!group Attributes
9
+ #
10
+
11
+ # @!attribute
12
+ # @return [Context] the JSON serialization context.
13
+ attr_reader :context
14
+
15
+ #
16
+ # @!endgroup Attributes
17
+
18
+
7
19
  # @param [Context] context the current serialization context.
8
20
  def initialize( context )
9
21
  @context = context
10
22
  @output = {}
11
23
  end
12
24
 
13
- # @overload identifier( type, id )
14
- # @param [String] type of the resource.
15
- # @param [Object] id of the resource.
16
- # @overload identifier( resource )
17
- # @param [#json_api_type,#id] resource an object that responds to `json_api_type` and `id`
18
- #
19
25
  # Write a resource linkage info.
20
26
  #
27
+ # @param [String] type of the resource.
28
+ # @param [Object] id of the resource.
21
29
  # @return [void]
22
30
  def identifier( type, id = nil )
23
- type, id = type.json_api_type, type.id if type.respond_to? :json_api_type
24
-
25
31
  output[:type] = type.to_s
26
32
  output[:id] = id.to_s
27
33
  end
@@ -59,7 +65,6 @@ module Shamu
59
65
 
60
66
  private
61
67
 
62
- attr_reader :context
63
68
  attr_reader :output
64
69
 
65
70
  end
@@ -2,28 +2,53 @@ module Shamu
2
2
  module JsonApi
3
3
  class Context
4
4
 
5
- def initialize( fields: nil )
5
+ # @param [Hash<Symbol,Array>] fields explicitly declare the attributes and
6
+ # resources that should be included in the response. The hash consists
7
+ # of a keys of the resource types and values as an array of fields to
8
+ # be included.
9
+ #
10
+ # A String value will be split on `,` to allow for easy parsing of
11
+ # HTTP query parameters.
12
+ #
13
+ # @param [Array<String>] namespaces to look for resource {Presenter
14
+ # presenters}. See {#find_presenter}.
15
+ #
16
+ # @param [Hash<Class,Class>] presenters a hash that maps resource classes
17
+ # to the presenter class to use when building responses. See
18
+ # {#find_presenter}.
19
+ def initialize( fields: nil, namespaces: [], presenters: {} )
6
20
  @included_resources = {}
7
21
  @all_resources = Set.new
8
22
  @fields = parse_fields( fields )
23
+ @namespaces = Array( namespaces )
24
+ @presenters = presenters || {}
9
25
  end
10
26
 
11
27
  # Add an included resource for a compound response.
12
28
  #
29
+ # If no `presenter` and no block are provided a default presenter will be
30
+ # obtained by calling {#find_presenter}.
31
+ #
13
32
  # @param [Object] resource to be serialized.
14
- # @param [Serializer] the serializer to use to serialize the object. If
15
- # not provided a default {Serializer} will be chosen.
33
+ # @param [Presenter] presenter to use to serialize the object. If
34
+ # not provided a default {Presenter} will be chosen.
16
35
  # @return [resource]
17
36
  # @yield (builder)
18
37
  # @yieldparam [ResourceBuilder] builder to write embedded resource to.
19
- def include_resource( resource, serializer = nil, &block )
38
+ def include_resource( resource, presenter = nil, &block )
20
39
  return if all_resources.include?( resource )
21
40
 
22
41
  all_resources << resource
23
- included_resources[resource] ||= { serializer: serializer, block: block }
42
+ included_resources[resource] ||= begin
43
+ presenter ||= find_presenter( resource ) unless block
44
+ { presenter: presenter, block: block }
45
+ end
24
46
  end
25
47
 
26
48
  # Collects all the currently included resources and resets the queue.
49
+ #
50
+ # @return [Array<Object,Hash>] returns the the resource and presentation
51
+ # options from each resource buffered with {#include_resource}.
27
52
  def collect_included_resources
28
53
  included = included_resources.dup
29
54
  @included_resources = {}
@@ -39,18 +64,52 @@ module Shamu
39
64
  #
40
65
  # @param [Symbol] type the resource type in question.
41
66
  # @param [Symbol] name of the field on the resouce in question.
42
- # @return [Boolean] true if the
43
- def include_field?( type, name )
44
- return true unless type_fields = fields[ type ]
67
+ # @param [Boolean] default true if the field should be included by default
68
+ # when no explicit fields have been selected.
69
+ # @return [Boolean] true if the field should be included.
70
+ def include_field?( type, name, default = true )
71
+ return default unless type_fields = fields[ type ]
45
72
 
46
73
  type_fields.include?( name )
47
74
  end
48
75
 
76
+ # Find a {Presenter} that can write the resource to a {ResourceBuilder}.
77
+ #
78
+ # - First looks for any explicit presenter given to the constructor that
79
+ # maps the resource's class to a specific presenter.
80
+ # - Next, looks through each of the namespaces given to the constructor.
81
+ # For each namespace, looks for a `Namespace::#{ resource.class.name
82
+ # }Presenter`. Will also check `resource.class.model_name.name` if
83
+ # available.
84
+ # - Fails with a {NoPresenter} error if a presenter cannot be found.
85
+ #
86
+ # @param [Object] resource to present.
87
+ # @return [Presenter]
88
+ # @raise [NoPresenter] if a presenter cannot be found.
89
+ def find_presenter( resource )
90
+ presenter = presenters[ resource.class ]
91
+ presenter ||= find_namespace_presenter( resource )
92
+
93
+ fail NoPresenter.new( resource, namespaces ) unless presenter
94
+
95
+ presenter.is_a?( Class ) ? presenter.new : presenter
96
+ end
97
+
98
+
99
+ # @return [Hash] of request param options to be output in the response meta.
100
+ def params_meta
101
+ return unless fields.any?
102
+
103
+ { fields: fields }
104
+ end
105
+
49
106
  private
50
107
 
51
108
  attr_reader :all_resources
52
109
  attr_reader :included_resources
53
110
  attr_reader :fields
111
+ attr_reader :namespaces
112
+ attr_reader :presenters
54
113
 
55
114
  def parse_fields( raw )
56
115
  return {} unless raw
@@ -64,6 +123,28 @@ module Shamu
64
123
  end
65
124
  end
66
125
  end
126
+
127
+ def find_namespace_presenter( resource )
128
+ presenter = find_namespace_presenter_for( resource.class.name.demodulize )
129
+ presenter ||= find_namespace_presenter_for( resource.model_name.element.camelize ) if resource.respond_to?( :model_name ) # rubocop:disable Metrics/LineLength
130
+ presenter ||= find_namespace_presenter_for( resource.class.model_name.element.camelize ) if resource.class.respond_to?( :model_name ) # rubocop:disable Metrics/LineLength
131
+ presenter
132
+ end
133
+
134
+ def find_namespace_presenter_for( name )
135
+ name = "#{ name }Presenter".to_sym
136
+
137
+ namespaces.each do |namespace|
138
+ begin
139
+ scope = namespace.constantize
140
+ return scope.const_get( name, false ) if scope.const_defined?( name, false )
141
+ rescue NameError # rubocop:disable Lint/HandleExceptions
142
+ end
143
+ end
144
+
145
+ nil
146
+ end
147
+
67
148
  end
68
149
  end
69
150
  end
@@ -25,5 +25,11 @@ module Shamu
25
25
  super
26
26
  end
27
27
  end
28
+
29
+ class NoPresenter < Error
30
+ def initialize( resource, namespaces )
31
+ super translate( :no_presenter, class: resource.class, namespaces: namespaces )
32
+ end
33
+ end
28
34
  end
29
35
  end
@@ -1,3 +1,5 @@
1
+ require "rack"
2
+
1
3
  module Shamu
2
4
  module JsonApi
3
5
 
@@ -17,7 +19,9 @@ module Shamu
17
19
  # @param [Integer] http_status code.
18
20
  # @param [String,Symbol] code application specific code for the error.
19
21
  # @param [String] human friendly title for the error.
20
- def summary( http_status, code, title = nil )
22
+ def summary( http_status, code = nil, title = nil )
23
+ code ||= ::Rack::Util::HTTP_STATUS_CODES[ code ].to_s.underscore
24
+
21
25
  output[:status] = http_status.to_s
22
26
  output[:code] = code.to_s
23
27
  output[:title] = title || code.to_s.titleize
@@ -27,7 +31,7 @@ module Shamu
27
31
  # @param [Exception] exception
28
32
  # @param [Integer] http_status code. Default 400.
29
33
  def exception( exception, http_status = nil )
30
- http_status ||= 400
34
+ http_status ||= 500
31
35
 
32
36
  name = exception.class.name.demodulize.gsub( /Error$/, "" )
33
37
  summary http_status, name.underscore, name.titleize
@@ -0,0 +1,18 @@
1
+ module Shamu
2
+ module JsonApi
3
+
4
+ # Present an object to a JSON API {ResourceBuilder builder}.
5
+ class Presenter
6
+
7
+ # Serialize the `resource` to the `builder`.
8
+ #
9
+ # @param [Object] resource to present.
10
+ # @param [ResourceBuilder] builder to write to.
11
+ # @return [void]
12
+ def present( resource, builder )
13
+ fail NotImplementedError
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -6,30 +6,32 @@ module Shamu
6
6
 
7
7
  # Output a single resource as the response data.
8
8
  # @param [Object] resource to write.
9
- # @param [Serializer] serializer used to write the resource state.
9
+ # @param [Presenter] presenter used to write the resource state.
10
10
  # @yield (builder)
11
11
  # @yieldparam [ResourceBuilder] builder used write the resources fields
12
12
  # and meta.
13
13
  #
14
- # @return [void]
15
- def resource( resource, serializer = nil, &block )
16
- output[:data] = build_resource( resource, serializer, &block )
14
+ # @return [self]
15
+ def resource( resource, presenter = nil, &block )
16
+ output[:data] = build_resource( resource, presenter, &block )
17
+ self
17
18
  end
18
19
 
19
- # Output a single resource as the response data.
20
+ # Output a multiple resources as the response data.
20
21
  #
21
22
  # @param [Enumerable<Object>] resources to write.
22
- # @param [Serializer] serializer used to write the resource state.
23
+ # @param [Presenter] presenter used to write the resource state.
23
24
  # @yield (builder, resource)
24
25
  # @yieldparam [ResourceBuilder] builder used write the resources fields
25
26
  # and meta.
26
27
  # @yieldparam [Object] resource being written.
27
- # @return [void]
28
- def resources( collection, serializer = nil, &block )
28
+ # @return [self]
29
+ def collection( collection, presenter = nil, &block )
29
30
  output[:data] =
30
31
  collection.map do |resource|
31
- build_resource resource, serializer, &block
32
+ build_resource resource, presenter, &block
32
33
  end
34
+ self
33
35
  end
34
36
 
35
37
  # @overload error( exception, http_status = nil )
@@ -38,18 +40,23 @@ module Shamu
38
40
  # @yield (builder)
39
41
  # @yieldparam [ErrorBuilder] builder used to describe the error.
40
42
  #
41
- # @return [void]
43
+ # @return [self]
42
44
  def error( exception = nil, http_status = nil, &block )
43
45
  builder = ErrorBuilder.new
44
46
 
45
47
  if block_given?
46
48
  yield builder
47
- else
49
+ elsif exception.is_a?( Exception )
48
50
  builder.exception( exception, http_status )
51
+ else
52
+ http_status ||= 500
53
+ builder.summary http_status, http_status.to_s, exception
49
54
  end
50
55
 
51
56
  errors = ( output[:errors] ||= [] )
52
57
  errors << builder.compile
58
+
59
+ self
53
60
  end
54
61
 
55
62
  # (see BaseBuilder#compile)
@@ -57,11 +64,15 @@ module Shamu
57
64
  @compiled ||= begin
58
65
  compiled = output.dup
59
66
  compiled[:jsonapi] = { version: "1.0" }
67
+ if params_meta = context.params_meta
68
+ compiled[:meta] ||= {}
69
+ compiled[:meta].reverse_merge!( params_meta )
70
+ end
60
71
 
61
72
  while context.included_resources?
62
73
  included = ( compiled[ :included ] ||= [] )
63
74
  context.collect_included_resources.each do |resource, options|
64
- included << build_resource( resource, options[:serializer], &options[:block] )
75
+ included << build_resource( resource, options[:presenter], &options[:block] )
65
76
  end
66
77
  end
67
78
 
@@ -69,24 +80,32 @@ module Shamu
69
80
  end
70
81
  end
71
82
 
72
- def to_json
83
+ # @return [Hash] the compiled resources.
84
+ def as_json( * )
85
+ compile.as_json
86
+ end
87
+
88
+ # @return [String]
89
+ def to_json( * )
73
90
  compile.to_json
74
91
  end
75
92
 
93
+ # @return [String]
76
94
  def to_s
77
- compile.to_s
95
+ to_json
78
96
  end
79
97
 
80
- private :identifier
98
+ # Responses don't have identifiers
99
+ undef :identifier
81
100
 
82
101
  private
83
102
 
84
- def build_resource( resource, serializer, &block )
85
- fail "A block is required if no serializer is given" if !serializer && !block_given?
103
+ def build_resource( resource, presenter, &block )
104
+ presenter = context.find_presenter( resource ) if !presenter && !block_given?
86
105
 
87
106
  builder = ResourceBuilder.new( context )
88
- if serializer
89
- serializer.serialize( builder )
107
+ if presenter
108
+ presenter.present( resource, builder )
90
109
  else
91
110
  yield builder
92
111
  end
@@ -5,8 +5,7 @@ module Shamu
5
5
  require "shamu/json_api/relationship_builder"
6
6
  require "shamu/json_api/resource_builder"
7
7
  require "shamu/json_api/response"
8
- require "shamu/json_api/serializer"
9
- require "shamu/json_api/support"
8
+ require "shamu/json_api/presenter"
10
9
  require "shamu/json_api/error"
11
10
  require "shamu/json_api/error_builder"
12
11
  end
@@ -28,5 +28,6 @@ en:
28
28
 
29
29
  json_api:
30
30
  errors:
31
- incomplete_resource: "`identifier` was not called to define the type and id of the resource"
32
- identifier_resource: "`identifier` must be called before defining any fields"
31
+ incomplete_resource: "`identifier` was not called to define the type and id of the resource."
32
+ identifier_resource: "`identifier` must be called before defining any fields."
33
+ no_presenter: No presenter available for %{class} objects. Looked in %{namespaces}.
@@ -0,0 +1,217 @@
1
+ require "rack"
2
+
3
+ module Shamu
4
+ module Rails
5
+
6
+ # Add support for writing resources as well-formed JSON API.
7
+ module JsonApi
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ before_action do
12
+ json_api error: "The 'include' parameter is not supported", status: :bad_request if params[:include]
13
+ end
14
+ end
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
38
+
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
60
+
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
74
+
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
78
+ end
79
+
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
97
+ end
98
+
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
121
+ end
122
+
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
162
+
163
+ options[:status] = status if status
164
+ render options.merge( context.except( :fields, :namespaces, :presenters ) )
165
+ end
166
+
167
+ def json_context_fields
168
+ params[:fields]
169
+ end
170
+
171
+ def json_context_namespaces
172
+ name = self.class.name.sub /Controller$/, ""
173
+ namespaces = [ name.pluralize ]
174
+ loop do
175
+ name = name.deconstantize
176
+ break if name.blank?
177
+
178
+ namespaces << name
179
+ end
180
+
181
+ namespaces
182
+ end
183
+
184
+ def json_context_presenters
185
+ end
186
+
187
+ def json_paginate_resources( response, resources, pagination )
188
+ pagination = resources.respond_to?( :current_page ) if pagination == :auto
189
+ return unless pagination
190
+
191
+ json_paginate resources, response
192
+ end
193
+
194
+ def json_http_status_code_from_error( error )
195
+ case error
196
+ when ActiveRecord::RecordNotFound then :not_found
197
+ when ActiveRecord::RecordInvalid then :unprocessable_entity
198
+ when /AccessDenied/ then :forbidden
199
+ else
200
+ if error.is_a?( Exception )
201
+ ActionDispatch::ExceptionWrapper.status_code_for_exception( error )
202
+ else
203
+ :bad_request
204
+ end
205
+ end
206
+ end
207
+
208
+ def json_http_status_code_from_request
209
+ case request.method
210
+ when "POST" then :created
211
+ when "HEAD" then :no_content
212
+ else :ok
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
@@ -16,6 +16,7 @@ module Shamu
16
16
  ::ActionController::Base.send :include, Shamu::Rails::Controller
17
17
  ::ActionController::Base.send :include, Shamu::Rails::Entity
18
18
  ::ActionController::Base.send :include, Shamu::Rails::Features
19
+ ::ActionController::Base.send :include, Shamu::Rails::JsonApi
19
20
  end
20
21
  end
21
22
 
data/lib/shamu/rails.rb CHANGED
@@ -5,6 +5,7 @@ module Shamu
5
5
  require "shamu/rails/entity"
6
6
  require "shamu/rails/controller"
7
7
  require "shamu/rails/features"
8
+ require "shamu/rails/json_api"
8
9
  require "shamu/rails/railtie"
9
10
  end
10
11
  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.3".freeze
4
+ VERSION_NUMBER = "0.0.4".freeze
5
5
 
6
6
  # Version suffix such as 'beta' or 'alpha'
7
7
  VERSION_SUFFIX = "".freeze
data/shamu.gemspec CHANGED
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
18
  spec.require_paths = ["lib"]
19
19
 
20
- spec.required_ruby_version = ">= 2.3.0"
20
+ spec.required_ruby_version = ">= 2.2.0"
21
21
 
22
22
 
23
23
  spec.add_dependency "activemodel", "~> 4.2"
@@ -1,5 +1,14 @@
1
1
  require "spec_helper"
2
2
 
3
+ module JsonApiContextSpec
4
+ class SymbolPresenter < Shamu::JsonApi::Presenter
5
+ end
6
+
7
+ class Resource
8
+ include Shamu::Attributes
9
+ end
10
+ end
11
+
3
12
  describe Shamu::JsonApi::Context do
4
13
  it "parses comma deliminated fields" do
5
14
  context = Shamu::JsonApi::Context.new fields: { "user" => "name, email," }
@@ -28,4 +37,30 @@ describe Shamu::JsonApi::Context do
28
37
  expect( context.include_field?( :user, :birthdate ) ).not_to be_truthy
29
38
  end
30
39
  end
40
+
41
+ describe "#find_presenter" do
42
+ it "finds explicitly defined presenter" do
43
+ klass = Class.new( Shamu::JsonApi::Presenter )
44
+
45
+ context = Shamu::JsonApi::Context.new presenters: { String => klass }
46
+ expect( context.find_presenter( "Ms. Piggy" ) ).to be_a klass
47
+ end
48
+
49
+ it "finds implicitly named presenter in namespaces" do
50
+ context = Shamu::JsonApi::Context.new namespaces: [ "JsonApiContextSpec" ]
51
+ expect( context.find_presenter( :symbols ) ).to be_a JsonApiContextSpec::SymbolPresenter
52
+ end
53
+
54
+ it "finds implicitly named model_name presenter in namespaces" do
55
+ resource = double model_name: ActiveModel::Name.new( Class, nil, "JsonApiContextSpec::Symbol" )
56
+ context = Shamu::JsonApi::Context.new namespaces: [ "JsonApiContextSpec" ]
57
+ expect( context.find_presenter( resource ) ).to be_a JsonApiContextSpec::SymbolPresenter
58
+ end
59
+
60
+ it "raises if no presenter can be found" do
61
+ expect do
62
+ Shamu::JsonApi::Context.new.find_presenter double
63
+ end.to raise_error Shamu::JsonApi::NoPresenter
64
+ end
65
+ end
31
66
  end
@@ -4,19 +4,19 @@ describe Shamu::JsonApi::Response do
4
4
  let( :context ) { Shamu::JsonApi::Context.new }
5
5
  let( :response ) { Shamu::JsonApi::Response.new context }
6
6
 
7
- it "uses serializer if given" do
8
- serializer = double Shamu::JsonApi::Serializer
9
- expect( serializer ).to receive( :serialize ) do |builder|
7
+ it "uses presenter if given" do
8
+ presenter = double Shamu::JsonApi::Presenter
9
+ expect( presenter ).to receive( :present ) do |_, builder|
10
10
  builder.identifier :response, 9
11
- end
11
+ end.with( anything, kind_of( Shamu::JsonApi::ResourceBuilder ) )
12
12
 
13
- response.resource double, serializer
13
+ response.resource double, presenter
14
14
  end
15
15
 
16
- it "expects a block if no serializer" do
16
+ it "expects a block if no presenter" do
17
17
  expect do
18
18
  response.resource double
19
- end.to raise_error /block/
19
+ end.to raise_error Shamu::JsonApi::NoPresenter
20
20
  end
21
21
 
22
22
  it "appends included resources" do
@@ -0,0 +1,106 @@
1
+ require "rails_helper"
2
+
3
+ module JsonApiControllerSpec
4
+
5
+ class Resource < Shamu::Entities::Entity
6
+ attribute :id
7
+ attribute :name
8
+ end
9
+
10
+ class ResourcesController < ActionController::Base
11
+ end
12
+
13
+ module Resources
14
+ class ResourcePresenter
15
+ def present( resource, builder )
16
+ builder.identifier :resource, resource.id
17
+ builder.attribute name: resource.name
18
+ end
19
+ end
20
+ end
21
+
22
+ end
23
+
24
+ describe JsonApiControllerSpec::ResourcesController, type: :controller do
25
+ controller JsonApiControllerSpec::ResourcesController do
26
+ def show
27
+ resource = JsonApiControllerSpec::Resource.new id: 562, name: "Example"
28
+ json_api resource: resource
29
+ end
30
+
31
+ def index
32
+ json_api collection: resources
33
+ end
34
+
35
+ def nope
36
+ json_api error: StandardError.new( "Nope" )
37
+ end
38
+
39
+ def resources
40
+ @resources ||= [ JsonApiControllerSpec::Resource.new( id: 562, name: "Example" ) ]
41
+ end
42
+
43
+ end
44
+
45
+ before( :each ) do
46
+ allow( controller ).to receive( :_routes ).and_return @routes
47
+ end
48
+
49
+ describe "#json_resource" do
50
+ subject do
51
+ get :show, id: 1, format: :json
52
+ JSON.parse( response.body )
53
+ end
54
+
55
+ it { is_expected.to include "data" => kind_of( Hash ) }
56
+ it { is_expected.to include "data" => hash_including( "attributes" => kind_of( Hash ) ) }
57
+
58
+ it "reflects fields param to meta" do
59
+ get :show, id: 1, fields: { people: "id,name" }
60
+ json = JSON.parse( response.body )
61
+ expect( json ).to include "meta" => hash_including( "fields" )
62
+ end
63
+
64
+ it "fails when 'include' paramter is given" do
65
+ get :show, id: 1, include: :contact
66
+
67
+ expect( response.code ).to eq "400"
68
+ expect( response.body ).to include "include"
69
+ end
70
+ end
71
+
72
+ describe "#json_collection" do
73
+ before( :each ) do
74
+ allow( controller.resources ).to receive( :current_page ).and_return 1
75
+ end
76
+
77
+ subject do
78
+ get :index, format: :json
79
+ JSON.parse( response.body )
80
+ end
81
+
82
+ it { is_expected.to include "data" => kind_of( Array ) }
83
+ it { is_expected.to include "links" => hash_including( "next" ) }
84
+ end
85
+
86
+ it "writes an error" do
87
+ routes.draw do
88
+ get "nope" => "json_api_controller_spec/resources#nope"
89
+ end
90
+
91
+ get :nope
92
+ json = JSON.parse( response.body )
93
+
94
+ expect( json ).to include "errors"
95
+ end
96
+
97
+ describe "#json_context" do
98
+ it "resolves namespaces from the controller" do
99
+ expect( controller.send( :json_context_namespaces ) ).to eq [
100
+ "JsonApiControllerSpec::Resources",
101
+ "JsonApiControllerSpec"
102
+ ]
103
+ end
104
+ end
105
+
106
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shamu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul Alexander
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-04-01 00:00:00.000000000 Z
11
+ date: 2016-04-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -273,14 +273,12 @@ files:
273
273
  - lib/shamu/json_api/README.md
274
274
  - lib/shamu/json_api/base_builder.rb
275
275
  - lib/shamu/json_api/context.rb
276
- - lib/shamu/json_api/entity_serializer.rb
277
276
  - lib/shamu/json_api/error.rb
278
277
  - lib/shamu/json_api/error_builder.rb
278
+ - lib/shamu/json_api/presenter.rb
279
279
  - lib/shamu/json_api/relationship_builder.rb
280
280
  - lib/shamu/json_api/resource_builder.rb
281
281
  - lib/shamu/json_api/response.rb
282
- - lib/shamu/json_api/serializer.rb
283
- - lib/shamu/json_api/support.rb
284
282
  - lib/shamu/locale/en.yml
285
283
  - lib/shamu/logger.rb
286
284
  - lib/shamu/rack.rb
@@ -293,6 +291,7 @@ files:
293
291
  - lib/shamu/rails/controller.rb
294
292
  - lib/shamu/rails/entity.rb
295
293
  - lib/shamu/rails/features.rb
294
+ - lib/shamu/rails/json_api.rb
296
295
  - lib/shamu/rails/railtie.rb
297
296
  - lib/shamu/rspec.rb
298
297
  - lib/shamu/rspec/matchers.rb
@@ -390,6 +389,7 @@ files:
390
389
  - spec/lib/shamu/rails/entity_spec.rb
391
390
  - spec/lib/shamu/rails/features.yml
392
391
  - spec/lib/shamu/rails/features_spec.rb
392
+ - spec/lib/shamu/rails/json_api_spec.rb
393
393
  - spec/lib/shamu/security/active_record_policy_spec.rb
394
394
  - spec/lib/shamu/security/hashed_value_spec.rb
395
395
  - spec/lib/shamu/security/policy_refinement_spec.rb
@@ -424,7 +424,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
424
424
  requirements:
425
425
  - - ">="
426
426
  - !ruby/object:Gem::Version
427
- version: 2.3.0
427
+ version: 2.2.0
428
428
  required_rubygems_version: !ruby/object:Gem::Requirement
429
429
  requirements:
430
430
  - - ">="
@@ -432,7 +432,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
432
432
  version: '0'
433
433
  requirements: []
434
434
  rubyforge_project:
435
- rubygems_version: 2.5.1
435
+ rubygems_version: 2.5.0
436
436
  signing_key:
437
437
  specification_version: 4
438
438
  summary: Have a whale of a good time adding Service Oriented Architecture to your
@@ -500,6 +500,7 @@ test_files:
500
500
  - spec/lib/shamu/rails/entity_spec.rb
501
501
  - spec/lib/shamu/rails/features.yml
502
502
  - spec/lib/shamu/rails/features_spec.rb
503
+ - spec/lib/shamu/rails/json_api_spec.rb
503
504
  - spec/lib/shamu/security/active_record_policy_spec.rb
504
505
  - spec/lib/shamu/security/hashed_value_spec.rb
505
506
  - spec/lib/shamu/security/policy_refinement_spec.rb
@@ -1,10 +0,0 @@
1
- module Shamu
2
- module JsonApi
3
-
4
- # A {Serializer} that can serialize all of the public attributes of an
5
- # {Entities::Entity}.
6
- class EntitySerializer < Serializer
7
-
8
- end
9
- end
10
- end
@@ -1,33 +0,0 @@
1
- module Shamu
2
- module JsonApi
3
-
4
- # Serialize an object to a JSON API stream.
5
- class Serializer
6
-
7
- # @param [Object] resource to be serialized.
8
- def initialize( resource )
9
- @resource = resource
10
- end
11
-
12
- # Serialize the {#resource} to the builder.
13
- # @param [ResourceBuilder] builder to write to.
14
- # @return [void]
15
- def serialize( builder )
16
- end
17
-
18
- private
19
-
20
- attr_reader :resource
21
-
22
- class << self
23
-
24
- # Find a {Serializer} that knows how to serialize the given resource.
25
- # @param [Object] resource to serialize.
26
- # @return [Serializer]
27
- def find( resource )
28
- end
29
- end
30
-
31
- end
32
- end
33
- end
@@ -1,8 +0,0 @@
1
- module Shamu
2
- module JsonApi
3
-
4
- # Add the ability to support JSON API serialization.
5
- module Support
6
- end
7
- end
8
- end