jsonapi-resources 0.0.4 → 0.0.5

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