jsonapi-resources 0.3.3 → 0.4.0

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