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
@@ -25,5 +25,6 @@ Gem::Specification.new do |spec|
25
25
  spec.add_development_dependency 'minitest-spec-rails'
26
26
  spec.add_development_dependency 'minitest-reporters'
27
27
  spec.add_development_dependency 'simplecov'
28
+ spec.add_development_dependency 'pry'
28
29
  spec.add_dependency 'rails', '>= 4.0'
29
30
  end
@@ -1,4 +1,6 @@
1
1
  require 'jsonapi/resource'
2
+ require 'jsonapi/response_document'
3
+ require 'jsonapi/acts_as_resource_controller'
2
4
  require 'jsonapi/resource_controller'
3
5
  require 'jsonapi/resources/version'
4
6
  require 'jsonapi/configuration'
@@ -6,3 +8,16 @@ require 'jsonapi/paginator'
6
8
  require 'jsonapi/formatter'
7
9
  require 'jsonapi/routing_ext'
8
10
  require 'jsonapi/mime_types'
11
+ require 'jsonapi/resource_serializer'
12
+ require 'jsonapi/exceptions'
13
+ require 'jsonapi/error'
14
+ require 'jsonapi/error_codes'
15
+ require 'jsonapi/request'
16
+ require 'jsonapi/operations_processor'
17
+ require 'jsonapi/active_record_operations_processor'
18
+ require 'jsonapi/association'
19
+ require 'jsonapi/include_directives'
20
+ require 'jsonapi/operation_result'
21
+ require 'jsonapi/operation_results'
22
+ require 'jsonapi/callbacks'
23
+
@@ -1,17 +1,28 @@
1
- require 'jsonapi/operations_processor'
1
+ class ActiveRecordOperationsProcessor < JSONAPI::OperationsProcessor
2
2
 
3
- module JSONAPI
4
- class ActiveRecordOperationsProcessor < OperationsProcessor
5
-
6
- private
7
- def transaction
3
+ private
4
+ def transaction
5
+ if @transactional
8
6
  ActiveRecord::Base.transaction do
9
7
  yield
10
8
  end
9
+ else
10
+ yield
11
11
  end
12
+ end
12
13
 
13
- def rollback
14
- raise ActiveRecord::Rollback
15
- end
14
+ def rollback
15
+ raise ActiveRecord::Rollback if @transactional
16
+ end
17
+
18
+ def process_operation(operation)
19
+ operation.apply(@context)
20
+ rescue ActiveRecord::DeleteRestrictionError => e
21
+ record_locked_error = JSONAPI::Exceptions::RecordLocked.new(e.message)
22
+ return JSONAPI::ErrorsOperationResult.new(record_locked_error.errors[0].code, record_locked_error.errors)
23
+
24
+ rescue ActiveRecord::RecordNotFound
25
+ record_not_found = JSONAPI::Exceptions::RecordNotFound.new(operation.associated_key)
26
+ return JSONAPI::ErrorsOperationResult.new(record_not_found.errors[0].code, record_not_found.errors)
16
27
  end
17
- end
28
+ end
@@ -0,0 +1,175 @@
1
+ require 'csv'
2
+
3
+ module JSONAPI
4
+ module ActsAsResourceController
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ before_filter :ensure_correct_media_type, only: [:create, :update, :create_association, :update_association]
9
+ before_filter :setup_request
10
+ after_filter :setup_response
11
+ end
12
+
13
+ def index
14
+ process_request_operations
15
+ end
16
+
17
+ def show
18
+ process_request_operations
19
+ end
20
+
21
+ def show_association
22
+ process_request_operations
23
+ end
24
+
25
+ def create
26
+ process_request_operations
27
+ end
28
+
29
+ def create_association
30
+ process_request_operations
31
+ end
32
+
33
+ def update_association
34
+ process_request_operations
35
+ end
36
+
37
+ def update
38
+ process_request_operations
39
+ end
40
+
41
+ def destroy
42
+ process_request_operations
43
+ end
44
+
45
+ def destroy_association
46
+ process_request_operations
47
+ end
48
+
49
+ def get_related_resource
50
+ process_request_operations
51
+ end
52
+
53
+ def get_related_resources
54
+ process_request_operations
55
+ end
56
+
57
+ # set the operations processor in the configuration or override this to use another operations processor
58
+ def create_operations_processor
59
+ JSONAPI.configuration.operations_processor.new
60
+ end
61
+
62
+ private
63
+ def resource_klass
64
+ @resource_klass ||= resource_klass_name.safe_constantize
65
+ end
66
+
67
+ def resource_serializer_klass
68
+ @resource_serializer_klass ||= JSONAPI::ResourceSerializer
69
+ end
70
+
71
+ def base_url
72
+ @base_url ||= request.protocol + request.host_with_port
73
+ end
74
+
75
+ def resource_klass_name
76
+ @resource_klass_name ||= "#{self.class.name.underscore.sub(/_controller$/, '').singularize}_resource".camelize
77
+ end
78
+
79
+ def ensure_correct_media_type
80
+ unless request.content_type == JSONAPI::MEDIA_TYPE
81
+ raise JSONAPI::Exceptions::UnsupportedMediaTypeError.new(request.content_type)
82
+ end
83
+ rescue => e
84
+ handle_exceptions(e)
85
+ end
86
+
87
+ def setup_request
88
+ @request = JSONAPI::Request.new(params, {
89
+ context: context,
90
+ key_formatter: key_formatter
91
+ })
92
+ render_errors(@request.errors) unless @request.errors.empty?
93
+ rescue => e
94
+ handle_exceptions(e)
95
+ end
96
+
97
+ def setup_response
98
+ if response.body.size > 0
99
+ response.headers['Content-Type'] = JSONAPI::MEDIA_TYPE
100
+ end
101
+ end
102
+
103
+ # override to set context
104
+ def context
105
+ {}
106
+ end
107
+
108
+ # Control by setting in an initializer:
109
+ # JSONAPI.configuration.json_key_format = :camelized_key
110
+ # JSONAPI.configuration.route = :camelized_route
111
+ #
112
+ # Override if you want to set a per controller key format.
113
+ # Must return a class derived from KeyFormatter.
114
+ def key_formatter
115
+ JSONAPI.configuration.key_formatter
116
+ end
117
+
118
+ def route_formatter
119
+ JSONAPI.configuration.route_formatter
120
+ end
121
+
122
+ def base_response_meta
123
+ {}
124
+ end
125
+
126
+ def render_errors(errors)
127
+ operation_results = JSONAPI::OperationResults.new()
128
+ result = JSONAPI::ErrorsOperationResult.new(errors[0].status, errors)
129
+ operation_results.add_result(result)
130
+
131
+ render_results(operation_results)
132
+ end
133
+
134
+ def render_results(operation_results)
135
+ response_doc = create_response_document(operation_results)
136
+ render status: response_doc.status, json: response_doc.contents
137
+ end
138
+
139
+ def create_response_document(operation_results)
140
+ JSONAPI::ResponseDocument.new(
141
+ operation_results,
142
+ {
143
+ primary_resource_klass: resource_klass,
144
+ include_directives: @request ? @request.include_directives : nil,
145
+ fields: @request ? @request.fields : nil,
146
+ base_url: base_url,
147
+ key_formatter: key_formatter,
148
+ route_formatter: route_formatter,
149
+ base_meta: base_response_meta,
150
+ resource_serializer_klass: resource_serializer_klass
151
+ }
152
+ )
153
+ end
154
+
155
+ def process_request_operations
156
+ operation_results = create_operations_processor.process(@request)
157
+ render_results(operation_results)
158
+ rescue => e
159
+ handle_exceptions(e)
160
+ end
161
+
162
+ # override this to process other exceptions
163
+ # Note: Be sure to either call super(e) or handle JSONAPI::Exceptions::Error and raise unhandled exceptions
164
+ def handle_exceptions(e)
165
+ case e
166
+ when JSONAPI::Exceptions::Error
167
+ render_errors(e.errors)
168
+ else # raise all other exceptions
169
+ # :nocov:
170
+ raise e
171
+ # :nocov:
172
+ end
173
+ end
174
+ end
175
+ end
@@ -1,4 +1,6 @@
1
1
  require 'jsonapi/formatter'
2
+ require 'jsonapi/operations_processor'
3
+ require 'jsonapi/active_record_operations_processor'
2
4
 
3
5
  module JSONAPI
4
6
  class Configuration
@@ -6,6 +8,7 @@ module JSONAPI
6
8
  :key_formatter,
7
9
  :route_format,
8
10
  :route_formatter,
11
+ :operations_processor,
9
12
  :allowed_request_params,
10
13
  :default_paginator,
11
14
  :default_page_size,
@@ -19,6 +22,9 @@ module JSONAPI
19
22
  #:underscored_route, :camelized_route, :dasherized_route, or custom
20
23
  self.route_format = :dasherized_route
21
24
 
25
+ #:basic, :active_record, or custom
26
+ self.operations_processor = :active_record
27
+
22
28
  self.allowed_request_params = [:include, :fields, :format, :controller, :action, :sort, :page]
23
29
 
24
30
  # :none, :offset, :paged, or a custom paginator name
@@ -39,6 +45,11 @@ module JSONAPI
39
45
  @route_formatter = JSONAPI::Formatter.formatter_for(format)
40
46
  end
41
47
 
48
+ def operations_processor=(operations_processor)
49
+ @operations_processor_name = operations_processor
50
+ @operations_processor = JSONAPI::OperationsProcessor.operations_processor_for(@operations_processor_name)
51
+ end
52
+
42
53
  def allowed_request_params=(allowed_request_params)
43
54
  @allowed_request_params = allowed_request_params
44
55
  end
@@ -17,8 +17,9 @@ module JSONAPI
17
17
  TYPE_MISMATCH = 116
18
18
  INVALID_PAGE_OBJECT = 117
19
19
  INVALID_PAGE_VALUE = 118
20
- INVALID_SORT_FORMAT = 119
21
- INVALID_FIELD_FORMAT = 120
20
+ INVALID_FIELD_FORMAT = 119
21
+ INVALID_FILTERS_SYNTAX = 120
22
+ SAVE_FAILED = 121
22
23
  FORBIDDEN = 403
23
24
  RECORD_NOT_FOUND = 404
24
25
  UNSUPPORTED_MEDIA_TYPE = 415
@@ -43,10 +44,12 @@ module JSONAPI
43
44
  TYPE_MISMATCH => 'TYPE_MISMATCH',
44
45
  INVALID_PAGE_OBJECT => 'INVALID_PAGE_OBJECT',
45
46
  INVALID_PAGE_VALUE => 'INVALID_PAGE_VALUE',
46
- INVALID_SORT_FORMAT => 'INVALID_SORT_FORMAT',
47
47
  INVALID_FIELD_FORMAT => 'INVALID_FIELD_FORMAT',
48
+ INVALID_FILTERS_SYNTAX => 'INVALID_FILTERS_SYNTAX',
49
+ SAVE_FAILED => 'SAVE_FAILED',
48
50
  FORBIDDEN => 'FORBIDDEN',
49
51
  RECORD_NOT_FOUND => 'RECORD_NOT_FOUND',
50
52
  UNSUPPORTED_MEDIA_TYPE => 'UNSUPPORTED_MEDIA_TYPE',
51
- LOCKED => 'LOCKED' }
53
+ LOCKED => 'LOCKED'
54
+ }
52
55
  end
@@ -67,6 +67,20 @@ module JSONAPI
67
67
  end
68
68
  end
69
69
 
70
+ class InvalidFiltersSyntax < Error
71
+ attr_accessor :filters
72
+ def initialize(filters)
73
+ @filters = filters
74
+ end
75
+
76
+ def errors
77
+ [JSONAPI::Error.new(code: JSONAPI::INVALID_FILTERS_SYNTAX,
78
+ status: :bad_request,
79
+ title: 'Invalid filters syntax',
80
+ detail: "#{filters} is not a valid syntax for filtering.")]
81
+ end
82
+ end
83
+
70
84
  class FilterNotAllowed < Error
71
85
  attr_accessor :filter
72
86
  def initialize(filter)
@@ -188,21 +202,6 @@ module JSONAPI
188
202
  end
189
203
  end
190
204
 
191
- class InvalidSortFormat < Error
192
- attr_accessor :sort_criteria, :resource
193
- def initialize(resource, sort_criteria)
194
- @resource = resource
195
- @sort_criteria = sort_criteria
196
- end
197
-
198
- def errors
199
- [JSONAPI::Error.new(code: JSONAPI::INVALID_SORT_FORMAT,
200
- status: :bad_request,
201
- title: 'Invalid sort format',
202
- detail: "#{sort_criteria} must start with a direction (+ or -)")]
203
- end
204
- end
205
-
206
205
  class ParametersNotAllowed < Error
207
206
  attr_accessor :params
208
207
  def initialize(params)
@@ -306,6 +305,15 @@ module JSONAPI
306
305
  end
307
306
  end
308
307
 
308
+ class SaveFailed < Error
309
+ def errors
310
+ [JSONAPI::Error.new(code: JSONAPI::SAVE_FAILED,
311
+ status: :unprocessable_entity,
312
+ title: 'Save failed or was cancelled',
313
+ detail: 'Save failed or was cancelled')]
314
+ end
315
+ end
316
+
309
317
  class InvalidPageObject < Error
310
318
  def errors
311
319
  [JSONAPI::Error.new(code: JSONAPI::INVALID_PAGE_OBJECT,
@@ -11,7 +11,7 @@ module JSONAPI
11
11
 
12
12
  def formatter_for(format)
13
13
  formatter_class_name = "#{format.to_s.camelize}Formatter"
14
- formatter_class_name.safe_constantize if formatter_class_name
14
+ formatter_class_name.safe_constantize
15
15
  end
16
16
  end
17
17
  end
@@ -42,11 +42,11 @@ module JSONAPI
42
42
 
43
43
  class ValueFormatter < Formatter
44
44
  class << self
45
- def format(raw_value, context)
45
+ def format(raw_value)
46
46
  super(raw_value)
47
47
  end
48
48
 
49
- def unformat(value, context)
49
+ def unformat(value)
50
50
  super(value)
51
51
  end
52
52
 
@@ -87,7 +87,7 @@ end
87
87
 
88
88
  class DefaultValueFormatter < JSONAPI::ValueFormatter
89
89
  class << self
90
- def format(raw_value, context)
90
+ def format(raw_value)
91
91
  raw_value
92
92
  end
93
93
  end
@@ -95,7 +95,7 @@ end
95
95
 
96
96
  class IdValueFormatter < JSONAPI::ValueFormatter
97
97
  class << self
98
- def format(raw_value, context)
98
+ def format(raw_value)
99
99
  return if raw_value.nil?
100
100
  raw_value.to_s
101
101
  end
@@ -0,0 +1,67 @@
1
+ module JSONAPI
2
+ class IncludeDirectives
3
+
4
+ # Construct an IncludeDirectives Hash from an array of dot separated include strings.
5
+ # For example ['posts.comments.tags']
6
+ # will transform into =>
7
+ # {
8
+ # :posts=>{
9
+ # :include=>true,
10
+ # :include_related=>{
11
+ # :comments=>{
12
+ # :include=>true,
13
+ # :include_related=>{
14
+ # :tags=>{
15
+ # :include=>true
16
+ # }
17
+ # }
18
+ # }
19
+ # }
20
+ # }
21
+ # }
22
+
23
+ def initialize(includes_array)
24
+ @include_directives_hash = {include_related: {}}
25
+ includes_array.each do |include|
26
+ parse_include(include)
27
+ end
28
+ end
29
+
30
+ def include_directives
31
+ @include_directives_hash
32
+ end
33
+
34
+ def model_includes
35
+ get_includes(@include_directives_hash)
36
+ end
37
+
38
+ private
39
+ def get_related(current_path)
40
+ current = @include_directives_hash
41
+ current_path.split('.').each do |fragment|
42
+ fragment = fragment.to_sym
43
+ current[:include_related][fragment] ||= {include: false, include_related: {}}
44
+ current = current[:include_related][fragment]
45
+ end
46
+ current
47
+ end
48
+
49
+ def get_includes(directive)
50
+ directive[:include_related].map do |name, directive|
51
+ sub = get_includes(directive)
52
+ sub.any? ? { name => sub } : name
53
+ end
54
+ end
55
+
56
+ def parse_include(include)
57
+ parts = include.split('.')
58
+ local_path = ''
59
+
60
+ parts.each do |name|
61
+ local_path += local_path.length > 0 ? ".#{name}" : name
62
+ related = get_related(local_path)
63
+ related[:include] = true
64
+ end
65
+ end
66
+ end
67
+ end