jsonapi_compliable 0.6.4 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.travis.yml +11 -3
  4. data/.yardopts +1 -0
  5. data/README.md +10 -1
  6. data/Rakefile +1 -0
  7. data/docs/JsonapiCompliable.html +202 -0
  8. data/docs/JsonapiCompliable/Adapters.html +119 -0
  9. data/docs/JsonapiCompliable/Adapters/Abstract.html +2285 -0
  10. data/docs/JsonapiCompliable/Adapters/ActiveRecord.html +2151 -0
  11. data/docs/JsonapiCompliable/Adapters/ActiveRecordSideloading.html +582 -0
  12. data/docs/JsonapiCompliable/Adapters/Null.html +1682 -0
  13. data/docs/JsonapiCompliable/Base.html +1395 -0
  14. data/docs/JsonapiCompliable/Deserializer.html +835 -0
  15. data/docs/JsonapiCompliable/Errors.html +115 -0
  16. data/docs/JsonapiCompliable/Errors/BadFilter.html +124 -0
  17. data/docs/JsonapiCompliable/Errors/StatNotFound.html +266 -0
  18. data/docs/JsonapiCompliable/Errors/UnsupportedPageSize.html +264 -0
  19. data/docs/JsonapiCompliable/Errors/ValidationError.html +124 -0
  20. data/docs/JsonapiCompliable/Extensions.html +117 -0
  21. data/docs/JsonapiCompliable/Extensions/BooleanAttribute.html +212 -0
  22. data/docs/JsonapiCompliable/Extensions/BooleanAttribute/ClassMethods.html +229 -0
  23. data/docs/JsonapiCompliable/Extensions/ExtraAttribute.html +242 -0
  24. data/docs/JsonapiCompliable/Extensions/ExtraAttribute/ClassMethods.html +237 -0
  25. data/docs/JsonapiCompliable/Query.html +1099 -0
  26. data/docs/JsonapiCompliable/Rails.html +211 -0
  27. data/docs/JsonapiCompliable/Resource.html +5241 -0
  28. data/docs/JsonapiCompliable/Scope.html +703 -0
  29. data/docs/JsonapiCompliable/Scoping.html +117 -0
  30. data/docs/JsonapiCompliable/Scoping/Base.html +843 -0
  31. data/docs/JsonapiCompliable/Scoping/DefaultFilter.html +318 -0
  32. data/docs/JsonapiCompliable/Scoping/ExtraFields.html +301 -0
  33. data/docs/JsonapiCompliable/Scoping/Filter.html +313 -0
  34. data/docs/JsonapiCompliable/Scoping/Filterable.html +364 -0
  35. data/docs/JsonapiCompliable/Scoping/Paginate.html +613 -0
  36. data/docs/JsonapiCompliable/Scoping/Sort.html +454 -0
  37. data/docs/JsonapiCompliable/SerializableTempId.html +216 -0
  38. data/docs/JsonapiCompliable/Sideload.html +2484 -0
  39. data/docs/JsonapiCompliable/Stats.html +117 -0
  40. data/docs/JsonapiCompliable/Stats/DSL.html +999 -0
  41. data/docs/JsonapiCompliable/Stats/Payload.html +391 -0
  42. data/docs/JsonapiCompliable/Util.html +117 -0
  43. data/docs/JsonapiCompliable/Util/FieldParams.html +228 -0
  44. data/docs/JsonapiCompliable/Util/Hash.html +471 -0
  45. data/docs/JsonapiCompliable/Util/IncludeParams.html +299 -0
  46. data/docs/JsonapiCompliable/Util/Persistence.html +435 -0
  47. data/docs/JsonapiCompliable/Util/RelationshipPayload.html +563 -0
  48. data/docs/JsonapiCompliable/Util/RenderOptions.html +250 -0
  49. data/docs/JsonapiCompliable/Util/ValidationResponse.html +532 -0
  50. data/docs/_index.html +527 -0
  51. data/docs/class_list.html +51 -0
  52. data/docs/css/common.css +1 -0
  53. data/docs/css/full_list.css +58 -0
  54. data/docs/css/style.css +492 -0
  55. data/docs/file.README.html +99 -0
  56. data/docs/file_list.html +56 -0
  57. data/docs/frames.html +17 -0
  58. data/docs/index.html +99 -0
  59. data/docs/js/app.js +248 -0
  60. data/docs/js/full_list.js +216 -0
  61. data/docs/js/jquery.js +4 -0
  62. data/docs/method_list.html +1731 -0
  63. data/docs/top-level-namespace.html +110 -0
  64. data/gemfiles/rails_5.gemfile +1 -1
  65. data/lib/jsonapi_compliable/adapters/abstract.rb +267 -4
  66. data/lib/jsonapi_compliable/adapters/active_record.rb +23 -1
  67. data/lib/jsonapi_compliable/adapters/null.rb +43 -3
  68. data/lib/jsonapi_compliable/base.rb +182 -33
  69. data/lib/jsonapi_compliable/deserializer.rb +90 -21
  70. data/lib/jsonapi_compliable/extensions/boolean_attribute.rb +12 -0
  71. data/lib/jsonapi_compliable/extensions/extra_attribute.rb +32 -0
  72. data/lib/jsonapi_compliable/extensions/temp_id.rb +11 -0
  73. data/lib/jsonapi_compliable/query.rb +94 -2
  74. data/lib/jsonapi_compliable/rails.rb +8 -0
  75. data/lib/jsonapi_compliable/resource.rb +548 -11
  76. data/lib/jsonapi_compliable/scope.rb +43 -1
  77. data/lib/jsonapi_compliable/scoping/base.rb +59 -8
  78. data/lib/jsonapi_compliable/scoping/default_filter.rb +33 -0
  79. data/lib/jsonapi_compliable/scoping/extra_fields.rb +33 -0
  80. data/lib/jsonapi_compliable/scoping/filter.rb +29 -2
  81. data/lib/jsonapi_compliable/scoping/filterable.rb +4 -0
  82. data/lib/jsonapi_compliable/scoping/paginate.rb +33 -0
  83. data/lib/jsonapi_compliable/scoping/sort.rb +18 -0
  84. data/lib/jsonapi_compliable/sideload.rb +229 -1
  85. data/lib/jsonapi_compliable/stats/dsl.rb +44 -0
  86. data/lib/jsonapi_compliable/stats/payload.rb +20 -0
  87. data/lib/jsonapi_compliable/util/field_params.rb +1 -0
  88. data/lib/jsonapi_compliable/util/hash.rb +18 -0
  89. data/lib/jsonapi_compliable/util/include_params.rb +22 -0
  90. data/lib/jsonapi_compliable/util/persistence.rb +13 -3
  91. data/lib/jsonapi_compliable/util/relationship_payload.rb +2 -0
  92. data/lib/jsonapi_compliable/util/render_options.rb +2 -0
  93. data/lib/jsonapi_compliable/util/validation_response.rb +16 -0
  94. data/lib/jsonapi_compliable/version.rb +1 -1
  95. metadata +60 -5
  96. data/gemfiles/rails_4.gemfile.lock +0 -208
  97. data/gemfiles/rails_5.gemfile.lock +0 -212
  98. data/lib/jsonapi_compliable/write.rb +0 -93
@@ -1,25 +1,65 @@
1
1
  module JsonapiCompliable
2
2
  module Adapters
3
+ # The Null adapter is a 'pass-through' adapter. It won't modify the scope.
4
+ # Useful when your customization does not support all possible
5
+ # configuration (e.g. the service you hit does not support sorting)
3
6
  class Null < Abstract
7
+ # (see Adapters::Abstract#filter)
4
8
  def filter(scope, attribute, value)
5
9
  scope
6
10
  end
7
11
 
12
+ # (see Adapters::Abstract#order)
8
13
  def order(scope, attribute, direction)
9
14
  scope
10
15
  end
11
16
 
12
- def paginate(scope, number, size)
17
+ # (see Adapters::Abstract#paginate)
18
+ def paginate(scope, current_page, per_page)
13
19
  scope
14
20
  end
15
21
 
16
- def sideload(scope, includes)
22
+ # (see Adapters::Abstract#count)
23
+ def count(scope, attr)
17
24
  scope
18
25
  end
19
26
 
20
- def transaction
27
+ # (see Adapters::Abstract#average)
28
+ def average(scope, attr)
29
+ scope
30
+ end
31
+
32
+ # (see Adapters::Abstract#sum)
33
+ def sum(scope, attr)
34
+ scope
35
+ end
36
+
37
+ # (see Adapters::Abstract#sum)
38
+ def maximum(scope, attr)
39
+ scope
40
+ end
41
+
42
+ # (see Adapters::Abstract#minimum)
43
+ def minimum(scope, attr)
44
+ scope
45
+ end
46
+
47
+ # Since this is a null adapter, just yield
48
+ # @see Adapters::ActiveRecord#transaction
49
+ # @return Result of yield
50
+ # @param [Class] model_class The class we're operating on
51
+ def transaction(model_class)
21
52
  yield
22
53
  end
54
+
55
+ # (see Adapters::Abstract#resolve)
56
+ def resolve(scope)
57
+ scope
58
+ end
59
+
60
+ # (see Adapters::Abstract#associate)
61
+ def associate(parent, child, association_name, association_type)
62
+ end
23
63
  end
24
64
  end
25
65
  end
@@ -1,9 +1,10 @@
1
1
  module JsonapiCompliable
2
+ # Provides main interface to jsonapi_compliable
3
+ #
4
+ # This gets mixed in to a "context" class, such as a Rails controller.
2
5
  module Base
3
6
  extend ActiveSupport::Concern
4
7
 
5
- MAX_PAGE_SIZE = 1_000
6
-
7
8
  included do
8
9
  class << self
9
10
  attr_accessor :_jsonapi_compliable
@@ -15,22 +16,77 @@ module JsonapiCompliable
15
16
  end
16
17
  end
17
18
 
18
- def resource
19
- @resource ||= self.class._jsonapi_compliable.new
19
+ # @!classmethods
20
+ module ClassMethods
21
+ # Define your JSONAPI configuration
22
+ #
23
+ # @example Inline Resource
24
+ # # 'Quick and Dirty' solution that does not require a separate
25
+ # # Resource object
26
+ # class PostsController < ApplicationController
27
+ # jsonapi do
28
+ # type :posts
29
+ # use_adapter JsonapiCompliable::Adapters::ActiveRecord
30
+ #
31
+ # allow_filter :title
32
+ # end
33
+ # end
34
+ #
35
+ # @example Resource Class (preferred)
36
+ # # Make code reusable by encapsulating it in a Resource class
37
+ # class PostsController < ApplicationController
38
+ # jsonapi resource: PostResource
39
+ # end
40
+ #
41
+ # @see Resource
42
+ # @param resource [Resource] the Resource class associated to this endpoint
43
+ # @return [void]
44
+ def jsonapi(foo = 'bar', resource: nil, &blk)
45
+ if resource
46
+ self._jsonapi_compliable = resource
47
+ else
48
+ if !self._jsonapi_compliable
49
+ self._jsonapi_compliable = Class.new(JsonapiCompliable::Resource)
50
+ end
51
+ end
52
+
53
+ self._jsonapi_compliable.class_eval(&blk) if blk
54
+ end
20
55
  end
21
56
 
22
- def resource!
23
- @resource = self.class._jsonapi_compliable.new
57
+ # Returns an instance of the associated Resource
58
+ #
59
+ # In other words, if you configured your controller as:
60
+ #
61
+ # jsonapi resource: MyResource
62
+ #
63
+ # This returns MyResource.new
64
+ #
65
+ # @return [Resource] the configured Resource for this controller
66
+ def resource
67
+ @resource ||= self.class._jsonapi_compliable.new
24
68
  end
25
69
 
70
+ # Instantiates the relevant Query object
71
+ #
72
+ # @see Query
73
+ # @return [Query] the Query object for this resource/params
26
74
  def query
27
75
  @query ||= Query.new(resource, params)
28
76
  end
29
77
 
78
+ # @see Query#to_hash
79
+ # @return [Hash] the normalized query hash for only the *current* resource
30
80
  def query_hash
31
81
  @query_hash ||= query.to_hash[resource.type]
32
82
  end
33
83
 
84
+ # Tracks the current context so we can refer to it within any
85
+ # random object. Helpful for easy-access to things like the current
86
+ # user.
87
+ #
88
+ # @api private
89
+ # @yieldreturn Code to run within the current context
34
90
  def wrap_context
35
91
  if self.class._jsonapi_compliable
36
92
  resource.with_context(self, action_name.to_sym) do
@@ -39,14 +95,57 @@ module JsonapiCompliable
39
95
  end
40
96
  end
41
97
 
98
+ # Use when direct, low-level access to the scope is required.
99
+ #
100
+ # @example Show Action
101
+ # # Scope#resolve returns an array, but we only want to render
102
+ # # one object, not an array
103
+ # scope = jsonapi_scope(Employee.where(id: params[:id]))
104
+ # render_jsonapi(scope.resolve.first, scope: false)
105
+ #
106
+ # @example Scope Chaining
107
+ # # Chain onto scope after running through typical DSL
108
+ # # Here, we'll add active: true to our hash if the user
109
+ # # is filtering on something
110
+ # scope = jsonapi_scope({})
111
+ # scope.object.merge!(active: true) if scope.object[:filter]
112
+ #
113
+ # @see Resource#build_scope
114
+ # @return [Scope] the configured scope
42
115
  def jsonapi_scope(scope, opts = {})
43
116
  resource.build_scope(scope, query, opts)
44
117
  end
45
118
 
119
+ # @see Deserializer#initialize
120
+ # @return [Deserializer]
46
121
  def deserialized_params
47
122
  @deserialized_params ||= JsonapiCompliable::Deserializer.new(params, request.env)
48
123
  end
49
124
 
125
+ # Create the resource model and process all nested relationships via the
126
+ # serialized parameters. Any error, including validation errors, will roll
127
+ # back the transaction.
128
+ #
129
+ # @example Basic Rails
130
+ # # Example Resource must have 'model'
131
+ # #
132
+ # # class PostResource < ApplicationResource
133
+ # # model Post
134
+ # # end
135
+ # def create
136
+ # post, success = jsonapi_create.to_a
137
+ #
138
+ # if success
139
+ # render_jsonapi(post, scope: false)
140
+ # else
141
+ # render_errors_for(post)
142
+ # end
143
+ # end
144
+ #
145
+ # @see Resource.model
146
+ # @see #resource
147
+ # @see #deserialized_params
148
+ # @return [Util::ValidationResponse]
50
149
  def jsonapi_create
51
150
  _persist do
52
151
  resource.persist_with_relationships \
@@ -56,6 +155,28 @@ module JsonapiCompliable
56
155
  end
57
156
  end
58
157
 
158
+ # Update the resource model and process all nested relationships via the
159
+ # serialized parameters. Any error, including validation errors, will roll
160
+ # back the transaction.
161
+ #
162
+ # @example Basic Rails
163
+ # # Example Resource must have 'model'
164
+ # #
165
+ # # class PostResource < ApplicationResource
166
+ # # model Post
167
+ # # end
168
+ # def update
169
+ # post, success = jsonapi_update.to_a
170
+ #
171
+ # if success
172
+ # render_jsonapi(post, scope: false)
173
+ # else
174
+ # render_errors_for(post)
175
+ # end
176
+ # end
177
+ #
178
+ # @see #jsonapi_create
179
+ # @return [Util::ValidationResponse]
59
180
  def jsonapi_update
60
181
  _persist do
61
182
  resource.persist_with_relationships \
@@ -65,21 +186,37 @@ module JsonapiCompliable
65
186
  end
66
187
  end
67
188
 
68
- def _persist
69
- validation_response = nil
70
- resource.transaction do
71
- object = yield
72
- validation_response = Util::ValidationResponse.new \
73
- object, deserialized_params
74
- raise Errors::ValidationError unless validation_response.to_a[1]
75
- end
76
- validation_response
77
- end
78
-
79
- def perform_render_jsonapi(opts)
80
- JSONAPI::Serializable::Renderer.render(opts.delete(:jsonapi), opts)
81
- end
82
-
189
+ # Similar to +render :json+ or +render :jsonapi+
190
+ #
191
+ # By default, this will "build" the scope via +#jsonapi_scope+. To avoid
192
+ # this, pass +scope: false+
193
+ #
194
+ # This builds relevant options and sends them to
195
+ # +JSONAPI::Serializable::Renderer.render+from
196
+ # {http://jsonapi-rb.org jsonapi-rb}
197
+ #
198
+ # @example Build Scope by Default
199
+ # # Employee.all returns an ActiveRecord::Relation. No SQL is fired at this point.
200
+ # # We further 'chain' onto this scope, applying pagination, sorting,
201
+ # # filters, etc that the user has requested.
202
+ # def index
203
+ # employees = Employee.all
204
+ # render_jsonapi(employees)
205
+ # end
206
+ #
207
+ # @example Avoid Building Scope by Default
208
+ # # Maybe we already manually scoped, and don't want to fire the logic twice
209
+ # # This code is equivalent to the above example
210
+ # def index
211
+ # scope = jsonapi_scope(Employee.all)
212
+ # # ... do other things with the scope ...
213
+ # render_jsonapi(scope.resolve, scope: false)
214
+ # end
215
+ #
216
+ # @param scope [Scope, Object] the scope to build or render.
217
+ # @param [Hash] opts the render options passed to {http://jsonapi-rb.org jsonapi-rb}
218
+ # @option opts [Boolean] :scope Default: true. Should we call #jsonapi_scope on this object?
219
+ # @see #jsonapi_scope
83
220
  def render_jsonapi(scope, opts = {})
84
221
  scope = jsonapi_scope(scope) unless opts[:scope] == false || scope.is_a?(JsonapiCompliable::Scope)
85
222
  opts = default_jsonapi_render_options.merge(opts)
@@ -89,29 +226,41 @@ module JsonapiCompliable
89
226
  perform_render_jsonapi(opts)
90
227
  end
91
228
 
92
- # render_jsonapi(foo) equivalent to
93
- # render jsonapi: foo, default_jsonapi_render_options
229
+ # Define a hash that will be automatically merged into your
230
+ # render_jsonapi call
231
+ #
232
+ # @example
233
+ # # this
234
+ # render_jsonapi(foo)
235
+ # # is equivalent to this
236
+ # render jsonapi: foo, default_jsonapi_render_options
237
+ #
238
+ # @see #render_jsonapi
239
+ # @return [Hash] the options hash you define
94
240
  def default_jsonapi_render_options
95
241
  {}.tap do |options|
96
242
  end
97
243
  end
98
244
 
245
+ private
246
+
99
247
  def force_includes?
100
248
  not params[:data].nil?
101
249
  end
102
250
 
103
- module ClassMethods
104
- def jsonapi(resource: nil, &blk)
105
- if resource
106
- self._jsonapi_compliable = resource
107
- else
108
- if !self._jsonapi_compliable
109
- self._jsonapi_compliable = Class.new(JsonapiCompliable::Resource)
110
- end
111
- end
251
+ def perform_render_jsonapi(opts)
252
+ JSONAPI::Serializable::Renderer.render(opts.delete(:jsonapi), opts)
253
+ end
112
254
 
113
- self._jsonapi_compliable.class_eval(&blk) if blk
255
+ def _persist
256
+ validation_response = nil
257
+ resource.transaction do
258
+ object = yield
259
+ validation_response = Util::ValidationResponse.new \
260
+ object, deserialized_params
261
+ raise Errors::ValidationError unless validation_response.to_a[1]
114
262
  end
263
+ validation_response
115
264
  end
116
265
  end
117
266
  end
@@ -1,35 +1,81 @@
1
+ # Responsible for parsing incoming write payloads
2
+ #
3
+ # Given a PUT payload like:
4
+ #
5
+ # {
6
+ # data: {
7
+ # id: '1',
8
+ # type: 'posts',
9
+ # attributes: { title: 'My Title' },
10
+ # relationships: {
11
+ # author: {
12
+ # data: {
13
+ # id: '1',
14
+ # type: 'authors'
15
+ # }
16
+ # }
17
+ # }
18
+ # },
19
+ # included: [
20
+ # {
21
+ # id: '1'
22
+ # type: 'authors',
23
+ # attributes: { name: 'Joe Author' }
24
+ # }
25
+ # ]
26
+ # }
27
+ #
28
+ # You can now easily deal with this payload:
29
+ #
30
+ # deserializer.attributes
31
+ # # => { id: '1', title: 'My Title' }
32
+ # deserializer.meta
33
+ # # => { type: 'posts', method: :update }
34
+ # deserializer.relationships
35
+ # # {
36
+ # # author: {
37
+ # # meta: { ... },
38
+ # # attributes: { ... },
39
+ # # relationships: { ... }
40
+ # # }
41
+ # # }
42
+ #
43
+ # When creating objects, we accept a +temp-id+ so that the client can track
44
+ # the object it just created. Expect this in +meta+:
45
+ #
46
+ # { type: 'authors', method: :create, temp_id: 'abc123' }
1
47
  class JsonapiCompliable::Deserializer
48
+ # @param payload [Hash] The incoming payload with symbolized keys
49
+ # @param env [Hash] the Rack env (e.g. +request.env+).
2
50
  def initialize(payload, env)
3
51
  @payload = payload
4
52
  @env = env
5
53
  end
6
54
 
55
+ # @return [Hash] the raw :data value of the payload
7
56
  def data
8
57
  @payload[:data]
9
58
  end
10
59
 
60
+ # @return [String] the raw :id value of the payload
11
61
  def id
12
62
  data[:id]
13
63
  end
14
64
 
65
+ # @return [Hash] the raw :attributes hash + +id+
15
66
  def attributes
16
67
  @attributes ||= raw_attributes.tap do |hash|
17
68
  hash.merge!(id: id) if id
18
69
  end
19
70
  end
20
71
 
21
- def attributes=(attrs)
22
- @attributes = attrs
23
- end
24
-
25
- def method
26
- case @env['REQUEST_METHOD']
27
- when 'POST' then :create
28
- when 'PUT', 'PATCH' then :update
29
- when 'DELETE' then :destroy
30
- end
31
- end
32
-
72
+ # 'meta' information about this resource. Includes:
73
+ #
74
+ # +type+: the jsonapi type
75
+ # +method+: create/update/destroy/disassociate. Based on the request env or the +method+ within the +relationships+ hash
76
+ # +temp_id+: the +temp-id+, if specified
77
+ #
78
+ # @return [Hash]
33
79
  def meta
34
80
  {
35
81
  type: data[:type],
@@ -38,23 +84,25 @@ class JsonapiCompliable::Deserializer
38
84
  }
39
85
  end
40
86
 
87
+ # @return [Hash] the relationships hash
41
88
  def relationships
42
89
  @relationships ||= process_relationships(raw_relationships)
43
90
  end
44
91
 
45
- def included
46
- @payload[:included] || []
47
- end
48
-
92
+ # Parses the +relationships+ recursively and builds an all-hash
93
+ # include directive like
94
+ #
95
+ # { posts: { comments: {} } }
96
+ #
97
+ # Relationships that have been marked for destruction will NOT
98
+ # be part of the include directive.
99
+ #
100
+ # @return [Hash] the include directive
49
101
  def include_directive(memo = {}, relationship_node = nil)
50
102
  relationship_node ||= relationships
51
103
 
52
104
  relationship_node.each_pair do |name, relationship_payload|
53
- arrayified = [relationship_payload].flatten
54
- next if arrayified.all? { |rp| removed?(rp) }
55
-
56
- memo[name] ||= {}
57
- deep_merge!(memo[name], sub_directives(memo[name], arrayified))
105
+ merge_include_directive(memo, name, relationship_payload)
58
106
  end
59
107
 
60
108
  memo
@@ -62,6 +110,27 @@ class JsonapiCompliable::Deserializer
62
110
 
63
111
  private
64
112
 
113
+ def merge_include_directive(memo, name, relationship_payload)
114
+ arrayified = [relationship_payload].flatten
115
+ return if arrayified.all? { |rp| removed?(rp) }
116
+
117
+ memo[name] ||= {}
118
+ deep_merge!(memo[name], sub_directives(memo[name], arrayified))
119
+ memo
120
+ end
121
+
122
+ def included
123
+ @payload[:included] || []
124
+ end
125
+
126
+ def method
127
+ case @env['REQUEST_METHOD']
128
+ when 'POST' then :create
129
+ when 'PUT', 'PATCH' then :update
130
+ when 'DELETE' then :destroy
131
+ end
132
+ end
133
+
65
134
  def removed?(relationship_payload)
66
135
  method = relationship_payload[:meta][:method]
67
136
  [:disassociate, :destroy].include?(method)