jsonapi-resources 0.0.4 → 0.0.5

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.
@@ -0,0 +1,94 @@
1
+ module JSONAPI
2
+ class Formatter
3
+ class << self
4
+ def format(arg)
5
+ arg.to_s
6
+ end
7
+
8
+ def unformat(arg)
9
+ arg
10
+ end
11
+
12
+ if RUBY_VERSION >= '2.0'
13
+ def formatter_for(format)
14
+ formatter_class_name = "#{format.to_s.camelize}Formatter"
15
+ Object.const_get formatter_class_name if formatter_class_name
16
+ end
17
+ else
18
+ def formatter_for(format)
19
+ formatter_class_name = "#{format.to_s.camelize}Formatter"
20
+ formatter_class_name.safe_constantize if formatter_class_name
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ class KeyFormatter < Formatter
27
+ class << self
28
+ def format(key)
29
+ super
30
+ end
31
+
32
+ def unformat(formatted_key)
33
+ super.to_sym
34
+ end
35
+ end
36
+ end
37
+
38
+ class ValueFormatter < Formatter
39
+ class << self
40
+ def format(raw_value, source, context)
41
+ super(raw_value)
42
+ end
43
+
44
+ def unformat(value, resource_klass, context)
45
+ super(value)
46
+ end
47
+
48
+ def value_formatter_for(type)
49
+ formatter_name = "#{type.to_s.camelize}Value"
50
+ formatter_for(formatter_name)
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ class UnderscoredKeyFormatter < JSONAPI::KeyFormatter
57
+ end
58
+
59
+ class CamelizedKeyFormatter < JSONAPI::KeyFormatter
60
+ class << self
61
+ def format(key)
62
+ super.camelize(:lower)
63
+ end
64
+
65
+ def unformat(formatted_key)
66
+ formatted_key.to_s.underscore.to_sym
67
+ end
68
+ end
69
+ end
70
+
71
+ class DasherizedKeyFormatter < JSONAPI::KeyFormatter
72
+ class << self
73
+ def format(key)
74
+ super.dasherize
75
+ end
76
+
77
+ def unformat(formatted_key)
78
+ formatted_key.to_s.underscore.to_sym
79
+ end
80
+ end
81
+ end
82
+
83
+ class DefaultValueFormatter < JSONAPI::ValueFormatter
84
+ class << self
85
+ def format(raw_value, source, context)
86
+ case raw_value
87
+ when String, Integer
88
+ return raw_value
89
+ else
90
+ return raw_value.to_s
91
+ end
92
+ end
93
+ end
94
+ end
@@ -67,9 +67,25 @@ module JSONAPI
67
67
  resource.save(context)
68
68
 
69
69
  return JSONAPI::OperationResult.new(:ok, resource)
70
+ end
71
+ end
70
72
 
71
- rescue JSONAPI::Exceptions::Error => e
72
- return JSONAPI::OperationResult.new(e.errors.count == 1 ? e.errors[0].code : :bad_request, nil, e.errors)
73
+ class CreateHasOneAssociationOperation < Operation
74
+ attr_reader :resource_id, :association_type, :key_value
75
+
76
+ def initialize(resource_klass, resource_id, association_type, key_value)
77
+ @resource_id = resource_id
78
+ @key_value = key_value
79
+ @association_type = association_type.to_sym
80
+ super(resource_klass)
81
+ end
82
+
83
+ def apply(context)
84
+ resource = @resource_klass.find_by_key(@resource_id, context)
85
+ resource.create_has_one_link(@association_type, @key_value, context)
86
+ resource.save(context)
87
+
88
+ return JSONAPI::OperationResult.new(:no_content)
73
89
  end
74
90
  end
75
91
 
@@ -79,7 +95,7 @@ module JSONAPI
79
95
  def initialize(resource_klass, resource_id, association_type, key_value)
80
96
  @resource_id = resource_id
81
97
  @key_value = key_value
82
- @association_type = association_type
98
+ @association_type = association_type.to_sym
83
99
  super(resource_klass)
84
100
  end
85
101
 
@@ -88,7 +104,7 @@ module JSONAPI
88
104
  resource.replace_has_one_link(@association_type, @key_value, context)
89
105
  resource.save(context)
90
106
 
91
- return JSONAPI::OperationResult.new(:created, resource)
107
+ return JSONAPI::OperationResult.new(:no_content)
92
108
  end
93
109
  end
94
110
 
@@ -98,7 +114,7 @@ module JSONAPI
98
114
  def initialize(resource_klass, resource_id, association_type, key_values)
99
115
  @resource_id = resource_id
100
116
  @key_values = key_values
101
- @association_type = association_type
117
+ @association_type = association_type.to_sym
102
118
  super(resource_klass)
103
119
  end
104
120
 
@@ -108,7 +124,26 @@ module JSONAPI
108
124
  resource.create_has_many_link(@association_type, value, context)
109
125
  end
110
126
 
111
- return JSONAPI::OperationResult.new(:created, resource)
127
+ return JSONAPI::OperationResult.new(:no_content)
128
+ end
129
+ end
130
+
131
+ class ReplaceHasManyAssociationOperation < Operation
132
+ attr_reader :resource_id, :association_type, :key_values
133
+
134
+ def initialize(resource_klass, resource_id, association_type, key_values)
135
+ @resource_id = resource_id
136
+ @key_values = key_values
137
+ @association_type = association_type.to_sym
138
+ super(resource_klass)
139
+ end
140
+
141
+ def apply(context)
142
+ resource = @resource_klass.find_by_key(@resource_id, context)
143
+ resource.replace_has_many_links(@association_type, @key_values, context)
144
+ resource.save(context)
145
+
146
+ return JSONAPI::OperationResult.new(:no_content)
112
147
  end
113
148
  end
114
149
 
@@ -118,7 +153,7 @@ module JSONAPI
118
153
  def initialize(resource_klass, resource_id, association_type, associated_key)
119
154
  @resource_id = resource_id
120
155
  @associated_key = associated_key
121
- @association_type = association_type
156
+ @association_type = association_type.to_sym
122
157
  super(resource_klass)
123
158
  end
124
159
 
@@ -127,8 +162,10 @@ module JSONAPI
127
162
  resource.remove_has_many_link(@association_type, @associated_key, context)
128
163
 
129
164
  return JSONAPI::OperationResult.new(:no_content)
130
- end
131
165
 
166
+ rescue ActiveRecord::RecordNotFound => e
167
+ raise JSONAPI::Exceptions::RecordNotFound.new(@associated_key)
168
+ end
132
169
  end
133
170
 
134
171
  class RemoveHasOneAssociationOperation < Operation
@@ -136,7 +173,7 @@ module JSONAPI
136
173
 
137
174
  def initialize(resource_klass, resource_id, association_type)
138
175
  @resource_id = resource_id
139
- @association_type = association_type
176
+ @association_type = association_type.to_sym
140
177
  super(resource_klass)
141
178
  end
142
179
 
@@ -5,14 +5,15 @@ module JSONAPI
5
5
  class Request
6
6
  include ResourceFor
7
7
 
8
- attr_accessor :fields, :includes, :filters, :errors, :operations, :resource_klass, :context
8
+ attr_accessor :fields, :include, :filters, :errors, :operations, :resource_klass, :context
9
9
 
10
- def initialize(context = nil, params = nil)
11
- @context = context
10
+ def initialize(params = nil, options = {})
11
+ @context = options.fetch(:context, nil)
12
+ @key_formatter = options.fetch(:key_formatter, JSONAPI.configuration.key_formatter)
12
13
  @errors = []
13
14
  @operations = []
14
15
  @fields = {}
15
- @includes = []
16
+ @include = []
16
17
  @filters = {}
17
18
 
18
19
  setup(params) if params
@@ -25,23 +26,23 @@ module JSONAPI
25
26
  case params[:action]
26
27
  when 'index'
27
28
  parse_fields(params)
28
- parse_includes(params)
29
+ parse_include(params)
29
30
  parse_filters(params)
30
31
  when 'show_associations'
31
32
  when 'show'
32
33
  parse_fields(params)
33
- parse_includes(params)
34
+ parse_include(params)
34
35
  when 'create'
35
36
  parse_fields(params)
36
- parse_includes(params)
37
+ parse_include(params)
37
38
  parse_add_operation(params)
38
39
  when 'create_association'
39
- parse_fields(params)
40
- parse_includes(params)
41
40
  parse_add_association_operation(params)
41
+ when 'update_association'
42
+ parse_update_association_operation(params)
42
43
  when 'update'
43
44
  parse_fields(params)
44
- parse_includes(params)
45
+ parse_include(params)
45
46
  parse_replace_operation(params)
46
47
  when 'destroy'
47
48
  parse_remove_operation(params)
@@ -58,13 +59,13 @@ module JSONAPI
58
59
  unless params[:fields].nil?
59
60
  if params[:fields].is_a?(String)
60
61
  value = params[:fields]
61
- resource_fields = value.split(',').map {|s| s.to_sym } unless value.nil? || value.empty?
62
- type = @resource_klass._serialize_as
62
+ resource_fields = value.split(',') unless value.nil? || value.empty?
63
+ type = @resource_klass._type
63
64
  fields[type] = resource_fields
64
65
  elsif params[:fields].is_a?(ActionController::Parameters)
65
66
  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
67
+ resource_fields = value.split(',') unless value.nil? || value.empty?
68
+ type = param
68
69
  fields[type] = resource_fields
69
70
  end
70
71
  end
@@ -72,15 +73,18 @@ module JSONAPI
72
73
 
73
74
  # Validate the fields
74
75
  fields.each do |type, values|
76
+ underscored_type = unformat_key(type)
75
77
  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
+ type_resource = self.class.resource_for(underscored_type)
79
+ if type_resource.nil? || !(@resource_klass._type == underscored_type ||
80
+ @resource_klass._has_association?(underscored_type))
78
81
  @errors.concat(JSONAPI::Exceptions::InvalidResource.new(type).errors)
79
82
  else
80
83
  unless values.nil?
84
+ valid_fields = type_resource.fields.collect {|key| format_key(key)}
81
85
  values.each do |field|
82
- if type_resource._validate_field(field)
83
- fields[type].push field
86
+ if valid_fields.include?(field)
87
+ fields[type].push unformat_key(field)
84
88
  else
85
89
  @errors.concat(JSONAPI::Exceptions::InvalidField.new(type, field).errors)
86
90
  end
@@ -91,14 +95,31 @@ module JSONAPI
91
95
  end
92
96
  end
93
97
 
94
- @fields = fields
98
+ @fields = fields.deep_transform_keys{ |key| unformat_key(key) }
99
+ end
100
+
101
+ def check_include(resource_klass, include_parts)
102
+ association_name = unformat_key(include_parts.first)
103
+
104
+ association = resource_klass._association(association_name)
105
+ if association
106
+ unless include_parts.last.empty?
107
+ check_include(Resource.resource_for(association.class_name), include_parts.last.partition('.'))
108
+ end
109
+ else
110
+ @errors.concat(JSONAPI::Exceptions::InvalidInclude.new(format_key(resource_klass._type),
111
+ include_parts.first, ).errors)
112
+ end
95
113
  end
96
114
 
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
115
+ def parse_include(params)
116
+ included_resources_raw = CSV.parse_line(params[:include]) unless params[:include].nil? || params[:include].empty?
117
+ @include = []
118
+ return if included_resources_raw.nil?
119
+ included_resources_raw.each do |include|
120
+ check_include(@resource_klass, include.partition('.'))
121
+ @include.push(unformat_key(include).to_s)
122
+ end
102
123
  end
103
124
 
104
125
  def parse_filters(params)
@@ -124,17 +145,16 @@ module JSONAPI
124
145
  end
125
146
 
126
147
  def parse_add_operation(params)
127
- object_params_raw = params.require(@resource_klass._type)
148
+ object_params_raw = params.require(format_key(@resource_klass._type))
128
149
 
129
150
  if object_params_raw.is_a?(Array)
130
151
  object_params_raw.each do |p|
131
152
  @operations.push JSONAPI::CreateResourceOperation.new(@resource_klass,
132
- @resource_klass.verify_create_params(p, @context))
153
+ parse_params(p, @resource_klass.createable_fields(@context)))
133
154
  end
134
155
  else
135
156
  @operations.push JSONAPI::CreateResourceOperation.new(@resource_klass,
136
- @resource_klass.verify_create_params(object_params_raw,
137
- @context))
157
+ parse_params(object_params_raw, @resource_klass.createable_fields(@context)))
138
158
  end
139
159
  rescue ActionController::ParameterMissing => e
140
160
  @errors.concat(JSONAPI::Exceptions::ParameterMissing.new(e.param).errors)
@@ -142,37 +162,141 @@ module JSONAPI
142
162
  @errors.concat(e.errors)
143
163
  end
144
164
 
165
+ def parse_params(params, allowed_fields)
166
+ # push links into top level param list with attributes in order to check for invalid params
167
+ if params[:links]
168
+ params[:links].each do |link, value|
169
+ params[link] = value
170
+ end
171
+ params.delete(:links)
172
+ end
173
+ verify_permitted_params(params, allowed_fields)
174
+
175
+ checked_attributes = {}
176
+ checked_has_one_associations = {}
177
+ checked_has_many_associations = {}
178
+
179
+ params.each do |key, value|
180
+ param = unformat_key(key)
181
+
182
+ association = @resource_klass._association(param)
183
+
184
+ if association.is_a?(JSONAPI::Association::HasOne)
185
+ checked_has_one_associations[param] = @resource_klass.resource_for(association.type).verify_key(value, context)
186
+ elsif association.is_a?(JSONAPI::Association::HasMany)
187
+ keys = []
188
+ if value.is_a?(Array)
189
+ value.each do |val|
190
+ keys.push(@resource_klass.resource_for(association.type).verify_key(val, context))
191
+ end
192
+ else
193
+ keys.push(@resource_klass.resource_for(association.type).verify_key(value, context))
194
+ end
195
+ checked_has_many_associations[param] = keys
196
+ else
197
+ checked_attributes[param] = unformat_value(param, value)
198
+ end
199
+ end
200
+
201
+ return {
202
+ 'attributes' => checked_attributes,
203
+ 'has_one' => checked_has_one_associations,
204
+ 'has_many' => checked_has_many_associations
205
+ }.deep_transform_keys{ |key| unformat_key(key) }
206
+ end
207
+
208
+ def unformat_value(attribute, value)
209
+ value_formatter = JSONAPI::ValueFormatter.value_formatter_for(@resource_klass._attribute_options(attribute)[:format])
210
+ value_formatter.unformat(value, @resource_klass, context)
211
+ end
212
+
213
+ def verify_permitted_params(params, allowed_fields)
214
+ formatted_allowed_fields = allowed_fields.collect {|field| format_key(field).to_sym}
215
+ params_not_allowed = []
216
+ params.keys.each do |key|
217
+ params_not_allowed.push(key) unless formatted_allowed_fields.include?(key.to_sym)
218
+ end
219
+ raise JSONAPI::Exceptions::ParametersNotAllowed.new(params_not_allowed) if params_not_allowed.length > 0
220
+ end
221
+
145
222
  def parse_add_association_operation(params)
146
- association_type = params[:association].to_sym
223
+ association_type = params[:association]
147
224
 
148
225
  parent_key = params[resource_klass._as_parent_key]
149
226
 
150
- if params[association_type].nil?
151
- raise ActionController::ParameterMissing.new(association_type)
227
+ association = resource_klass._association(association_type)
228
+
229
+ if association.is_a?(JSONAPI::Association::HasOne)
230
+ plural_association_type = association_type.pluralize
231
+
232
+ if params[plural_association_type].nil?
233
+ raise ActionController::ParameterMissing.new(plural_association_type)
234
+ end
235
+
236
+ object_params = {links: {association_type => params[plural_association_type]}}
237
+ verified_param_set = parse_params(object_params, @resource_klass.updateable_fields(@context))
238
+
239
+ @operations.push JSONAPI::CreateHasOneAssociationOperation.new(resource_klass,
240
+ parent_key,
241
+ association_type,
242
+ verified_param_set[:has_one].values[0])
243
+ else
244
+ if params[association_type].nil?
245
+ raise ActionController::ParameterMissing.new(association_type)
246
+ end
247
+
248
+ object_params = {links: {association_type => params[association_type]}}
249
+ verified_param_set = parse_params(object_params, @resource_klass.updateable_fields(@context))
250
+
251
+ @operations.push JSONAPI::CreateHasManyAssociationOperation.new(resource_klass,
252
+ parent_key,
253
+ association_type,
254
+ verified_param_set[:has_many].values[0])
152
255
  end
256
+ rescue ActionController::ParameterMissing => e
257
+ @errors.concat(JSONAPI::Exceptions::ParameterMissing.new(e.param).errors)
258
+ end
153
259
 
154
- object_params = {links: {association_type => params[association_type]}}
155
- verified_param_set = @resource_klass.verify_update_params(object_params, @context)
260
+ def parse_update_association_operation(params)
261
+ association_type = params[:association]
262
+
263
+ parent_key = params[resource_klass._as_parent_key]
156
264
 
157
265
  association = resource_klass._association(association_type)
158
266
 
159
267
  if association.is_a?(JSONAPI::Association::HasOne)
268
+ plural_association_type = association_type.pluralize
269
+
270
+ if params[plural_association_type].nil?
271
+ raise ActionController::ParameterMissing.new(plural_association_type)
272
+ end
273
+
274
+ object_params = {links: {association_type => params[plural_association_type]}}
275
+ verified_param_set = parse_params(object_params, @resource_klass.updateable_fields(@context))
276
+
160
277
  @operations.push JSONAPI::ReplaceHasOneAssociationOperation.new(resource_klass,
161
- parent_key,
162
- association_type,
163
- verified_param_set[:has_one].values[0])
278
+ parent_key,
279
+ association_type,
280
+ verified_param_set[:has_one].values[0])
164
281
  else
165
- @operations.push JSONAPI::CreateHasManyAssociationOperation.new(resource_klass,
166
- parent_key,
167
- association_type,
168
- verified_param_set[:has_many].values[0])
282
+ if params[association_type].nil?
283
+ raise ActionController::ParameterMissing.new(association_type)
284
+ end
285
+
286
+ object_params = {links: {association_type => params[association_type]}}
287
+ verified_param_set = parse_params(object_params, @resource_klass.updateable_fields(@context))
288
+
289
+ @operations.push JSONAPI::ReplaceHasManyAssociationOperation.new(resource_klass,
290
+ parent_key,
291
+ association_type,
292
+ verified_param_set[:has_many].values[0])
169
293
  end
170
294
  rescue ActionController::ParameterMissing => e
171
295
  @errors.concat(JSONAPI::Exceptions::ParameterMissing.new(e.param).errors)
172
296
  end
173
297
 
174
298
  def parse_replace_operation(params)
175
- object_params_raw = params.require(@resource_klass._type)
299
+ object_params_raw = params.require(format_key(@resource_klass._type))
176
300
 
177
301
  keys = params[@resource_klass._key]
178
302
  if object_params_raw.is_a?(Array)
@@ -189,9 +313,8 @@ module JSONAPI
189
313
  raise JSONAPI::Exceptions::KeyNotIncludedInURL.new(object_params[@resource_klass._key])
190
314
  end
191
315
  @operations.push JSONAPI::ReplaceFieldsOperation.new(@resource_klass,
192
- object_params[@resource_klass._key],
193
- @resource_klass.verify_update_params(object_params,
194
- @context))
316
+ object_params[@resource_klass._key],
317
+ parse_params(object_params, @resource_klass.updateable_fields(@context)))
195
318
  end
196
319
  else
197
320
  if !object_params_raw[@resource_klass._key].nil? && keys != object_params_raw[@resource_klass._key]
@@ -200,8 +323,7 @@ module JSONAPI
200
323
 
201
324
  @operations.push JSONAPI::ReplaceFieldsOperation.new(@resource_klass,
202
325
  params[@resource_klass._key],
203
- @resource_klass.verify_update_params(object_params_raw,
204
- @context))
326
+ parse_params(object_params_raw, @resource_klass.updateable_fields(@context)))
205
327
  end
206
328
 
207
329
  rescue ActionController::ParameterMissing => e
@@ -223,7 +345,7 @@ module JSONAPI
223
345
  end
224
346
 
225
347
  def parse_remove_association_operation(params)
226
- association_type = params[:association].to_sym
348
+ association_type = params[:association]
227
349
 
228
350
  parent_key = params[resource_klass._as_parent_key]
229
351
 
@@ -250,5 +372,13 @@ module JSONAPI
250
372
  end
251
373
  return keys
252
374
  end
375
+
376
+ def format_key(key)
377
+ @key_formatter.format(key)
378
+ end
379
+
380
+ def unformat_key(key)
381
+ @key_formatter.unformat(key)
382
+ end
253
383
  end
254
- end
384
+ end