sanger-jsonapi-resources 0.1.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 (39) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +22 -0
  3. data/README.md +53 -0
  4. data/lib/generators/jsonapi/USAGE +13 -0
  5. data/lib/generators/jsonapi/controller_generator.rb +14 -0
  6. data/lib/generators/jsonapi/resource_generator.rb +14 -0
  7. data/lib/generators/jsonapi/templates/jsonapi_controller.rb +4 -0
  8. data/lib/generators/jsonapi/templates/jsonapi_resource.rb +4 -0
  9. data/lib/jsonapi/acts_as_resource_controller.rb +320 -0
  10. data/lib/jsonapi/cached_resource_fragment.rb +127 -0
  11. data/lib/jsonapi/callbacks.rb +51 -0
  12. data/lib/jsonapi/compiled_json.rb +36 -0
  13. data/lib/jsonapi/configuration.rb +258 -0
  14. data/lib/jsonapi/error.rb +47 -0
  15. data/lib/jsonapi/error_codes.rb +60 -0
  16. data/lib/jsonapi/exceptions.rb +563 -0
  17. data/lib/jsonapi/formatter.rb +169 -0
  18. data/lib/jsonapi/include_directives.rb +100 -0
  19. data/lib/jsonapi/link_builder.rb +152 -0
  20. data/lib/jsonapi/mime_types.rb +41 -0
  21. data/lib/jsonapi/naive_cache.rb +30 -0
  22. data/lib/jsonapi/operation.rb +24 -0
  23. data/lib/jsonapi/operation_dispatcher.rb +88 -0
  24. data/lib/jsonapi/operation_result.rb +65 -0
  25. data/lib/jsonapi/operation_results.rb +35 -0
  26. data/lib/jsonapi/paginator.rb +209 -0
  27. data/lib/jsonapi/processor.rb +328 -0
  28. data/lib/jsonapi/relationship.rb +94 -0
  29. data/lib/jsonapi/relationship_builder.rb +167 -0
  30. data/lib/jsonapi/request_parser.rb +678 -0
  31. data/lib/jsonapi/resource.rb +1255 -0
  32. data/lib/jsonapi/resource_controller.rb +5 -0
  33. data/lib/jsonapi/resource_controller_metal.rb +16 -0
  34. data/lib/jsonapi/resource_serializer.rb +531 -0
  35. data/lib/jsonapi/resources/version.rb +5 -0
  36. data/lib/jsonapi/response_document.rb +135 -0
  37. data/lib/jsonapi/routing_ext.rb +262 -0
  38. data/lib/jsonapi-resources.rb +27 -0
  39. metadata +223 -0
@@ -0,0 +1,209 @@
1
+ module JSONAPI
2
+ class Paginator
3
+ def initialize(_params)
4
+ end
5
+
6
+ def apply(_relation, _order_options)
7
+ # relation
8
+ end
9
+
10
+ def links_page_params(_options = {})
11
+ # :nocov:
12
+ {}
13
+ # :nocov:
14
+ end
15
+
16
+ class << self
17
+ def requires_record_count
18
+ # :nocov:
19
+ false
20
+ # :nocov:
21
+ end
22
+
23
+ def paginator_for(paginator)
24
+ paginator_class_name = "#{paginator.to_s.camelize}Paginator"
25
+ paginator_class_name.safe_constantize if paginator_class_name
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ class OffsetPaginator < JSONAPI::Paginator
32
+ attr_reader :limit, :offset
33
+
34
+ def initialize(params)
35
+ parse_pagination_params(params)
36
+ verify_pagination_params
37
+ end
38
+
39
+ def self.requires_record_count
40
+ true
41
+ end
42
+
43
+ def apply(relation, _order_options)
44
+ relation.offset(@offset).limit(@limit)
45
+ end
46
+
47
+ def links_page_params(options = {})
48
+ record_count = options[:record_count]
49
+ links_page_params = {}
50
+
51
+ links_page_params['first'] = {
52
+ 'offset' => 0,
53
+ 'limit' => @limit
54
+ }
55
+
56
+ if @offset > 0
57
+ previous_offset = @offset - @limit
58
+
59
+ previous_offset = 0 if previous_offset < 0
60
+
61
+ links_page_params['prev'] = {
62
+ 'offset' => previous_offset,
63
+ 'limit' => @limit
64
+ }
65
+ end
66
+
67
+ next_offset = @offset + @limit
68
+
69
+ unless next_offset >= record_count
70
+ links_page_params['next'] = {
71
+ 'offset' => next_offset,
72
+ 'limit' => @limit
73
+ }
74
+ end
75
+
76
+ if record_count
77
+ last_offset = record_count - @limit
78
+
79
+ last_offset = 0 if last_offset < 0
80
+
81
+ links_page_params['last'] = {
82
+ 'offset' => last_offset,
83
+ 'limit' => @limit
84
+ }
85
+ end
86
+
87
+ links_page_params
88
+ end
89
+
90
+ private
91
+
92
+ def parse_pagination_params(params)
93
+ if params.nil?
94
+ @offset = 0
95
+ @limit = JSONAPI.configuration.default_page_size
96
+ elsif params.is_a?(ActionController::Parameters)
97
+ validparams = params.permit(:offset, :limit)
98
+
99
+ @offset = validparams[:offset] ? validparams[:offset].to_i : 0
100
+ @limit = validparams[:limit] ? validparams[:limit].to_i : JSONAPI.configuration.default_page_size
101
+ else
102
+ fail JSONAPI::Exceptions::InvalidPageObject.new
103
+ end
104
+ rescue ActionController::UnpermittedParameters => e
105
+ raise JSONAPI::Exceptions::PageParametersNotAllowed.new(e.params)
106
+ end
107
+
108
+ def verify_pagination_params
109
+ if @limit < 1
110
+ fail JSONAPI::Exceptions::InvalidPageValue.new(:limit, @limit)
111
+ elsif @limit > JSONAPI.configuration.maximum_page_size
112
+ fail JSONAPI::Exceptions::InvalidPageValue.new(:limit, @limit,
113
+ detail: "Limit exceeds maximum page size of #{JSONAPI.configuration.maximum_page_size}.")
114
+ end
115
+
116
+ if @offset < 0
117
+ fail JSONAPI::Exceptions::InvalidPageValue.new(:offset, @offset)
118
+ end
119
+ end
120
+ end
121
+
122
+ class PagedPaginator < JSONAPI::Paginator
123
+ attr_reader :size, :number
124
+
125
+ def initialize(params)
126
+ parse_pagination_params(params)
127
+ verify_pagination_params
128
+ end
129
+
130
+ def self.requires_record_count
131
+ true
132
+ end
133
+
134
+ def calculate_page_count(record_count)
135
+ (record_count / @size.to_f).ceil
136
+ end
137
+
138
+ def apply(relation, _order_options)
139
+ offset = (@number - 1) * @size
140
+ relation.offset(offset).limit(@size)
141
+ end
142
+
143
+ def links_page_params(options = {})
144
+ record_count = options[:record_count]
145
+ page_count = calculate_page_count(record_count)
146
+
147
+ links_page_params = {}
148
+
149
+ links_page_params['first'] = {
150
+ 'number' => 1,
151
+ 'size' => @size
152
+ }
153
+
154
+ if @number > 1
155
+ links_page_params['prev'] = {
156
+ 'number' => @number - 1,
157
+ 'size' => @size
158
+ }
159
+ end
160
+
161
+ unless @number >= page_count
162
+ links_page_params['next'] = {
163
+ 'number' => @number + 1,
164
+ 'size' => @size
165
+ }
166
+ end
167
+
168
+ if record_count
169
+ links_page_params['last'] = {
170
+ 'number' => page_count == 0 ? 1 : page_count,
171
+ 'size' => @size
172
+ }
173
+ end
174
+
175
+ links_page_params
176
+ end
177
+
178
+ private
179
+
180
+ def parse_pagination_params(params)
181
+ if params.nil?
182
+ @number = 1
183
+ @size = JSONAPI.configuration.default_page_size
184
+ elsif params.is_a?(ActionController::Parameters)
185
+ validparams = params.permit(:number, :size)
186
+
187
+ @size = validparams[:size] ? validparams[:size].to_i : JSONAPI.configuration.default_page_size
188
+ @number = validparams[:number] ? validparams[:number].to_i : 1
189
+ else
190
+ @size = JSONAPI.configuration.default_page_size
191
+ @number = params.to_i
192
+ end
193
+ rescue ActionController::UnpermittedParameters => e
194
+ raise JSONAPI::Exceptions::PageParametersNotAllowed.new(e.params)
195
+ end
196
+
197
+ def verify_pagination_params
198
+ if @size < 1
199
+ fail JSONAPI::Exceptions::InvalidPageValue.new(:size, @size)
200
+ elsif @size > JSONAPI.configuration.maximum_page_size
201
+ fail JSONAPI::Exceptions::InvalidPageValue.new(:size, @size,
202
+ detail: "size exceeds maximum page size of #{JSONAPI.configuration.maximum_page_size}.")
203
+ end
204
+
205
+ if @number < 1
206
+ fail JSONAPI::Exceptions::InvalidPageValue.new(:number, @number)
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,328 @@
1
+ module JSONAPI
2
+ class Processor
3
+ include Callbacks
4
+ define_jsonapi_resources_callbacks :find,
5
+ :show,
6
+ :show_relationship,
7
+ :show_related_resource,
8
+ :show_related_resources,
9
+ :create_resource,
10
+ :remove_resource,
11
+ :replace_fields,
12
+ :replace_to_one_relationship,
13
+ :replace_polymorphic_to_one_relationship,
14
+ :create_to_many_relationships,
15
+ :replace_to_many_relationships,
16
+ :remove_to_many_relationships,
17
+ :remove_to_one_relationship,
18
+ :operation
19
+
20
+ class << self
21
+ def processor_instance_for(resource_klass, operation_type, params)
22
+ _processor_from_resource_type(resource_klass).new(resource_klass, operation_type, params)
23
+ end
24
+
25
+ def _processor_from_resource_type(resource_klass)
26
+ processor = resource_klass.name.gsub(/Resource$/,'Processor').safe_constantize
27
+ if processor.nil?
28
+ processor = JSONAPI.configuration.default_processor_klass
29
+ end
30
+
31
+ return processor
32
+ end
33
+
34
+ def transactional_operation_type?(operation_type)
35
+ case operation_type
36
+ when :find, :show, :show_related_resource, :show_related_resources
37
+ return false
38
+ else
39
+ return true
40
+ end
41
+ end
42
+ end
43
+
44
+ attr_reader :resource_klass, :operation_type, :params, :context, :result, :result_options
45
+
46
+ def initialize(resource_klass, operation_type, params)
47
+ @resource_klass = resource_klass
48
+ @operation_type = operation_type
49
+ @params = params
50
+ @context = params[:context]
51
+ @result = nil
52
+ @result_options = {}
53
+ end
54
+
55
+ def process
56
+ run_callbacks :operation do
57
+ run_callbacks operation_type do
58
+ @result = send(operation_type)
59
+ end
60
+ end
61
+
62
+ rescue JSONAPI::Exceptions::Error => e
63
+ @result = JSONAPI::ErrorsOperationResult.new(e.errors[0].code, e.errors)
64
+ end
65
+
66
+ def find
67
+ filters = params[:filters]
68
+ include_directives = params[:include_directives]
69
+ sort_criteria = params.fetch(:sort_criteria, [])
70
+ paginator = params[:paginator]
71
+ fields = params[:fields]
72
+
73
+ verified_filters = resource_klass.verify_filters(filters, context)
74
+ find_options = {
75
+ context: context,
76
+ include_directives: include_directives,
77
+ sort_criteria: sort_criteria,
78
+ paginator: paginator,
79
+ fields: fields
80
+ }
81
+
82
+ resource_records = if params[:cache_serializer]
83
+ resource_klass.find_serialized_with_caching(verified_filters,
84
+ params[:cache_serializer],
85
+ find_options)
86
+ else
87
+ resource_klass.find(verified_filters, find_options)
88
+ end
89
+
90
+ page_options = {}
91
+ if (JSONAPI.configuration.top_level_meta_include_record_count ||
92
+ (paginator && paginator.class.requires_record_count))
93
+ page_options[:record_count] = resource_klass.find_count(verified_filters,
94
+ context: context,
95
+ include_directives: include_directives)
96
+ end
97
+
98
+ if (JSONAPI.configuration.top_level_meta_include_page_count && page_options[:record_count])
99
+ page_options[:page_count] = paginator ? paginator.calculate_page_count(page_options[:record_count]) : 1
100
+ end
101
+
102
+ if JSONAPI.configuration.top_level_links_include_pagination && paginator
103
+ page_options[:pagination_params] = paginator.links_page_params(page_options)
104
+ end
105
+
106
+ return JSONAPI::ResourcesOperationResult.new(:ok, resource_records, page_options)
107
+ end
108
+
109
+ def show
110
+ include_directives = params[:include_directives]
111
+ fields = params[:fields]
112
+ id = params[:id]
113
+
114
+ key = resource_klass.verify_key(id, context)
115
+
116
+ find_options = {
117
+ context: context,
118
+ include_directives: include_directives,
119
+ fields: fields
120
+ }
121
+
122
+ resource_record = if params[:cache_serializer]
123
+ resource_klass.find_by_key_serialized_with_caching(key,
124
+ params[:cache_serializer],
125
+ find_options)
126
+ else
127
+ resource_klass.find_by_key(key, find_options)
128
+ end
129
+
130
+ return JSONAPI::ResourceOperationResult.new(:ok, resource_record)
131
+ end
132
+
133
+ def show_relationship
134
+ parent_key = params[:parent_key]
135
+ relationship_type = params[:relationship_type].to_sym
136
+
137
+ parent_resource = resource_klass.find_by_key(parent_key, context: context)
138
+
139
+ return JSONAPI::LinksObjectOperationResult.new(:ok,
140
+ parent_resource,
141
+ resource_klass._relationship(relationship_type))
142
+ end
143
+
144
+ def show_related_resource
145
+ source_klass = params[:source_klass]
146
+ source_id = params[:source_id]
147
+ relationship_type = params[:relationship_type].to_sym
148
+ fields = params[:fields]
149
+
150
+ # TODO Should fetch related_resource from cache if caching enabled
151
+ source_resource = source_klass.find_by_key(source_id, context: context, fields: fields)
152
+
153
+ related_resource = source_resource.public_send(relationship_type)
154
+
155
+ return JSONAPI::ResourceOperationResult.new(:ok, related_resource)
156
+ end
157
+
158
+ def show_related_resources
159
+ source_klass = params[:source_klass]
160
+ source_id = params[:source_id]
161
+ relationship_type = params[:relationship_type]
162
+ filters = params[:filters]
163
+ sort_criteria = params[:sort_criteria]
164
+ paginator = params[:paginator]
165
+ fields = params[:fields]
166
+ include_directives = params[:include_directives]
167
+
168
+ source_resource ||= source_klass.find_by_key(source_id, context: context, fields: fields)
169
+
170
+ rel_opts = {
171
+ filters: filters,
172
+ sort_criteria: sort_criteria,
173
+ paginator: paginator,
174
+ fields: fields,
175
+ context: context,
176
+ include_directives: include_directives
177
+ }
178
+
179
+ related_resources = nil
180
+ if params[:cache_serializer]
181
+ # TODO Could also avoid instantiating source_resource as actual Resource by
182
+ # allowing LinkBuilder to accept CachedResourceFragment as source in
183
+ # relationships_related_link
184
+ scope = source_resource.public_send(:"records_for_#{relationship_type}", rel_opts)
185
+ relationship = source_klass._relationship(relationship_type)
186
+ related_resources = relationship.resource_klass.find_serialized_with_caching(
187
+ scope,
188
+ params[:cache_serializer],
189
+ rel_opts
190
+ )
191
+ else
192
+ related_resources = source_resource.public_send(relationship_type, rel_opts)
193
+ end
194
+
195
+ if ((JSONAPI.configuration.top_level_meta_include_record_count) ||
196
+ (paginator && paginator.class.requires_record_count) ||
197
+ (JSONAPI.configuration.top_level_meta_include_page_count))
198
+ related_resource_records = source_resource.public_send("records_for_" + relationship_type)
199
+ records = resource_klass.filter_records(filters, {},
200
+ related_resource_records)
201
+
202
+ record_count = resource_klass.count_records(records)
203
+ end
204
+
205
+ if (JSONAPI.configuration.top_level_meta_include_page_count && record_count)
206
+ page_count = paginator.calculate_page_count(record_count)
207
+ end
208
+
209
+ pagination_params = if paginator && JSONAPI.configuration.top_level_links_include_pagination
210
+ page_options = {}
211
+ page_options[:record_count] = record_count if paginator.class.requires_record_count
212
+ paginator.links_page_params(page_options)
213
+ else
214
+ {}
215
+ end
216
+
217
+ opts = {}
218
+ opts.merge!(pagination_params: pagination_params) if JSONAPI.configuration.top_level_links_include_pagination
219
+ opts.merge!(record_count: record_count) if JSONAPI.configuration.top_level_meta_include_record_count
220
+ opts.merge!(page_count: page_count) if JSONAPI.configuration.top_level_meta_include_page_count
221
+
222
+ return JSONAPI::RelatedResourcesOperationResult.new(:ok,
223
+ source_resource,
224
+ relationship_type,
225
+ related_resources,
226
+ opts)
227
+ end
228
+
229
+ def create_resource
230
+ data = params[:data]
231
+ resource = resource_klass.create(context)
232
+ result = resource.replace_fields(data)
233
+
234
+ return JSONAPI::ResourceOperationResult.new((result == :completed ? :created : :accepted), resource)
235
+ end
236
+
237
+ def remove_resource
238
+ resource_id = params[:resource_id]
239
+
240
+ resource = resource_klass.find_by_key(resource_id, context: context)
241
+ result = resource.remove
242
+
243
+ return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
244
+ end
245
+
246
+ def replace_fields
247
+ resource_id = params[:resource_id]
248
+ data = params[:data]
249
+
250
+ resource = resource_klass.find_by_key(resource_id, context: context)
251
+ result = resource.replace_fields(data)
252
+
253
+ return JSONAPI::ResourceOperationResult.new(result == :completed ? :ok : :accepted, resource)
254
+ end
255
+
256
+ def replace_to_one_relationship
257
+ resource_id = params[:resource_id]
258
+ relationship_type = params[:relationship_type].to_sym
259
+ key_value = params[:key_value]
260
+
261
+ resource = resource_klass.find_by_key(resource_id, context: context)
262
+ result = resource.replace_to_one_link(relationship_type, key_value)
263
+
264
+ return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
265
+ end
266
+
267
+ def replace_polymorphic_to_one_relationship
268
+ resource_id = params[:resource_id]
269
+ relationship_type = params[:relationship_type].to_sym
270
+ key_value = params[:key_value]
271
+ key_type = params[:key_type]
272
+
273
+ resource = resource_klass.find_by_key(resource_id, context: context)
274
+ result = resource.replace_polymorphic_to_one_link(relationship_type, key_value, key_type)
275
+
276
+ return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
277
+ end
278
+
279
+ def create_to_many_relationships
280
+ resource_id = params[:resource_id]
281
+ relationship_type = params[:relationship_type].to_sym
282
+ data = params[:data]
283
+
284
+ resource = resource_klass.find_by_key(resource_id, context: context)
285
+ result = resource.create_to_many_links(relationship_type, data)
286
+
287
+ return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
288
+ end
289
+
290
+ def replace_to_many_relationships
291
+ resource_id = params[:resource_id]
292
+ relationship_type = params[:relationship_type].to_sym
293
+ data = params.fetch(:data)
294
+
295
+ resource = resource_klass.find_by_key(resource_id, context: context)
296
+ result = resource.replace_to_many_links(relationship_type, data)
297
+
298
+ return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
299
+ end
300
+
301
+ def remove_to_many_relationships
302
+ resource_id = params[:resource_id]
303
+ relationship_type = params[:relationship_type].to_sym
304
+ associated_keys = params[:associated_keys]
305
+
306
+ resource = resource_klass.find_by_key(resource_id, context: context)
307
+
308
+ complete = true
309
+ associated_keys.each do |key|
310
+ result = resource.remove_to_many_link(relationship_type, key)
311
+ if complete && result != :completed
312
+ complete = false
313
+ end
314
+ end
315
+ return JSONAPI::OperationResult.new(complete ? :no_content : :accepted)
316
+ end
317
+
318
+ def remove_to_one_relationship
319
+ resource_id = params[:resource_id]
320
+ relationship_type = params[:relationship_type].to_sym
321
+
322
+ resource = resource_klass.find_by_key(resource_id, context: context)
323
+ result = resource.remove_to_one_link(relationship_type)
324
+
325
+ return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
326
+ end
327
+ end
328
+ end
@@ -0,0 +1,94 @@
1
+ module JSONAPI
2
+ class Relationship
3
+ attr_reader :acts_as_set, :foreign_key, :options, :name,
4
+ :class_name, :polymorphic, :always_include_linkage_data,
5
+ :parent_resource, :eager_load_on_include
6
+
7
+ def initialize(name, options = {})
8
+ @name = name.to_s
9
+ @options = options
10
+ @acts_as_set = options.fetch(:acts_as_set, false) == true
11
+ @foreign_key = options[:foreign_key] ? options[:foreign_key].to_sym : nil
12
+ @parent_resource = options[:parent_resource]
13
+ @relation_name = options.fetch(:relation_name, @name)
14
+ @polymorphic = options.fetch(:polymorphic, false) == true
15
+ @always_include_linkage_data = options.fetch(:always_include_linkage_data, false) == true
16
+ @eager_load_on_include = options.fetch(:eager_load_on_include, true) == true
17
+ end
18
+
19
+ alias_method :polymorphic?, :polymorphic
20
+
21
+ def primary_key
22
+ @primary_key ||= resource_klass._primary_key
23
+ end
24
+
25
+ def resource_klass
26
+ @resource_klass ||= @parent_resource.resource_for(@class_name)
27
+ end
28
+
29
+ def table_name
30
+ @table_name ||= resource_klass._table_name
31
+ end
32
+
33
+ def type
34
+ @type ||= resource_klass._type.to_sym
35
+ end
36
+
37
+ def relation_name(options)
38
+ case @relation_name
39
+ when Symbol
40
+ # :nocov:
41
+ @relation_name
42
+ # :nocov:
43
+ when String
44
+ @relation_name.to_sym
45
+ when Proc
46
+ @relation_name.call(options)
47
+ end
48
+ end
49
+
50
+ def type_for_source(source)
51
+ if polymorphic?
52
+ resource = source.public_send(name)
53
+ resource.class._type if resource
54
+ else
55
+ type
56
+ end
57
+ end
58
+
59
+ def belongs_to?
60
+ false
61
+ end
62
+
63
+ class ToOne < Relationship
64
+ attr_reader :foreign_key_on
65
+
66
+ def initialize(name, options = {})
67
+ super
68
+ @class_name = options.fetch(:class_name, name.to_s.camelize)
69
+ @foreign_key ||= "#{name}_id".to_sym
70
+ @foreign_key_on = options.fetch(:foreign_key_on, :self)
71
+ end
72
+
73
+ def belongs_to?
74
+ foreign_key_on == :self
75
+ end
76
+
77
+ def polymorphic_type
78
+ "#{name}_type" if polymorphic?
79
+ end
80
+ end
81
+
82
+ class ToMany < Relationship
83
+ attr_reader :reflect, :inverse_relationship
84
+
85
+ def initialize(name, options = {})
86
+ super
87
+ @class_name = options.fetch(:class_name, name.to_s.camelize.singularize)
88
+ @foreign_key ||= "#{name.to_s.singularize}_ids".to_sym
89
+ @reflect = options.fetch(:reflect, true) == true
90
+ @inverse_relationship = options.fetch(:inverse_relationship, parent_resource._type.to_s.singularize.to_sym) if parent_resource
91
+ end
92
+ end
93
+ end
94
+ end