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 +4 -4
- data/lib/shamu/attributes.rb +19 -3
- data/lib/shamu/json_api/base_builder.rb +14 -9
- data/lib/shamu/json_api/context.rb +89 -8
- data/lib/shamu/json_api/error.rb +6 -0
- data/lib/shamu/json_api/error_builder.rb +6 -2
- data/lib/shamu/json_api/presenter.rb +18 -0
- data/lib/shamu/json_api/response.rb +38 -19
- data/lib/shamu/json_api.rb +1 -2
- data/lib/shamu/locale/en.yml +3 -2
- data/lib/shamu/rails/json_api.rb +217 -0
- data/lib/shamu/rails/railtie.rb +1 -0
- data/lib/shamu/rails.rb +1 -0
- data/lib/shamu/version.rb +1 -1
- data/shamu.gemspec +1 -1
- data/spec/lib/shamu/json_api/context_spec.rb +35 -0
- data/spec/lib/shamu/json_api/response_spec.rb +7 -7
- data/spec/lib/shamu/rails/json_api_spec.rb +106 -0
- metadata +8 -7
- data/lib/shamu/json_api/entity_serializer.rb +0 -10
- data/lib/shamu/json_api/serializer.rb +0 -33
- data/lib/shamu/json_api/support.rb +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7dbbdb9cc8883493b91766988eafe575fdc6d7b4
|
4
|
+
data.tar.gz: c4c2e75d33b28dd7759d656d60c5b1dd444a3dc7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ff33cde446d56d6a4d0930f0cd74d00b9a893af315bf2dcdcfb70f0289f33d53cdcfa93c229df4672c54947d1ec409cd8fb195913607be34b8432d972fc49b8d
|
7
|
+
data.tar.gz: e47d92497b0485d0cb356550d932aacce2f2a8a47177b2f3e51f7ff960ed2d990301f00a176751ca4b4ff9861edbcaa784de6da7bacad103b4f92a1be907de12
|
data/lib/shamu/attributes.rb
CHANGED
@@ -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
|
-
|
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 [
|
15
|
-
# not provided a default {
|
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,
|
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] ||=
|
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
|
-
# @
|
43
|
-
|
44
|
-
|
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
|
data/lib/shamu/json_api/error.rb
CHANGED
@@ -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 ||=
|
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 [
|
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 [
|
15
|
-
def resource( resource,
|
16
|
-
output[:data] = build_resource( resource,
|
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
|
20
|
+
# Output a multiple resources as the response data.
|
20
21
|
#
|
21
22
|
# @param [Enumerable<Object>] resources to write.
|
22
|
-
# @param [
|
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 [
|
28
|
-
def
|
28
|
+
# @return [self]
|
29
|
+
def collection( collection, presenter = nil, &block )
|
29
30
|
output[:data] =
|
30
31
|
collection.map do |resource|
|
31
|
-
build_resource resource,
|
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 [
|
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
|
-
|
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[:
|
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
|
-
|
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
|
-
|
95
|
+
to_json
|
78
96
|
end
|
79
97
|
|
80
|
-
|
98
|
+
# Responses don't have identifiers
|
99
|
+
undef :identifier
|
81
100
|
|
82
101
|
private
|
83
102
|
|
84
|
-
def build_resource( resource,
|
85
|
-
|
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
|
89
|
-
|
107
|
+
if presenter
|
108
|
+
presenter.present( resource, builder )
|
90
109
|
else
|
91
110
|
yield builder
|
92
111
|
end
|
data/lib/shamu/json_api.rb
CHANGED
@@ -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/
|
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
|
data/lib/shamu/locale/en.yml
CHANGED
@@ -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
|
data/lib/shamu/rails/railtie.rb
CHANGED
@@ -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
data/lib/shamu/version.rb
CHANGED
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.
|
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
|
8
|
-
|
9
|
-
expect(
|
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,
|
13
|
+
response.resource double, presenter
|
14
14
|
end
|
15
15
|
|
16
|
-
it "expects a block if no
|
16
|
+
it "expects a block if no presenter" do
|
17
17
|
expect do
|
18
18
|
response.resource double
|
19
|
-
end.to raise_error
|
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.
|
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-
|
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.
|
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.
|
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,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
|