jsonapi_compliable 0.6.4 → 0.6.5

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