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.
- checksums.yaml +4 -4
- data/README.md +274 -102
- data/jsonapi-resources.gemspec +1 -0
- data/lib/jsonapi-resources.rb +15 -0
- data/lib/jsonapi/active_record_operations_processor.rb +21 -10
- data/lib/jsonapi/acts_as_resource_controller.rb +175 -0
- data/lib/jsonapi/configuration.rb +11 -0
- data/lib/jsonapi/error_codes.rb +7 -4
- data/lib/jsonapi/exceptions.rb +23 -15
- data/lib/jsonapi/formatter.rb +5 -5
- data/lib/jsonapi/include_directives.rb +67 -0
- data/lib/jsonapi/operation.rb +185 -65
- data/lib/jsonapi/operation_result.rb +38 -5
- data/lib/jsonapi/operation_results.rb +33 -0
- data/lib/jsonapi/operations_processor.rb +49 -9
- data/lib/jsonapi/paginator.rb +31 -17
- data/lib/jsonapi/request.rb +347 -163
- data/lib/jsonapi/resource.rb +159 -56
- data/lib/jsonapi/resource_controller.rb +1 -234
- data/lib/jsonapi/resource_serializer.rb +55 -69
- data/lib/jsonapi/resources/version.rb +1 -1
- data/lib/jsonapi/response_document.rb +87 -0
- data/lib/jsonapi/routing_ext.rb +17 -11
- data/test/controllers/controller_test.rb +602 -326
- data/test/fixtures/active_record.rb +96 -6
- data/test/fixtures/line_items.yml +7 -1
- data/test/fixtures/numeros_telefone.yml +3 -0
- data/test/fixtures/purchase_orders.yml +6 -0
- data/test/integration/requests/request_test.rb +129 -60
- data/test/integration/routes/routes_test.rb +17 -17
- data/test/test_helper.rb +23 -5
- data/test/unit/jsonapi_request/jsonapi_request_test.rb +48 -0
- data/test/unit/operation/operations_processor_test.rb +242 -54
- data/test/unit/resource/resource_test.rb +108 -2
- data/test/unit/serializer/include_directives_test.rb +108 -0
- data/test/unit/serializer/response_document_test.rb +61 -0
- data/test/unit/serializer/serializer_test.rb +679 -520
- metadata +26 -2
data/jsonapi-resources.gemspec
CHANGED
@@ -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
|
data/lib/jsonapi-resources.rb
CHANGED
@@ -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
|
-
|
1
|
+
class ActiveRecordOperationsProcessor < JSONAPI::OperationsProcessor
|
2
2
|
|
3
|
-
|
4
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
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
|
data/lib/jsonapi/error_codes.rb
CHANGED
@@ -17,8 +17,9 @@ module JSONAPI
|
|
17
17
|
TYPE_MISMATCH = 116
|
18
18
|
INVALID_PAGE_OBJECT = 117
|
19
19
|
INVALID_PAGE_VALUE = 118
|
20
|
-
|
21
|
-
|
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
|
data/lib/jsonapi/exceptions.rb
CHANGED
@@ -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,
|
data/lib/jsonapi/formatter.rb
CHANGED
@@ -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
|
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
|
45
|
+
def format(raw_value)
|
46
46
|
super(raw_value)
|
47
47
|
end
|
48
48
|
|
49
|
-
def unformat(value
|
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
|
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
|
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
|