shamu 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
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