jsonapi-resources 0.3.3 → 0.4.0

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +274 -102
  3. data/jsonapi-resources.gemspec +1 -0
  4. data/lib/jsonapi-resources.rb +15 -0
  5. data/lib/jsonapi/active_record_operations_processor.rb +21 -10
  6. data/lib/jsonapi/acts_as_resource_controller.rb +175 -0
  7. data/lib/jsonapi/configuration.rb +11 -0
  8. data/lib/jsonapi/error_codes.rb +7 -4
  9. data/lib/jsonapi/exceptions.rb +23 -15
  10. data/lib/jsonapi/formatter.rb +5 -5
  11. data/lib/jsonapi/include_directives.rb +67 -0
  12. data/lib/jsonapi/operation.rb +185 -65
  13. data/lib/jsonapi/operation_result.rb +38 -5
  14. data/lib/jsonapi/operation_results.rb +33 -0
  15. data/lib/jsonapi/operations_processor.rb +49 -9
  16. data/lib/jsonapi/paginator.rb +31 -17
  17. data/lib/jsonapi/request.rb +347 -163
  18. data/lib/jsonapi/resource.rb +159 -56
  19. data/lib/jsonapi/resource_controller.rb +1 -234
  20. data/lib/jsonapi/resource_serializer.rb +55 -69
  21. data/lib/jsonapi/resources/version.rb +1 -1
  22. data/lib/jsonapi/response_document.rb +87 -0
  23. data/lib/jsonapi/routing_ext.rb +17 -11
  24. data/test/controllers/controller_test.rb +602 -326
  25. data/test/fixtures/active_record.rb +96 -6
  26. data/test/fixtures/line_items.yml +7 -1
  27. data/test/fixtures/numeros_telefone.yml +3 -0
  28. data/test/fixtures/purchase_orders.yml +6 -0
  29. data/test/integration/requests/request_test.rb +129 -60
  30. data/test/integration/routes/routes_test.rb +17 -17
  31. data/test/test_helper.rb +23 -5
  32. data/test/unit/jsonapi_request/jsonapi_request_test.rb +48 -0
  33. data/test/unit/operation/operations_processor_test.rb +242 -54
  34. data/test/unit/resource/resource_test.rb +108 -2
  35. data/test/unit/serializer/include_directives_test.rb +108 -0
  36. data/test/unit/serializer/response_document_test.rb +61 -0
  37. data/test/unit/serializer/serializer_test.rb +679 -520
  38. metadata +26 -2
@@ -1,162 +1,282 @@
1
1
  module JSONAPI
2
2
  class Operation
3
+ attr_reader :resource_klass, :options, :transactional
3
4
 
4
- attr_reader :resource_klass
5
-
6
- def initialize(resource_klass)
5
+ def initialize(resource_klass, options = {})
7
6
  @resource_klass = resource_klass
7
+ @options = options
8
+ @transactional = true
9
+ end
10
+
11
+ def apply(context)
12
+ end
13
+ end
14
+
15
+ class FindOperation < Operation
16
+ attr_reader :filters, :include_directives, :sort_criteria, :paginator
17
+
18
+ def initialize(resource_klass, options = {})
19
+ @filters = options[:filters]
20
+ @include_directives = options[:include_directives]
21
+ @sort_criteria = options.fetch(:sort_criteria, [])
22
+ @paginator = options[:paginator]
23
+ super(resource_klass, false)
24
+ end
25
+
26
+ def apply(context)
27
+ resource_records = @resource_klass.find(@resource_klass.verify_filters(@filters, context),
28
+ context: context,
29
+ include_directives: @include_directives,
30
+ sort_criteria: @sort_criteria,
31
+ paginator: @paginator)
32
+
33
+ return JSONAPI::ResourcesOperationResult.new(:ok, resource_records)
34
+
35
+ rescue JSONAPI::Exceptions::Error => e
36
+ return JSONAPI::ErrorsOperationResult.new(e.errors[0].code, e.errors)
37
+ end
38
+ end
39
+
40
+ class ShowOperation < Operation
41
+ attr_reader :id, :include_directives
42
+
43
+ def initialize(resource_klass, options = {})
44
+ @id = options.fetch(:id)
45
+ @include_directives = options[:include_directives]
46
+ @transactional = false
47
+ super(resource_klass, options)
48
+ end
49
+
50
+ def apply(context)
51
+ key = @resource_klass.verify_key(@id, context)
52
+
53
+ resource_record = resource_klass.find_by_key(key,
54
+ context: context,
55
+ include_directives: @include_directives)
56
+
57
+ return JSONAPI::ResourceOperationResult.new(:ok, resource_record)
58
+
59
+ rescue JSONAPI::Exceptions::Error => e
60
+ return JSONAPI::ErrorsOperationResult.new(e.errors[0].code, e.errors)
61
+ end
62
+ end
63
+
64
+ class ShowAssociationOperation < Operation
65
+ attr_reader :parent_key, :association_type
66
+
67
+ def initialize(resource_klass, options = {})
68
+ @parent_key = options.fetch(:parent_key)
69
+ @association_type = options.fetch(:association_type)
70
+ @transactional = false
71
+ super(resource_klass, options)
8
72
  end
9
73
 
10
74
  def apply(context)
75
+ parent_resource = resource_klass.find_by_key(@parent_key, context: context)
76
+
77
+ return JSONAPI::LinksObjectOperationResult.new(:ok,
78
+ parent_resource,
79
+ resource_klass._association(@association_type))
80
+
81
+ rescue JSONAPI::Exceptions::Error => e
82
+ return JSONAPI::ErrorsOperationResult.new(e.errors[0].code, e.errors)
83
+ end
84
+ end
85
+
86
+ class ShowRelatedResourceOperation < Operation
87
+ attr_reader :source_klass, :source_id, :association_type
88
+
89
+ def initialize(resource_klass, options = {})
90
+ @source_klass = options.fetch(:source_klass)
91
+ @source_id = options.fetch(:source_id)
92
+ @association_type = options.fetch(:association_type)
93
+ @transactional = false
94
+ super(resource_klass, options)
95
+ end
96
+
97
+ def apply(context)
98
+ source_resource = @source_klass.find_by_key(@source_id, context: context)
99
+
100
+ related_resource = source_resource.send(@association_type)
101
+
102
+ return JSONAPI::ResourceOperationResult.new(:ok, related_resource)
103
+
104
+ rescue JSONAPI::Exceptions::Error => e
105
+ return JSONAPI::ErrorsOperationResult.new(e.errors[0].code, e.errors)
106
+ end
107
+ end
108
+
109
+ class ShowRelatedResourcesOperation < Operation
110
+ attr_reader :source_klass, :source_id, :association_type, :filters, :sort_criteria, :paginator
111
+
112
+ def initialize(resource_klass, options = {})
113
+ @source_klass = options.fetch(:source_klass)
114
+ @source_id = options.fetch(:source_id)
115
+ @association_type = options.fetch(:association_type)
116
+ @filters = options[:filters]
117
+ @sort_criteria = options[:sort_criteria]
118
+ @paginator = options[:paginator]
119
+ @transactional = false
120
+ super(resource_klass, options)
121
+ end
122
+
123
+ def apply(context)
124
+ source_resource = @source_klass.find_by_key(@source_id, context: context)
125
+
126
+ related_resource = source_resource.send(@association_type,
127
+ {
128
+ filters: @filters,
129
+ sort_criteria: @sort_criteria,
130
+ paginator: @paginator
131
+ })
132
+
133
+ return JSONAPI::ResourceOperationResult.new(:ok, related_resource)
134
+
135
+ rescue JSONAPI::Exceptions::Error => e
136
+ return JSONAPI::ErrorsOperationResult.new(e.errors[0].code, e.errors)
11
137
  end
12
138
  end
13
139
 
14
140
  class CreateResourceOperation < Operation
15
- attr_reader :values
141
+ attr_reader :data
16
142
 
17
- def initialize(resource_klass, values = {})
18
- @values = values
19
- super(resource_klass)
143
+ def initialize(resource_klass, options = {})
144
+ @data = options.fetch(:data)
145
+ super(resource_klass, options)
20
146
  end
21
147
 
22
148
  def apply(context)
23
149
  resource = @resource_klass.create(context)
24
- resource.replace_fields(@values)
150
+ result = resource.replace_fields(@data)
25
151
 
26
- return JSONAPI::OperationResult.new(:created, resource)
152
+ return JSONAPI::ResourceOperationResult.new((result == :completed ? :created : :accepted), resource)
27
153
 
28
154
  rescue JSONAPI::Exceptions::Error => e
29
- return JSONAPI::OperationResult.new(e.errors[0].code, nil, e.errors)
155
+ return JSONAPI::ErrorsOperationResult.new(e.errors[0].code, e.errors)
30
156
  end
31
157
  end
32
158
 
33
159
  class RemoveResourceOperation < Operation
34
160
  attr_reader :resource_id
35
- def initialize(resource_klass, resource_id)
36
- @resource_id = resource_id
37
- super(resource_klass)
161
+ def initialize(resource_klass, options = {})
162
+ @resource_id = options.fetch(:resource_id)
163
+ super(resource_klass, options)
38
164
  end
39
165
 
40
166
  def apply(context)
41
167
  resource = @resource_klass.find_by_key(@resource_id, context: context)
42
- resource.remove
168
+ result = resource.remove
43
169
 
44
- return JSONAPI::OperationResult.new(:no_content)
170
+ return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
45
171
 
46
- rescue ActiveRecord::DeleteRestrictionError => e
47
- record_locked_error = JSONAPI::Exceptions::RecordLocked.new(e.message)
48
- return JSONAPI::OperationResult.new(record_locked_error.errors[0].code, nil, record_locked_error.errors)
49
172
  rescue JSONAPI::Exceptions::Error => e
50
- return JSONAPI::OperationResult.new(e.errors[0].code, nil, e.errors)
173
+ return JSONAPI::ErrorsOperationResult.new(e.errors[0].code, e.errors)
51
174
  end
52
175
  end
53
176
 
54
177
  class ReplaceFieldsOperation < Operation
55
- attr_reader :values, :resource_id
178
+ attr_reader :data, :resource_id
56
179
 
57
- def initialize(resource_klass, resource_id, values)
58
- @resource_id = resource_id
59
- @values = values
60
- super(resource_klass)
180
+ def initialize(resource_klass, options = {})
181
+ @resource_id = options.fetch(:resource_id)
182
+ @data = options.fetch(:data)
183
+ super(resource_klass, options)
61
184
  end
62
185
 
63
186
  def apply(context)
64
187
  resource = @resource_klass.find_by_key(@resource_id, context: context)
65
- resource.replace_fields(values)
188
+ result = resource.replace_fields(data)
66
189
 
67
- return JSONAPI::OperationResult.new(:ok, resource)
190
+ return JSONAPI::ResourceOperationResult.new(result == :completed ? :ok : :accepted, resource)
68
191
  end
69
192
  end
70
193
 
71
194
  class ReplaceHasOneAssociationOperation < Operation
72
195
  attr_reader :resource_id, :association_type, :key_value
73
196
 
74
- def initialize(resource_klass, resource_id, association_type, key_value)
75
- @resource_id = resource_id
76
- @key_value = key_value
77
- @association_type = association_type.to_sym
78
- super(resource_klass)
197
+ def initialize(resource_klass, options = {})
198
+ @resource_id = options.fetch(:resource_id)
199
+ @key_value = options.fetch(:key_value)
200
+ @association_type = options.fetch(:association_type).to_sym
201
+ super(resource_klass, options)
79
202
  end
80
203
 
81
204
  def apply(context)
82
205
  resource = @resource_klass.find_by_key(@resource_id, context: context)
83
- resource.replace_has_one_link(@association_type, @key_value)
206
+ result = resource.replace_has_one_link(@association_type, @key_value)
84
207
 
85
- return JSONAPI::OperationResult.new(:no_content)
208
+ return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
86
209
  end
87
210
  end
88
211
 
89
212
  class CreateHasManyAssociationOperation < Operation
90
- attr_reader :resource_id, :association_type, :key_values
213
+ attr_reader :resource_id, :association_type, :data
91
214
 
92
- def initialize(resource_klass, resource_id, association_type, key_values)
93
- @resource_id = resource_id
94
- @key_values = key_values
95
- @association_type = association_type.to_sym
96
- super(resource_klass)
215
+ def initialize(resource_klass, options)
216
+ @resource_id = options.fetch(:resource_id)
217
+ @data = options.fetch(:data)
218
+ @association_type = options.fetch(:association_type).to_sym
219
+ super(resource_klass, options)
97
220
  end
98
221
 
99
222
  def apply(context)
100
223
  resource = @resource_klass.find_by_key(@resource_id, context: context)
101
- resource.create_has_many_links(@association_type, @key_values)
224
+ result = resource.create_has_many_links(@association_type, @data)
102
225
 
103
- return JSONAPI::OperationResult.new(:no_content)
226
+ return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
104
227
  end
105
228
  end
106
229
 
107
230
  class ReplaceHasManyAssociationOperation < Operation
108
- attr_reader :resource_id, :association_type, :key_values
231
+ attr_reader :resource_id, :association_type, :data
109
232
 
110
- def initialize(resource_klass, resource_id, association_type, key_values)
111
- @resource_id = resource_id
112
- @key_values = key_values
113
- @association_type = association_type.to_sym
114
- super(resource_klass)
233
+ def initialize(resource_klass, options)
234
+ @resource_id = options.fetch(:resource_id)
235
+ @data = options.fetch(:data)
236
+ @association_type = options.fetch(:association_type).to_sym
237
+ super(resource_klass, options)
115
238
  end
116
239
 
117
240
  def apply(context)
118
241
  resource = @resource_klass.find_by_key(@resource_id, context: context)
119
- resource.replace_has_many_links(@association_type, @key_values)
242
+ result = resource.replace_has_many_links(@association_type, @data)
120
243
 
121
- return JSONAPI::OperationResult.new(:no_content)
244
+ return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
122
245
  end
123
246
  end
124
247
 
125
248
  class RemoveHasManyAssociationOperation < Operation
126
249
  attr_reader :resource_id, :association_type, :associated_key
127
250
 
128
- def initialize(resource_klass, resource_id, association_type, associated_key)
129
- @resource_id = resource_id
130
- @associated_key = associated_key
131
- @association_type = association_type.to_sym
132
- super(resource_klass)
251
+ def initialize(resource_klass, options)
252
+ @resource_id = options.fetch(:resource_id)
253
+ @associated_key = options.fetch(:associated_key)
254
+ @association_type = options.fetch(:association_type).to_sym
255
+ super(resource_klass, options)
133
256
  end
134
257
 
135
258
  def apply(context)
136
259
  resource = @resource_klass.find_by_key(@resource_id, context: context)
137
- resource.remove_has_many_link(@association_type, @associated_key)
138
-
139
- return JSONAPI::OperationResult.new(:no_content)
260
+ result = resource.remove_has_many_link(@association_type, @associated_key)
140
261
 
141
- rescue ActiveRecord::RecordNotFound => e
142
- raise JSONAPI::Exceptions::RecordNotFound.new(@associated_key)
262
+ return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
143
263
  end
144
264
  end
145
265
 
146
266
  class RemoveHasOneAssociationOperation < Operation
147
267
  attr_reader :resource_id, :association_type
148
268
 
149
- def initialize(resource_klass, resource_id, association_type)
150
- @resource_id = resource_id
151
- @association_type = association_type.to_sym
152
- super(resource_klass)
269
+ def initialize(resource_klass, options)
270
+ @resource_id = options.fetch(:resource_id)
271
+ @association_type = options.fetch(:association_type).to_sym
272
+ super(resource_klass, options)
153
273
  end
154
274
 
155
275
  def apply(context)
156
276
  resource = @resource_klass.find_by_key(@resource_id, context: context)
157
- resource.remove_has_one_link(@association_type)
277
+ result = resource.remove_has_one_link(@association_type)
158
278
 
159
- return JSONAPI::OperationResult.new(:no_content)
279
+ return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
160
280
  end
161
281
  end
162
282
  end
@@ -1,15 +1,48 @@
1
1
  module JSONAPI
2
2
  class OperationResult
3
- attr_accessor :code, :errors, :resource
3
+ attr_accessor :code
4
+ attr_accessor :meta
4
5
 
5
- def initialize(code, resource = nil, errors = [])
6
+ def initialize(code)
6
7
  @code = code
7
- @resource = resource
8
+ @meta = {}
9
+ end
10
+ end
11
+
12
+ class ErrorsOperationResult < OperationResult
13
+ attr_accessor :errors
14
+
15
+ def initialize(code, errors)
8
16
  @errors = errors
17
+ super(code)
18
+ end
19
+ end
20
+
21
+ class ResourceOperationResult < OperationResult
22
+ attr_accessor :resource
23
+
24
+ def initialize(code, resource)
25
+ @resource = resource
26
+ super(code)
27
+ end
28
+ end
29
+
30
+ class ResourcesOperationResult < OperationResult
31
+ attr_accessor :resources
32
+
33
+ def initialize(code, resources)
34
+ @resources = resources
35
+ super(code)
9
36
  end
37
+ end
38
+
39
+ class LinksObjectOperationResult < OperationResult
40
+ attr_accessor :parent_resource, :association
10
41
 
11
- def has_errors?
12
- errors.count > 0
42
+ def initialize(code, parent_resource, association)
43
+ @parent_resource = parent_resource
44
+ @association = association
45
+ super(code)
13
46
  end
14
47
  end
15
48
  end
@@ -0,0 +1,33 @@
1
+ module JSONAPI
2
+ class OperationResults
3
+ attr_accessor :results
4
+ attr_accessor :meta
5
+
6
+ def initialize
7
+ @results = []
8
+ @has_errors = false
9
+ @meta = {}
10
+ end
11
+
12
+ def add_result(result)
13
+ @has_errors = true if result.is_a?(JSONAPI::ErrorsOperationResult)
14
+ @results.push(result)
15
+ end
16
+
17
+ def has_errors?
18
+ @has_errors
19
+ end
20
+
21
+ def all_errors
22
+ errors = []
23
+ if @has_errors
24
+ @results.each do |result|
25
+ if result.is_a?(JSONAPI::ErrorsOperationResult)
26
+ errors.concat(result.errors)
27
+ end
28
+ end
29
+ end
30
+ errors
31
+ end
32
+ end
33
+ end
@@ -1,30 +1,63 @@
1
- require 'jsonapi/operation_result'
2
- require 'jsonapi/callbacks'
3
-
4
1
  module JSONAPI
5
2
  class OperationsProcessor
6
3
  include Callbacks
7
- define_jsonapi_resources_callbacks :operation, :operations
4
+ define_jsonapi_resources_callbacks :operation,
5
+ :operations,
6
+ :find_operation,
7
+ :show_operation,
8
+ :show_association_operation,
9
+ :show_related_resource_operation,
10
+ :show_related_resources_operation,
11
+ :create_resource_operation,
12
+ :remove_resource_operation,
13
+ :replace_fields_operation,
14
+ :replace_has_one_association_operation,
15
+ :create_has_many_association_operation,
16
+ :replace_has_many_association_operation,
17
+ :remove_has_many_association_operation,
18
+ :remove_has_one_association_operation
19
+
20
+ class << self
21
+ def operations_processor_for(operations_processor)
22
+ operations_processor_class_name = "#{operations_processor.to_s.camelize}OperationsProcessor"
23
+ operations_processor_class_name.safe_constantize
24
+ end
25
+ end
8
26
 
9
27
  def process(request)
10
- @results = []
28
+ @results = JSONAPI::OperationResults.new
11
29
  @request = request
12
30
  @context = request.context
13
31
  @operations = request.operations
14
32
 
33
+ # Use transactions if more than one operation and if one of the operations can be transactional
34
+ # Even if transactional transactions won't be used unless the derived OperationsProcessor supports them.
35
+ @transactional = false
36
+ if @operations.length > 1
37
+ @operations.each do |operation|
38
+ @transactional = @transactional | operation.transactional
39
+ end
40
+ end
41
+
15
42
  run_callbacks :operations do
16
43
  transaction do
44
+ @operations_meta = {}
17
45
  @operations.each do |operation|
18
46
  @operation = operation
19
- @result = nil
47
+ @operation_meta = {}
20
48
  run_callbacks :operation do
21
- @result = @operation.apply(@context)
22
- @results.push(@result)
23
- if @result.has_errors?
49
+ @result = nil
50
+ run_callbacks @operation.class.name.demodulize.underscore.to_sym do
51
+ @result = process_operation(@operation)
52
+ end
53
+ @result.meta.merge!(@operation_meta)
54
+ @results.add_result(@result)
55
+ if @results.has_errors?
24
56
  rollback
25
57
  end
26
58
  end
27
59
  end
60
+ @results.meta = @operations_meta
28
61
  end
29
62
  end
30
63
  @results
@@ -41,5 +74,12 @@ module JSONAPI
41
74
 
42
75
  def rollback
43
76
  end
77
+
78
+ def process_operation(operation)
79
+ operation.apply(@context)
80
+ end
44
81
  end
82
+ end
83
+
84
+ class BasicOperationsProcessor < JSONAPI::OperationsProcessor
45
85
  end