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 +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
|