jsonapi-resources 0.0.1
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 +7 -0
- data/.gitignore +20 -0
- data/Gemfile +22 -0
- data/LICENSE.txt +22 -0
- data/README.md +451 -0
- data/Rakefile +24 -0
- data/jsonapi-resources.gemspec +29 -0
- data/lib/jsonapi-resources.rb +2 -0
- data/lib/jsonapi/active_record_operations_processor.rb +17 -0
- data/lib/jsonapi/association.rb +45 -0
- data/lib/jsonapi/error.rb +17 -0
- data/lib/jsonapi/error_codes.rb +16 -0
- data/lib/jsonapi/exceptions.rb +177 -0
- data/lib/jsonapi/operation.rb +151 -0
- data/lib/jsonapi/operation_result.rb +15 -0
- data/lib/jsonapi/operations_processor.rb +47 -0
- data/lib/jsonapi/request.rb +254 -0
- data/lib/jsonapi/resource.rb +417 -0
- data/lib/jsonapi/resource_controller.rb +169 -0
- data/lib/jsonapi/resource_for.rb +25 -0
- data/lib/jsonapi/resource_serializer.rb +209 -0
- data/lib/jsonapi/resources/version.rb +5 -0
- data/lib/jsonapi/routing_ext.rb +104 -0
- data/test/config/database.yml +5 -0
- data/test/controllers/controller_test.rb +940 -0
- data/test/fixtures/active_record.rb +585 -0
- data/test/helpers/functional_helpers.rb +59 -0
- data/test/helpers/hash_helpers.rb +13 -0
- data/test/helpers/value_matchers.rb +60 -0
- data/test/helpers/value_matchers_test.rb +40 -0
- data/test/integration/requests/request_test.rb +39 -0
- data/test/integration/routes/routes_test.rb +85 -0
- data/test/test_helper.rb +98 -0
- data/test/unit/operation/operations_processor_test.rb +188 -0
- data/test/unit/resource/resource_test.rb +45 -0
- data/test/unit/serializer/serializer_test.rb +429 -0
- metadata +193 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
class OperationResult
|
3
|
+
attr_accessor :code, :errors, :resource
|
4
|
+
|
5
|
+
def initialize(code, resource = nil, errors = [])
|
6
|
+
@code = code
|
7
|
+
@resource = resource
|
8
|
+
@errors = errors
|
9
|
+
end
|
10
|
+
|
11
|
+
def has_errors?
|
12
|
+
errors.count > 0
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'jsonapi/operation_result'
|
2
|
+
|
3
|
+
module JSONAPI
|
4
|
+
class OperationsProcessor
|
5
|
+
|
6
|
+
def process(request)
|
7
|
+
@results = []
|
8
|
+
@resources = []
|
9
|
+
|
10
|
+
context = request.context
|
11
|
+
|
12
|
+
transaction {
|
13
|
+
request.operations.each do |operation|
|
14
|
+
before_operation(context, operation)
|
15
|
+
|
16
|
+
result = operation.apply(context)
|
17
|
+
|
18
|
+
after_operation(context, result)
|
19
|
+
|
20
|
+
@results.push(result)
|
21
|
+
if result.has_errors?
|
22
|
+
rollback
|
23
|
+
end
|
24
|
+
end
|
25
|
+
}
|
26
|
+
@results
|
27
|
+
end
|
28
|
+
|
29
|
+
def before_operation(context, operation)
|
30
|
+
end
|
31
|
+
|
32
|
+
def after_operation(context, result)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# The base OperationsProcessor provides no transaction support
|
38
|
+
# Override the transaction and rollback methods to provide transaction support.
|
39
|
+
# For ActiveRecord transactions you can use the ActiveRecordOperationsProcessor
|
40
|
+
def transaction
|
41
|
+
yield
|
42
|
+
end
|
43
|
+
|
44
|
+
def rollback
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,254 @@
|
|
1
|
+
require 'jsonapi/resource_for'
|
2
|
+
require 'jsonapi/operation'
|
3
|
+
|
4
|
+
module JSONAPI
|
5
|
+
class Request
|
6
|
+
include ResourceFor
|
7
|
+
|
8
|
+
attr_accessor :fields, :includes, :filters, :errors, :operations, :resource_klass, :context
|
9
|
+
|
10
|
+
def initialize(context = nil, params = nil)
|
11
|
+
@context = context
|
12
|
+
@errors = []
|
13
|
+
@operations = []
|
14
|
+
@fields = {}
|
15
|
+
@includes = []
|
16
|
+
@filters = {}
|
17
|
+
|
18
|
+
setup(params) if params
|
19
|
+
end
|
20
|
+
|
21
|
+
def setup(params)
|
22
|
+
@resource_klass ||= self.class.resource_for(params[:controller]) if params[:controller]
|
23
|
+
|
24
|
+
unless params.nil?
|
25
|
+
case params[:action]
|
26
|
+
when 'index'
|
27
|
+
parse_fields(params)
|
28
|
+
parse_includes(params)
|
29
|
+
parse_filters(params)
|
30
|
+
when 'show_associations'
|
31
|
+
when 'show'
|
32
|
+
parse_fields(params)
|
33
|
+
parse_includes(params)
|
34
|
+
when 'create'
|
35
|
+
parse_fields(params)
|
36
|
+
parse_includes(params)
|
37
|
+
parse_add_operation(params)
|
38
|
+
when 'create_association'
|
39
|
+
parse_fields(params)
|
40
|
+
parse_includes(params)
|
41
|
+
parse_add_association_operation(params)
|
42
|
+
when 'update'
|
43
|
+
parse_fields(params)
|
44
|
+
parse_includes(params)
|
45
|
+
parse_replace_operation(params)
|
46
|
+
when 'destroy'
|
47
|
+
parse_remove_operation(params)
|
48
|
+
when 'destroy_association'
|
49
|
+
parse_remove_association_operation(params)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def parse_fields(params)
|
55
|
+
fields = {}
|
56
|
+
|
57
|
+
# Extract the fields for each type from the fields parameters
|
58
|
+
unless params[:fields].nil?
|
59
|
+
if params[:fields].is_a?(String)
|
60
|
+
value = params[:fields]
|
61
|
+
resource_fields = value.split(',').map {|s| s.to_sym } unless value.nil? || value.empty?
|
62
|
+
type = @resource_klass._serialize_as
|
63
|
+
fields[type] = resource_fields
|
64
|
+
elsif params[:fields].is_a?(ActionController::Parameters)
|
65
|
+
params[:fields].each do |param, value|
|
66
|
+
resource_fields = value.split(',').map {|s| s.to_sym } unless value.nil? || value.empty?
|
67
|
+
type = param.to_sym
|
68
|
+
fields[type] = resource_fields
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Validate the fields
|
74
|
+
fields.each do |type, values|
|
75
|
+
fields[type] = []
|
76
|
+
type_resource = self.class.resource_for(type)
|
77
|
+
if type_resource.nil? || !(@resource_klass._type == type || @resource_klass._has_association?(type))
|
78
|
+
@errors.concat(JSONAPI::Exceptions::InvalidResource.new(type).errors)
|
79
|
+
else
|
80
|
+
unless values.nil?
|
81
|
+
values.each do |field|
|
82
|
+
if type_resource._validate_field(field)
|
83
|
+
fields[type].push field
|
84
|
+
else
|
85
|
+
@errors.concat(JSONAPI::Exceptions::InvalidField.new(type, field).errors)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
else
|
89
|
+
@errors.concat(JSONAPI::Exceptions::InvalidField.new(type, 'nil').errors)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
@fields = fields
|
95
|
+
end
|
96
|
+
|
97
|
+
def parse_includes(params)
|
98
|
+
includes = params[:include]
|
99
|
+
included_resources = []
|
100
|
+
included_resources += CSV.parse_line(includes) unless includes.nil? || includes.empty?
|
101
|
+
@includes = included_resources
|
102
|
+
end
|
103
|
+
|
104
|
+
def parse_filters(params)
|
105
|
+
# Coerce :ids -> :id
|
106
|
+
if params[:ids]
|
107
|
+
params[:id] = params[:ids]
|
108
|
+
params.delete(:ids)
|
109
|
+
end
|
110
|
+
|
111
|
+
filters = {}
|
112
|
+
params.each do |key, value|
|
113
|
+
filter = key.to_sym
|
114
|
+
|
115
|
+
if [:include, :fields, :format, :controller, :action, :sort].include?(filter)
|
116
|
+
# Ignore non-filter parameters
|
117
|
+
elsif @resource_klass._allowed_filter?(filter)
|
118
|
+
filters[filter] = value
|
119
|
+
else
|
120
|
+
@errors.concat(JSONAPI::Exceptions::FilterNotAllowed.new(filter).errors)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
@filters = filters
|
124
|
+
end
|
125
|
+
|
126
|
+
def parse_add_operation(params)
|
127
|
+
object_params_raw = params.require(@resource_klass._type)
|
128
|
+
|
129
|
+
if object_params_raw.is_a?(Array)
|
130
|
+
object_params_raw.each do |p|
|
131
|
+
@operations.push JSONAPI::CreateResourceOperation.new(@resource_klass,
|
132
|
+
@resource_klass.verify_create_params(p, @context))
|
133
|
+
end
|
134
|
+
else
|
135
|
+
@operations.push JSONAPI::CreateResourceOperation.new(@resource_klass,
|
136
|
+
@resource_klass.verify_create_params(object_params_raw,
|
137
|
+
@context))
|
138
|
+
end
|
139
|
+
rescue ActionController::ParameterMissing => e
|
140
|
+
@errors.concat(JSONAPI::Exceptions::ParameterMissing.new(e.param).errors)
|
141
|
+
rescue JSONAPI::Exceptions::Error => e
|
142
|
+
@errors.concat(e.errors)
|
143
|
+
end
|
144
|
+
|
145
|
+
def parse_add_association_operation(params)
|
146
|
+
association_type = params[:association].to_sym
|
147
|
+
|
148
|
+
parent_key = params[resource_klass._as_parent_key]
|
149
|
+
|
150
|
+
if params[association_type].nil?
|
151
|
+
raise ActionController::ParameterMissing.new(association_type)
|
152
|
+
end
|
153
|
+
|
154
|
+
object_params = {links: {association_type => params[association_type]}}
|
155
|
+
verified_param_set = @resource_klass.verify_update_params(object_params, @context)
|
156
|
+
|
157
|
+
association = resource_klass._association(association_type)
|
158
|
+
|
159
|
+
if association.is_a?(JSONAPI::Association::HasOne)
|
160
|
+
@operations.push JSONAPI::ReplaceHasOneAssociationOperation.new(resource_klass,
|
161
|
+
parent_key,
|
162
|
+
association_type,
|
163
|
+
verified_param_set[:has_one].values[0])
|
164
|
+
else
|
165
|
+
@operations.push JSONAPI::CreateHasManyAssociationOperation.new(resource_klass,
|
166
|
+
parent_key,
|
167
|
+
association_type,
|
168
|
+
verified_param_set[:has_many].values[0])
|
169
|
+
end
|
170
|
+
rescue ActionController::ParameterMissing => e
|
171
|
+
@errors.concat(JSONAPI::Exceptions::ParameterMissing.new(e.param).errors)
|
172
|
+
end
|
173
|
+
|
174
|
+
def parse_replace_operation(params)
|
175
|
+
object_params_raw = params.require(@resource_klass._type)
|
176
|
+
|
177
|
+
keys = params[@resource_klass._key]
|
178
|
+
if object_params_raw.is_a?(Array)
|
179
|
+
if keys.count != object_params_raw.count
|
180
|
+
raise JSONAPI::Exceptions::CountMismatch
|
181
|
+
end
|
182
|
+
|
183
|
+
object_params_raw.each do |object_params|
|
184
|
+
if object_params[@resource_klass._key].nil?
|
185
|
+
raise JSONAPI::Exceptions::MissingKey.new
|
186
|
+
end
|
187
|
+
|
188
|
+
if !keys.include?(object_params[@resource_klass._key])
|
189
|
+
raise JSONAPI::Exceptions::KeyNotIncludedInURL.new(object_params[@resource_klass._key])
|
190
|
+
end
|
191
|
+
@operations.push JSONAPI::ReplaceFieldsOperation.new(@resource_klass,
|
192
|
+
object_params[@resource_klass._key],
|
193
|
+
@resource_klass.verify_update_params(object_params,
|
194
|
+
@context))
|
195
|
+
end
|
196
|
+
else
|
197
|
+
if !object_params_raw[@resource_klass._key].nil? && keys != object_params_raw[@resource_klass._key]
|
198
|
+
raise JSONAPI::Exceptions::KeyNotIncludedInURL.new(object_params_raw[@resource_klass._key])
|
199
|
+
end
|
200
|
+
|
201
|
+
@operations.push JSONAPI::ReplaceFieldsOperation.new(@resource_klass,
|
202
|
+
params[@resource_klass._key],
|
203
|
+
@resource_klass.verify_update_params(object_params_raw,
|
204
|
+
@context))
|
205
|
+
end
|
206
|
+
|
207
|
+
rescue ActionController::ParameterMissing => e
|
208
|
+
@errors.concat(JSONAPI::Exceptions::ParameterMissing.new(e.param).errors)
|
209
|
+
rescue JSONAPI::Exceptions::Error => e
|
210
|
+
@errors.concat(e.errors)
|
211
|
+
end
|
212
|
+
|
213
|
+
def parse_remove_operation(params)
|
214
|
+
keys = parse_key_array(params.permit(@resource_klass._key)[@resource_klass._key])
|
215
|
+
|
216
|
+
keys.each do |key|
|
217
|
+
@operations.push JSONAPI::RemoveResourceOperation.new(@resource_klass, key)
|
218
|
+
end
|
219
|
+
rescue ActionController::UnpermittedParameters => e
|
220
|
+
@errors.concat(JSONAPI::Exceptions::ParametersNotAllowed.new(e.params).errors)
|
221
|
+
rescue JSONAPI::Exceptions::Error => e
|
222
|
+
@errors.concat(e.errors)
|
223
|
+
end
|
224
|
+
|
225
|
+
def parse_remove_association_operation(params)
|
226
|
+
association_type = params[:association].to_sym
|
227
|
+
|
228
|
+
parent_key = params[resource_klass._as_parent_key]
|
229
|
+
|
230
|
+
association = resource_klass._association(association_type)
|
231
|
+
if association.is_a?(JSONAPI::Association::HasMany)
|
232
|
+
keys = parse_key_array(params[:keys])
|
233
|
+
keys.each do |key|
|
234
|
+
@operations.push JSONAPI::RemoveHasManyAssociationOperation.new(resource_klass,
|
235
|
+
parent_key,
|
236
|
+
association_type,
|
237
|
+
key)
|
238
|
+
end
|
239
|
+
else
|
240
|
+
@operations.push JSONAPI::RemoveHasOneAssociationOperation.new(resource_klass,
|
241
|
+
parent_key,
|
242
|
+
association_type)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def parse_key_array(raw)
|
247
|
+
keys = []
|
248
|
+
raw.split(/,/).collect do |key|
|
249
|
+
keys.push @resource_klass.verify_key(key, context)
|
250
|
+
end
|
251
|
+
return keys
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
@@ -0,0 +1,417 @@
|
|
1
|
+
require 'jsonapi/resource_for'
|
2
|
+
require 'jsonapi/association'
|
3
|
+
require 'action_dispatch/routing/mapper'
|
4
|
+
|
5
|
+
module JSONAPI
|
6
|
+
class Resource
|
7
|
+
include ResourceFor
|
8
|
+
|
9
|
+
@@resource_types = {}
|
10
|
+
|
11
|
+
attr_reader :object
|
12
|
+
|
13
|
+
def initialize(object = create_new_object)
|
14
|
+
@object = object
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_new_object
|
18
|
+
self.class._model_class.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def remove(context)
|
22
|
+
@object.destroy
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_has_many_link(association_type, association_key_value, context)
|
26
|
+
association = self.class._associations[association_type]
|
27
|
+
related_resource = self.class.resource_for(association.serialize_type_name).find_by_key(association_key_value, context)
|
28
|
+
|
29
|
+
@object.send(association.serialize_type_name) << related_resource.object
|
30
|
+
end
|
31
|
+
|
32
|
+
def replace_has_many_links(association_type, association_key_values, context)
|
33
|
+
association = self.class._associations[association_type]
|
34
|
+
|
35
|
+
@object.send("#{association.key}=", association_key_values)
|
36
|
+
end
|
37
|
+
|
38
|
+
def replace_has_one_link(association_type, association_key_value, context)
|
39
|
+
association = self.class._associations[association_type]
|
40
|
+
|
41
|
+
@object.send("#{association.key}=", association_key_value)
|
42
|
+
end
|
43
|
+
|
44
|
+
def remove_has_many_link(association_type, key, context)
|
45
|
+
association = self.class._associations[association_type]
|
46
|
+
|
47
|
+
@object.send(association.serialize_type_name).delete(key)
|
48
|
+
end
|
49
|
+
|
50
|
+
def remove_has_one_link(association_type, context)
|
51
|
+
association = self.class._associations[association_type]
|
52
|
+
|
53
|
+
@object.send("#{association.key}=", nil)
|
54
|
+
end
|
55
|
+
|
56
|
+
def replace_fields(field_data, context)
|
57
|
+
field_data[:attributes].each do |attribute, value|
|
58
|
+
send "#{attribute}=", value
|
59
|
+
end
|
60
|
+
|
61
|
+
field_data[:has_one].each do |association_type, value|
|
62
|
+
if value.nil?
|
63
|
+
remove_has_one_link(association_type, context)
|
64
|
+
else
|
65
|
+
replace_has_one_link(association_type, value, context)
|
66
|
+
end
|
67
|
+
end if field_data[:has_one]
|
68
|
+
|
69
|
+
field_data[:has_many].each do |association_type, values|
|
70
|
+
replace_has_many_links(association_type, values, context)
|
71
|
+
end if field_data[:has_many]
|
72
|
+
end
|
73
|
+
|
74
|
+
def save
|
75
|
+
@object.save!
|
76
|
+
rescue ActiveRecord::RecordInvalid => e
|
77
|
+
errors = []
|
78
|
+
e.record.errors.messages.each do |element|
|
79
|
+
element[1].each do |message|
|
80
|
+
errors.push(JSONAPI::Error.new(
|
81
|
+
code: JSONAPI::VALIDATION_ERROR,
|
82
|
+
status: :bad_request,
|
83
|
+
title: "#{element[0]} - #{message}",
|
84
|
+
detail: "can't be blank",
|
85
|
+
path: "\\#{element[0]}"))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
raise JSONAPI::Exceptions::ValidationErrors.new(errors)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Override this on a resource instance to override the fetchable keys
|
92
|
+
def fetchable(keys, context = nil)
|
93
|
+
keys
|
94
|
+
end
|
95
|
+
|
96
|
+
class << self
|
97
|
+
def inherited(base)
|
98
|
+
base._attributes = (_attributes || Set.new).dup
|
99
|
+
base._associations = (_associations || {}).dup
|
100
|
+
base._allowed_filters = (_allowed_filters || Set.new).dup
|
101
|
+
|
102
|
+
type = base.name.demodulize.sub(/Resource$/, '').underscore
|
103
|
+
base._type = type.pluralize.to_sym
|
104
|
+
# If eager loading is on this is how all the resource types are setup
|
105
|
+
# If eager loading is off some resource types will be initialized in
|
106
|
+
# _resource_name_from_type
|
107
|
+
@@resource_types[base._type] ||= base.name.demodulize
|
108
|
+
end
|
109
|
+
|
110
|
+
attr_accessor :_attributes, :_associations, :_allowed_filters , :_type
|
111
|
+
|
112
|
+
def routing_options(options)
|
113
|
+
@_routing_resource_options = options
|
114
|
+
end
|
115
|
+
|
116
|
+
def routing_resource_options
|
117
|
+
@_routing_resource_options ||= {}
|
118
|
+
end
|
119
|
+
|
120
|
+
# Methods used in defining a resource class
|
121
|
+
def attributes(*attrs)
|
122
|
+
@_attributes.merge attrs
|
123
|
+
attrs.each do |attr|
|
124
|
+
attribute(attr)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def attribute(attr)
|
129
|
+
@_attributes.add attr
|
130
|
+
define_method attr do
|
131
|
+
@object.method(attr).call
|
132
|
+
end unless method_defined?(attr)
|
133
|
+
|
134
|
+
define_method "#{attr}=" do |value|
|
135
|
+
@object.send "#{attr}=", value
|
136
|
+
end unless method_defined?("#{attr}=")
|
137
|
+
end
|
138
|
+
|
139
|
+
def has_one(*attrs)
|
140
|
+
_associate(Association::HasOne, *attrs)
|
141
|
+
end
|
142
|
+
|
143
|
+
def has_many(*attrs)
|
144
|
+
_associate(Association::HasMany, *attrs)
|
145
|
+
end
|
146
|
+
|
147
|
+
def model_name(model)
|
148
|
+
@_model_name = model.to_sym
|
149
|
+
end
|
150
|
+
|
151
|
+
def filters(*attrs)
|
152
|
+
@_allowed_filters.merge(attrs)
|
153
|
+
end
|
154
|
+
|
155
|
+
def filter(attr)
|
156
|
+
@_allowed_filters.add(attr.to_sym)
|
157
|
+
end
|
158
|
+
|
159
|
+
def key(key)
|
160
|
+
@_key = key.to_sym
|
161
|
+
end
|
162
|
+
|
163
|
+
# Override in your resource to filter the updateable keys
|
164
|
+
def updateable(keys, context = nil)
|
165
|
+
keys
|
166
|
+
end
|
167
|
+
|
168
|
+
# Override in your resource to filter the createable keys
|
169
|
+
def createable(keys, context = nil)
|
170
|
+
keys
|
171
|
+
end
|
172
|
+
|
173
|
+
# Override this method if you have more complex requirements than this basic find method provides
|
174
|
+
def find(filters, context = nil)
|
175
|
+
includes = []
|
176
|
+
where_filters = {}
|
177
|
+
|
178
|
+
filters.each do |filter, value|
|
179
|
+
if _associations.include?(filter)
|
180
|
+
if _associations[filter].is_a?(JSONAPI::Association::HasMany)
|
181
|
+
includes.push(filter.to_sym)
|
182
|
+
where_filters["#{filter}.#{_associations[filter].primary_key}"] = value
|
183
|
+
else
|
184
|
+
where_filters["#{_associations[filter].key}"] = value
|
185
|
+
end
|
186
|
+
else
|
187
|
+
where_filters[filter] = value
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
resources = []
|
192
|
+
_model_class.where(where_filters).includes(includes).each do |object|
|
193
|
+
resources.push self.new(object)
|
194
|
+
end
|
195
|
+
|
196
|
+
return resources
|
197
|
+
end
|
198
|
+
|
199
|
+
def find_by_key(key, context = nil)
|
200
|
+
obj = _model_class.where({_key => key}).first
|
201
|
+
if obj.nil?
|
202
|
+
raise JSONAPI::Exceptions::RecordNotFound.new(key)
|
203
|
+
end
|
204
|
+
self.new(obj)
|
205
|
+
end
|
206
|
+
|
207
|
+
def verify_create_params(object_params, context = nil)
|
208
|
+
verify_params(object_params, createable(_updateable_associations | _attributes.to_a), context)
|
209
|
+
end
|
210
|
+
|
211
|
+
def verify_update_params(object_params, context = nil)
|
212
|
+
verify_params(object_params, updateable(_updateable_associations | _attributes.to_a), context)
|
213
|
+
end
|
214
|
+
|
215
|
+
def verify_params(object_params, allowed_params, context)
|
216
|
+
# push links into top level param list with attributes in order to check for invalid params
|
217
|
+
if object_params[:links]
|
218
|
+
object_params[:links].each do |link, value|
|
219
|
+
object_params[link] = value
|
220
|
+
end
|
221
|
+
object_params.delete(:links)
|
222
|
+
end
|
223
|
+
verify_permitted_params(object_params, allowed_params)
|
224
|
+
|
225
|
+
checked_attributes = {}
|
226
|
+
checked_has_one_associations = {}
|
227
|
+
checked_has_many_associations = {}
|
228
|
+
|
229
|
+
object_params.each do |key, value|
|
230
|
+
param = key.to_sym
|
231
|
+
|
232
|
+
association = _associations[param]
|
233
|
+
|
234
|
+
if association.is_a?(JSONAPI::Association::HasOne)
|
235
|
+
checked_has_one_associations[param.to_sym] = resource_for(association.serialize_type_name).verify_key(value, context)
|
236
|
+
elsif association.is_a?(JSONAPI::Association::HasMany)
|
237
|
+
keys = []
|
238
|
+
value.each do |value|
|
239
|
+
keys.push(resource_for(association.serialize_type_name).verify_key(value, context))
|
240
|
+
end
|
241
|
+
checked_has_many_associations[param.to_sym] = keys
|
242
|
+
else
|
243
|
+
checked_attributes[param] = value
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
return {
|
248
|
+
attributes: checked_attributes,
|
249
|
+
has_one: checked_has_one_associations,
|
250
|
+
has_many: checked_has_many_associations
|
251
|
+
}
|
252
|
+
end
|
253
|
+
|
254
|
+
def verify_filters(filters, context = nil)
|
255
|
+
verified_filters = {}
|
256
|
+
filters.each do |filter, raw_value|
|
257
|
+
verified_filter = verify_filter(filter, raw_value, context)
|
258
|
+
verified_filters[verified_filter[0]] = verified_filter[1]
|
259
|
+
end
|
260
|
+
verified_filters
|
261
|
+
end
|
262
|
+
|
263
|
+
def is_filter_association?(filter)
|
264
|
+
filter == _serialize_as || _associations.include?(filter)
|
265
|
+
end
|
266
|
+
|
267
|
+
def verify_filter(filter, raw, context = nil)
|
268
|
+
filter_values = []
|
269
|
+
filter_values += CSV.parse_line(raw) unless raw.nil? || raw.empty?
|
270
|
+
|
271
|
+
if is_filter_association?(filter)
|
272
|
+
verify_association_filter(filter, filter_values, context)
|
273
|
+
else
|
274
|
+
verify_custom_filter(filter, filter_values, context)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
# override to allow for key processing and checking
|
279
|
+
def verify_key(key, context = nil)
|
280
|
+
return key
|
281
|
+
end
|
282
|
+
|
283
|
+
# override to allow for custom filters
|
284
|
+
def verify_custom_filter(filter, value, context = nil)
|
285
|
+
return filter, value
|
286
|
+
end
|
287
|
+
|
288
|
+
# override to allow for custom association logic, such as uuids, multiple keys or permission checks on keys
|
289
|
+
def verify_association_filter(filter, raw, context = nil)
|
290
|
+
return filter, raw
|
291
|
+
end
|
292
|
+
|
293
|
+
# quasi private class methods
|
294
|
+
def _updateable_associations
|
295
|
+
associations = []
|
296
|
+
|
297
|
+
@_associations.each do |key, association|
|
298
|
+
if association.is_a?(JSONAPI::Association::HasOne) || association.treat_as_set
|
299
|
+
associations.push(key)
|
300
|
+
end
|
301
|
+
end
|
302
|
+
associations
|
303
|
+
end
|
304
|
+
|
305
|
+
def _has_association?(type)
|
306
|
+
@_associations.has_key?(type)
|
307
|
+
end
|
308
|
+
|
309
|
+
def _association(type)
|
310
|
+
type = type.to_sym unless type.is_a?(Symbol)
|
311
|
+
@_associations[type]
|
312
|
+
end
|
313
|
+
|
314
|
+
def _model_name
|
315
|
+
@_model_name ||= self.name.demodulize.sub(/Resource$/, '')
|
316
|
+
end
|
317
|
+
|
318
|
+
def _serialize_as
|
319
|
+
@_serialize_as ||= self._type
|
320
|
+
end
|
321
|
+
|
322
|
+
def _key
|
323
|
+
@_key ||= :id
|
324
|
+
end
|
325
|
+
|
326
|
+
def _as_parent_key
|
327
|
+
@_as_parent_key ||= "#{_serialize_as.to_s.singularize}_#{_key}"
|
328
|
+
end
|
329
|
+
|
330
|
+
def _allowed_filters
|
331
|
+
!@_allowed_filters.nil? ? @_allowed_filters : Set.new([_key])
|
332
|
+
end
|
333
|
+
|
334
|
+
def _resource_name_from_type(type)
|
335
|
+
class_name = @@resource_types[type]
|
336
|
+
if class_name.nil?
|
337
|
+
class_name = type.to_s.singularize.camelize + 'Resource'
|
338
|
+
@@resource_types[type] = class_name
|
339
|
+
end
|
340
|
+
return class_name
|
341
|
+
end
|
342
|
+
|
343
|
+
if RUBY_VERSION >= '2.0'
|
344
|
+
def _model_class
|
345
|
+
@model ||= Object.const_get(_model_name)
|
346
|
+
end
|
347
|
+
else
|
348
|
+
def _model_class
|
349
|
+
@model ||= _model_name.to_s.safe_constantize
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
def _allowed_filter?(filter)
|
354
|
+
_allowed_filters.include?(filter.to_sym)
|
355
|
+
end
|
356
|
+
|
357
|
+
def _validate_field(field)
|
358
|
+
_attributes.include?(field) || _associations.key?(field)
|
359
|
+
end
|
360
|
+
|
361
|
+
private
|
362
|
+
|
363
|
+
def _associate(klass, *attrs)
|
364
|
+
options = attrs.extract_options!
|
365
|
+
|
366
|
+
attrs.each do |attr|
|
367
|
+
@_associations[attr] = klass.new(attr, options)
|
368
|
+
|
369
|
+
if @_associations[attr].is_a?(JSONAPI::Association::HasOne)
|
370
|
+
key = @_associations[attr].key
|
371
|
+
|
372
|
+
define_method key do
|
373
|
+
@object.method(key).call
|
374
|
+
end unless method_defined?(key)
|
375
|
+
|
376
|
+
define_method "_#{attr}_object" do
|
377
|
+
type_name = self.class._associations[attr].serialize_type_name
|
378
|
+
resource_class = self.class.resource_for(type_name)
|
379
|
+
if resource_class
|
380
|
+
associated_object = @object.send attr
|
381
|
+
return resource_class.new(associated_object)
|
382
|
+
end
|
383
|
+
end
|
384
|
+
elsif @_associations[attr].is_a?(JSONAPI::Association::HasMany)
|
385
|
+
key = @_associations[attr].key
|
386
|
+
|
387
|
+
define_method key do
|
388
|
+
@object.method(key).call
|
389
|
+
end unless method_defined?(key)
|
390
|
+
|
391
|
+
define_method "_#{attr}_objects" do
|
392
|
+
type_name = self.class._associations[attr].serialize_type_name
|
393
|
+
resource_class = self.class.resource_for(type_name)
|
394
|
+
resources = []
|
395
|
+
if resource_class
|
396
|
+
associated_objects = @object.send attr
|
397
|
+
associated_objects.each do |associated_object|
|
398
|
+
resources.push resource_class.new(associated_object)
|
399
|
+
end
|
400
|
+
end
|
401
|
+
return resources
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
def verify_permitted_params(params, allowed_param_set)
|
408
|
+
params_not_allowed = []
|
409
|
+
params.keys.each do |key|
|
410
|
+
param = key.to_sym
|
411
|
+
params_not_allowed.push(param) unless allowed_param_set.include?(param)
|
412
|
+
end
|
413
|
+
raise JSONAPI::Exceptions::ParametersNotAllowed.new(params_not_allowed) if params_not_allowed.length > 0
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|