jsonapionify 0.9.0 → 0.9.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.
Files changed (84) hide show
  1. checksums.yaml +13 -5
  2. data/.rubocop.yml +1 -0
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +8 -0
  5. data/README.md +85 -3
  6. data/Rakefile +14 -0
  7. data/jsonapionify.gemspec +3 -0
  8. data/lib/jsonapionify/api/action.rb +84 -121
  9. data/lib/jsonapionify/api/attribute.rb +97 -20
  10. data/lib/jsonapionify/api/base/class_methods.rb +5 -4
  11. data/lib/jsonapionify/api/base/delegation.rb +20 -4
  12. data/lib/jsonapionify/api/base/doc_helper.rb +3 -3
  13. data/lib/jsonapionify/api/base/reloader.rb +1 -1
  14. data/lib/jsonapionify/api/base/resource_definitions.rb +28 -15
  15. data/lib/jsonapionify/api/base.rb +6 -0
  16. data/lib/jsonapionify/api/context.rb +18 -5
  17. data/lib/jsonapionify/api/context_delegate.rb +24 -7
  18. data/lib/jsonapionify/api/errors.rb +2 -0
  19. data/lib/jsonapionify/api/errors_object.rb +6 -5
  20. data/lib/jsonapionify/api/relationship/blocks.rb +1 -1
  21. data/lib/jsonapionify/api/relationship/many.rb +35 -11
  22. data/lib/jsonapionify/api/relationship/one.rb +17 -7
  23. data/lib/jsonapionify/api/relationship.rb +20 -6
  24. data/lib/jsonapionify/api/resource/builders.rb +81 -30
  25. data/lib/jsonapionify/api/resource/caching.rb +28 -0
  26. data/lib/jsonapionify/api/resource/caller.rb +61 -0
  27. data/lib/jsonapionify/api/resource/class_methods.rb +6 -2
  28. data/lib/jsonapionify/api/resource/defaults/actions.rb +47 -0
  29. data/lib/jsonapionify/api/resource/defaults/errors.rb +61 -15
  30. data/lib/jsonapionify/api/resource/defaults/hooks.rb +68 -0
  31. data/lib/jsonapionify/api/resource/defaults/options.rb +16 -28
  32. data/lib/jsonapionify/api/resource/defaults/params.rb +3 -0
  33. data/lib/jsonapionify/api/resource/defaults/request_contexts.rb +80 -32
  34. data/lib/jsonapionify/api/resource/defaults/response_contexts.rb +13 -6
  35. data/lib/jsonapionify/api/resource/defaults.rb +1 -1
  36. data/lib/jsonapionify/api/resource/definitions/actions.rb +81 -55
  37. data/lib/jsonapionify/api/resource/definitions/attributes.rb +46 -10
  38. data/lib/jsonapionify/api/resource/definitions/contexts.rb +6 -2
  39. data/lib/jsonapionify/api/resource/definitions/helpers.rb +1 -1
  40. data/lib/jsonapionify/api/resource/definitions/pagination.rb +47 -56
  41. data/lib/jsonapionify/api/resource/definitions/params.rb +11 -15
  42. data/lib/jsonapionify/api/resource/definitions/relationships.rb +43 -7
  43. data/lib/jsonapionify/api/resource/definitions/request_headers.rb +6 -3
  44. data/lib/jsonapionify/api/resource/definitions/response_headers.rb +1 -1
  45. data/lib/jsonapionify/api/resource/definitions/scopes.rb +5 -5
  46. data/lib/jsonapionify/api/resource/definitions/sorting.rb +12 -11
  47. data/lib/jsonapionify/api/resource/definitions.rb +1 -1
  48. data/lib/jsonapionify/api/resource/error_handling.rb +92 -20
  49. data/lib/jsonapionify/api/resource/exec.rb +11 -0
  50. data/lib/jsonapionify/api/resource/includer.rb +89 -1
  51. data/lib/jsonapionify/api/resource.rb +55 -8
  52. data/lib/jsonapionify/api/response.rb +43 -14
  53. data/lib/jsonapionify/api/server/media_type.rb +36 -0
  54. data/lib/jsonapionify/api/server/request.rb +25 -11
  55. data/lib/jsonapionify/api/server.rb +8 -4
  56. data/lib/jsonapionify/api/sort_field.rb +18 -0
  57. data/lib/jsonapionify/api/sort_field_set.rb +1 -1
  58. data/lib/jsonapionify/api/test_helper.rb +46 -0
  59. data/lib/jsonapionify/documentation/template.erb +2 -2
  60. data/lib/jsonapionify/documentation.rb +10 -0
  61. data/lib/jsonapionify/structure/collections/base.rb +10 -3
  62. data/lib/jsonapionify/structure/helpers/object_defaults.rb +5 -10
  63. data/lib/jsonapionify/structure/maps/relationships.rb +4 -0
  64. data/lib/jsonapionify/structure/objects/attributes.rb +4 -0
  65. data/lib/jsonapionify/structure/objects/base.rb +22 -9
  66. data/lib/jsonapionify/structure/objects/error.rb +2 -0
  67. data/lib/jsonapionify/structure/objects/jsonapi.rb +1 -0
  68. data/lib/jsonapionify/structure/objects/link.rb +1 -0
  69. data/lib/jsonapionify/structure/objects/relationship.rb +2 -0
  70. data/lib/jsonapionify/structure/objects/resource.rb +2 -0
  71. data/lib/jsonapionify/structure/objects/resource_identifier.rb +12 -4
  72. data/lib/jsonapionify/structure/objects/top_level.rb +4 -2
  73. data/lib/jsonapionify/types/array_type.rb +16 -11
  74. data/lib/jsonapionify/types/boolean_type.rb +9 -4
  75. data/lib/jsonapionify/types/date_string_type.rb +7 -10
  76. data/lib/jsonapionify/types/float_type.rb +13 -0
  77. data/lib/jsonapionify/types/integer_type.rb +12 -0
  78. data/lib/jsonapionify/types/object_type.rb +7 -2
  79. data/lib/jsonapionify/types/string_type.rb +12 -0
  80. data/lib/jsonapionify/types/time_string_type.rb +8 -10
  81. data/lib/jsonapionify/types.rb +43 -5
  82. data/lib/jsonapionify/version.rb +1 -1
  83. data/lib/jsonapionify.rb +36 -1
  84. metadata +121 -74
@@ -9,82 +9,109 @@ module JSONAPIonify::Api
9
9
  klass.class_eval do
10
10
  extend JSONAPIonify::InheritedAttributes
11
11
  include JSONAPIonify::Callbacks
12
- define_callbacks :request, :list, :create, :read, :update, :delete,
13
- :show, :add, :remove, :replace, :exception
12
+ define_callbacks(
13
+ :request, :exception, :response,
14
+ :list, :commit_list,
15
+ :create, :commit_create,
16
+ :read, :commit_read,
17
+ :update, :commit_update,
18
+ :delete, :commit_delete,
19
+ :show, :commit_show,
20
+ :add, :commit_add,
21
+ :remove, :commit_remove,
22
+ :replace, :commit_replace
23
+ )
14
24
  inherited_array_attribute :action_definitions
15
25
  end
16
26
  end
17
27
 
18
- def list(**options, &block)
28
+ def list(content_type: nil, only_associated: false, callbacks: true, &block)
29
+ options = {
30
+ content_type: content_type,
31
+ only_associated: only_associated,
32
+ callbacks: callbacks,
33
+ cacheable: true
34
+ }
19
35
  define_action(:list, 'GET', **options, &block).tap do |action|
20
36
  action.response status: 200 do |context|
37
+ builder = context.respond_to?(:builder) ? context.builder : nil
21
38
  context.response_object[:data] = build_collection(
22
- context.request,
39
+ context,
23
40
  context.response_collection,
24
- fields: context.fields
41
+ fields: context.fields,
42
+ include_cursors: (context.links.keys & [:first, :last, :next, :prev]).present?,
43
+ &builder
25
44
  )
26
45
  context.response_object.to_json
27
46
  end
28
47
  end
29
48
  end
30
49
 
31
- def index(**options, &block)
32
- warn 'the `index` action will soon be deprecated, use `list` instead!'
33
- list(**options, &block)
34
- end
35
-
36
- def create(**options, &block)
50
+ def create(content_type: nil, only_associated: false, callbacks: true, &block)
51
+ options = {
52
+ content_type: content_type,
53
+ only_associated: only_associated,
54
+ callbacks: callbacks,
55
+ cacheable: false,
56
+ example_input: :resource
57
+ }
37
58
  define_action(:create, 'POST', **options, &block).tap do |action|
38
59
  action.response status: 201 do |context|
39
- context.response_object[:data] = build_resource(context.request, context.instance, fields: context.fields)
40
- response_headers['Location'] = build_url(context.request, context.instance)
60
+ builder = context.respond_to?(:builder) ? context.builder : nil
61
+ context.response_object[:data] = build_resource(context, context.instance, fields: context.fields, &builder)
62
+ response_headers['Location'] = build_url(context, context.instance)
41
63
  context.response_object.to_json
42
64
  end
43
65
  end
44
66
  end
45
67
 
46
- def read(**options, &block)
68
+ def read(content_type: nil, only_associated: false, callbacks: true, &block)
69
+ options = {
70
+ content_type: content_type,
71
+ only_associated: only_associated,
72
+ callbacks: callbacks,
73
+ cacheable: true
74
+ }
47
75
  define_action(:read, 'GET', '/:id', **options, &block).tap do |action|
48
76
  action.response status: 200 do |context|
49
- context.response_object[:data] = build_resource(context.request, context.instance, fields: context.fields)
77
+ builder = context.respond_to?(:builder) ? context.builder : nil
78
+ context.response_object[:data] = build_resource(context, context.instance, fields: context.fields, &builder)
50
79
  context.response_object.to_json
51
80
  end
52
81
  end
53
82
  end
54
83
 
55
- def update(**options, &block)
84
+ def update(content_type: nil, only_associated: false, callbacks: true, &block)
85
+ options = {
86
+ content_type: content_type,
87
+ only_associated: only_associated,
88
+ callbacks: callbacks,
89
+ cacheable: false,
90
+ example_input: :resource
91
+ }
56
92
  define_action(:update, 'PATCH', '/:id', **options, &block).tap do |action|
57
93
  action.response status: 200 do |context|
58
- context.response_object[:data] = build_resource(context.request, context.instance, fields: context.fields)
94
+ builder = context.respond_to?(:builder) ? context.builder : nil
95
+ context.response_object[:data] = build_resource(context, context.instance, fields: context.fields, &builder)
59
96
  context.response_object.to_json
60
97
  end
61
98
  end
62
99
  end
63
100
 
64
- def delete(**options, &block)
101
+ def delete(content_type: nil, only_associated: false, callbacks: true, &block)
102
+ options = {
103
+ content_type: content_type,
104
+ only_associated: only_associated,
105
+ callbacks: callbacks,
106
+ cacheable: false
107
+ }
65
108
  define_action(:delete, 'DELETE', '/:id', **options, &block).tap do |action|
66
109
  action.response status: 204
67
110
  end
68
111
  end
69
112
 
70
113
  def process(request)
71
- path_actions = self.path_actions(request)
72
- if request.options? && path_actions.present?
73
- allow = [*path_actions.map(&:request_method), 'OPTIONS']
74
- requests = allow.each_with_object({}) do |method, h|
75
- h[method] = options_for_method(method)
76
- end
77
- Action.dummy do
78
- response_headers['Allow'] = allow.join(', ')
79
- end.response(status: 200, accept: 'application/vnd.api+json') do
80
- JSONAPIonify.new_object(
81
- meta: {
82
- type: self.class.type,
83
- requests: requests
84
- }
85
- ).to_json
86
- end.call(self, request)
87
- elsif (action = find_supported_action(request))
114
+ if (action = find_supported_action(request))
88
115
  action.call(self, request)
89
116
  elsif (rel = find_supported_relationship(request))
90
117
  relationship(rel.name).process(request)
@@ -93,24 +120,18 @@ module JSONAPIonify::Api
93
120
  end
94
121
  end
95
122
 
96
- def options_for_method(method)
97
- case method
98
- when 'GET'
99
- { attributes: attributes.select(&:read).map(&:options_json) }
100
- when 'POST', 'PUT', 'PATCH'
101
- { attributes: attributes.select(&:write).map(&:options_json) }
102
- else
103
- {}
123
+ def after(*action_names, &block)
124
+ return after_request &block if action_names.blank?
125
+ action_names.each do |action_name|
126
+ send("after_#{action_name}", &block)
104
127
  end
105
128
  end
106
129
 
107
- def before(action_name = nil, &block)
108
- if action_name == :index
109
- warn 'the `index` action will soon be deprecated, use `list` instead!'
110
- action_name = :list
130
+ def before(*action_names, &block)
131
+ return before_request &block if action_names.blank?
132
+ action_names.each do |action_name|
133
+ send("before_#{action_name}", &block)
111
134
  end
112
- return before_request &block if action_name == nil
113
- send("before_#{action_name}", &block)
114
135
  end
115
136
 
116
137
  def define_action(name, *args, **options, &block)
@@ -150,22 +171,27 @@ module JSONAPIonify::Api
150
171
 
151
172
  def find_supported_relationship(request)
152
173
  relationship_definitions.find do |rel|
153
- relationship(rel.name).path_actions(request).present?
174
+ relationship = self.relationship(rel.name)
175
+ relationship != self && relationship.path_actions(request).present?
154
176
  end
155
177
  end
156
178
 
157
179
  def remove_action(*names)
158
- if names.include? :index
159
- warn 'the `index` action will soon be deprecated, use `list` instead!'
160
- names << :list
161
- end
162
180
  action_definitions.delete_if do |action_definition|
163
181
  names.include? action_definition.name
164
182
  end
165
183
  end
166
184
 
185
+ def call_action(name, request, **context_overrides)
186
+ action(name).call(self, request, **context_overrides)
187
+ end
188
+
189
+ def action(name)
190
+ actions.find { |action| action.name == name }
191
+ end
192
+
167
193
  def actions
168
- return if action_definitions.blank?
194
+ return [] if action_definitions.blank?
169
195
  action_definitions.select do |action|
170
196
  action.only_associated == false ||
171
197
  (respond_to?(:rel) && action.only_associated == true)
@@ -9,19 +9,31 @@ module JSONAPIonify::Api
9
9
  delegate :id_attribute, :attributes, to: :class
10
10
 
11
11
  context(:fields, readonly: true) do |context|
12
- should_error = false
13
- fields = (context.request.params['fields'] || {}).each_with_object(self.class.api.fields) do |(type, fields), field_map|
12
+ should_error = false
13
+ input_fields = context.request.params['fields'] || {}
14
+ actionable_fields = self.class.fields_for_action(context.action_name, context)
15
+ input_fields.each_with_object(
16
+ actionable_fields
17
+ ) do |(type, fields), field_map|
14
18
  type_sym = type.to_sym
19
+ field_symbols = fields.to_s.split(',').map(&:to_sym)
15
20
  field_map[type_sym] =
16
- fields.to_s.split(',').map(&:to_sym).each_with_object([]) do |field, field_list|
17
- attribute = self.class.api.resource(type_sym).attributes.find do |attribute|
18
- attribute.read? && attribute.name == field
21
+ field_symbols.each_with_object([]) do |field, field_list|
22
+ type_attributes = self.class.api.resource(type_sym).attributes
23
+ attribute = type_attributes.find do |attribute|
24
+ attribute.name == field &&
25
+ attribute.supports_read_for_action?(context.action_name, context)
26
+ end
27
+ if attribute
28
+ field_list << attribute.name
29
+ else
30
+ error(:field_not_permitted, type, field)
31
+ should_error = true
19
32
  end
20
- attribute ? field_list << attribute.name : error(:field_not_permitted, type, field) && (should_error = true)
21
33
  end
34
+ end.tap do
35
+ halt if should_error
22
36
  end
23
- raise Errors::RequestError if should_error
24
- fields
25
37
  end
26
38
  end
27
39
  end
@@ -32,20 +44,44 @@ module JSONAPIonify::Api
32
44
  end
33
45
  end
34
46
 
35
- def attribute(name, type, description = '', **options)
36
- Attribute.new(name, type, description, **options).tap do |new_attribute|
47
+ def attribute(name, type, description = '', **options, &block)
48
+ Attribute.new(
49
+ name, type, description, **options, &block
50
+ ).tap do |new_attribute|
37
51
  attributes.delete(new_attribute)
38
52
  attributes << new_attribute
39
53
  end
40
54
  end
41
55
 
56
+ def remove_attribute(name)
57
+ attributes.delete_if { |attr| attr.name == name.to_sym }
58
+ end
59
+
42
60
  def fields
43
61
  attributes.select(&:read?).map(&:name)
44
62
  end
45
63
 
64
+ def builder(&block)
65
+ context :builder, readonly: true, persisted: true do |context|
66
+ proc do |resource, instance|
67
+ block.call resource, instance, context
68
+ end
69
+ end
70
+ end
71
+
46
72
  def field_valid?(name)
47
73
  fields.include? name.to_sym
48
74
  end
49
75
 
76
+ def fields_for_action(action, context)
77
+ api.fields.each_with_object({}) do |(type, attrs), fields|
78
+ fields[type] = attrs.select do |attr|
79
+ api.resource(type).attributes.find do |type_attr|
80
+ type_attr.name == attr
81
+ end.supports_read_for_action? action, context
82
+ end
83
+ end
84
+ end
85
+
50
86
  end
51
87
  end
@@ -8,8 +8,12 @@ module JSONAPIonify::Api
8
8
  end
9
9
  end
10
10
 
11
- def context(name, readonly: false, &block)
12
- self.context_definitions[name.to_sym] = Context.new(block, readonly)
11
+ def context(name, **opts, &block)
12
+ self.context_definitions[name.to_sym] = Context.new(
13
+ **opts,
14
+ existing_context: self.context_definitions[name.to_sym],
15
+ &block
16
+ )
13
17
  end
14
18
 
15
19
  end
@@ -6,7 +6,7 @@ module JSONAPIonify::Api
6
6
  end
7
7
 
8
8
  def authentication(&block)
9
- context :authentication, readonly: true do |context|
9
+ context :authentication, readonly: true, persisted: true do |context|
10
10
  OpenStruct.new.tap do |authentication_object|
11
11
  if instance_exec(context.request, authentication_object, &block) == false
12
12
  error_now :forbidden
@@ -12,7 +12,7 @@ module JSONAPIonify::Api
12
12
  define_method method do |**options|
13
13
  @links[method] = URI.parse(@url).tap do |uri|
14
14
  page_params = { page: options }.deep_stringify_keys
15
- uri.query = @params.deep_merge(page_params).to_param
15
+ uri.query = @params.merge(page_params).to_param
16
16
  end.to_s
17
17
  end
18
18
  end
@@ -35,22 +35,14 @@ module JSONAPIonify::Api
35
35
  slice =
36
36
  if (params['before'] && params['first']) || (params['after'] && params['last'])
37
37
  error :forbidden do
38
- message 'Illegal combination of parameters'
38
+ detail 'Illegal combination of parameters'
39
39
  end
40
40
  elsif (after = params['after'])
41
41
  key_values = parse_and_validate_cursor(:after, after, context)
42
- array_select_past_cursor(
43
- collection,
44
- context.sort_params,
45
- key_values
46
- ).first(size)
42
+ array_select_past_cursor(collection, context.sort_params, key_values).first(size)
47
43
  elsif (before = params['before'])
48
44
  key_values = parse_and_validate_cursor(:before, before, context)
49
- array_select_past_cursor(
50
- collection,
51
- context.sort_params.reverse,
52
- key_values
53
- ).last(size)
45
+ array_select_past_cursor(collection, context.sort_params.invert, key_values).last(size)
54
46
  elsif params['last']
55
47
  collection.last(size)
56
48
  else
@@ -59,8 +51,8 @@ module JSONAPIonify::Api
59
51
 
60
52
  links.first first: size
61
53
  links.last last: size
62
- links.prev before: build_cursor_from_instance(context.request, slice.first), last: size unless slice.first == collection.first
63
- links.next after: build_cursor_from_instance(context.request, slice.last), first: size unless slice.last == collection.last
54
+ links.prev before: build_cursor_from_instance(context.request, slice.first), last: size unless !slice.first || slice.first == collection.first
55
+ links.next after: build_cursor_from_instance(context.request, slice.last), first: size unless !slice.last || slice.last == collection.last
64
56
 
65
57
  slice
66
58
  end
@@ -71,22 +63,15 @@ module JSONAPIonify::Api
71
63
  slice =
72
64
  if (params['before'] && params['first']) || (params['after'] && params['last'])
73
65
  error :forbidden do
74
- message 'Illegal combination of parameters'
66
+ detail 'Illegal combination of parameters'
75
67
  end
76
68
  elsif (after = params['after'])
77
69
  key_values = parse_and_validate_cursor(:after, after, context)
78
- arel_select_past_cursor(
79
- collection,
80
- context.sort_params,
81
- key_values
82
- ).limit(size)
70
+ arel_select_past_cursor(collection, context.sort_params, key_values).limit(size)
83
71
  elsif (before = params['before'])
84
72
  key_values = parse_and_validate_cursor(:before, before, context)
85
- ids = arel_select_past_cursor(
86
- collection,
87
- context.sort_params.reverse,
88
- key_values
89
- ).reverse_order.limit(size).pluck(:id)
73
+ ids = arel_select_past_cursor(collection, context.sort_params.invert, key_values)
74
+ .reverse_order.limit(size).pluck(id_attribute)
90
75
  collection.where(id_attribute => ids)
91
76
  elsif params['last']
92
77
  ids = collection.reverse_order.limit(size).pluck(id_attribute)
@@ -97,8 +82,8 @@ module JSONAPIonify::Api
97
82
 
98
83
  links.first first: size
99
84
  links.last last: size
100
- links.prev before: build_cursor_from_instance(context.request, slice.first), last: size unless slice.first == collection.first
101
- links.next after: build_cursor_from_instance(context.request, slice.last), first: size unless slice.last == collection.last
85
+ links.prev before: build_cursor_from_instance(context.request, slice.first), last: size unless !slice.first || slice.first == collection.first
86
+ links.next after: build_cursor_from_instance(context.request, slice.last), first: size unless !slice.last || slice.last == collection.last
102
87
 
103
88
  slice
104
89
  end
@@ -114,26 +99,30 @@ module JSONAPIonify::Api
114
99
  param :page, :before, actions: %i{list}
115
100
  param :page, :first, actions: %i{list}
116
101
  param :page, :last, actions: %i{list}
117
- context :paginated_collection do |context|
118
- collection = context.sorted_collection
119
- _, block = pagination_strategies.to_a.reverse.to_h.find do |mod, _|
120
- Object.const_defined?(mod, false) && context.collection.class <= Object.const_get(mod, false)
121
- end
102
+ context :paginated_collection, readonly: true do |context|
103
+ if context.root_request?
104
+ collection = context.sorted_collection
105
+ _, block = pagination_strategies.to_a.reverse.to_h.find do |mod, _|
106
+ Object.const_defined?(mod, false) && context.collection.class <= Object.const_get(mod, false)
107
+ end
122
108
 
123
- links_delegate = PaginationLinksDelegate.new(
124
- context.request.url,
125
- self.class.sticky_params(context.params),
126
- context.links
127
- )
128
-
129
- instance_exec(
130
- collection,
131
- context.request.params['page'] || {},
132
- links_delegate,
133
- per,
134
- context,
135
- &block
136
- )
109
+ links_delegate = PaginationLinksDelegate.new(
110
+ context.request.url,
111
+ context.params,
112
+ context.links
113
+ )
114
+
115
+ instance_exec(
116
+ collection,
117
+ context.request.params['page'] || {},
118
+ links_delegate,
119
+ per,
120
+ context,
121
+ &block
122
+ )
123
+ else
124
+ context.collection
125
+ end
137
126
  end
138
127
  end
139
128
 
@@ -166,18 +155,20 @@ module JSONAPIonify::Api
166
155
  end
167
156
 
168
157
  def arel_select_past_cursor(collection, sort_params, key_values)
169
- subselect = sort_params.length.times.map do |i|
158
+ subquery = sort_params.length.times.map do |i|
170
159
  set = sort_params[0..i]
171
160
  *contains_fields, outside_field = set
172
161
  contains_fields.reduce(collection.reorder(nil)) do |relation, field|
173
- relation.where <<-SQL.strip, value: key_values[field.name.to_s]
174
- "#{field.name}" #{field.contains_operator} :value
175
- SQL
176
- end.where(<<-SQL.strip, key_values[outside_field.name.to_s]).to_sql
177
- "#{outside_field.name}" #{outside_field.outside_operator} ?
178
- SQL
179
- end.join(' UNION ')
180
- collection.from("(#{subselect}) AS #{collection.table_name}")
162
+ relation.where(
163
+ collection.arel_table[field.name].send(field.contains_arel, key_values[field.name.to_s])
164
+ )
165
+ end.where(
166
+ collection.arel_table[outside_field.name].send(outside_field.outside_arel, key_values[outside_field.name.to_s])
167
+ )
168
+ end.map { |rel| "( #{rel.to_sql} )" }.join(' UNION ')
169
+ collection.from("(#{subquery}) AS #{collection.table_name}").tap(&:first)
170
+ rescue ActiveRecord::StatementInvalid
171
+ collection.where(id: collection.find_by_sql(subquery).map(&:id))
181
172
  end
182
173
 
183
174
  def parse_and_validate_cursor(param, cursor, context)
@@ -199,7 +190,7 @@ module JSONAPIonify::Api
199
190
  detail 'The cursor sort does not match the request sort'
200
191
  end
201
192
  end
202
- raise Errors::RequestError if should_error
193
+ halt if should_error
203
194
 
204
195
  options['a']
205
196
  end
@@ -6,7 +6,7 @@ module JSONAPIonify::Api
6
6
  extend JSONAPIonify::InheritedAttributes
7
7
  inherited_hash_attribute :param_definitions
8
8
 
9
- context(:params, readonly: true) do |context|
9
+ context(:params, readonly: true, persisted: true) do |context|
10
10
  should_error = false
11
11
 
12
12
  params = self.class.param_definitions.select do |_, v|
@@ -27,18 +27,23 @@ module JSONAPIonify::Api
27
27
  reserved = ParamOptions.reserved?(k)
28
28
  allowed = params.keys.include? keypath
29
29
  valid = ParamOptions.valid?(k) || v.is_a?(Hash)
30
- unless reserved || (allowed && valid)
30
+ unless reserved || (allowed && valid) || !context.root_request?
31
31
  should_error = true
32
32
  error :parameter_invalid, ParamOptions.keypath_to_string(*keypath)
33
33
  end
34
- end
34
+ end unless context.request.options?
35
35
 
36
36
  # Check for requirement
37
- if (missing_params = ParamOptions.missing_parameters(context.request.params, required_params.values.map(&:keypath))).present?
37
+ missing_params =
38
+ ParamOptions.missing_parameters(
39
+ context.request.params,
40
+ required_params.values.map(&:keypath)
41
+ )
42
+ if context.root_request? && missing_params.present?
38
43
  error :parameters_missing, missing_params
39
44
  end
40
45
 
41
- raise Errors::RequestError if should_error
46
+ halt if should_error
42
47
 
43
48
  # Return the params
44
49
  context.request.params
@@ -58,22 +63,13 @@ module JSONAPIonify::Api
58
63
  definition.keypath == keypath
59
64
  end
60
65
  next {} unless definition
61
- value = definition.extract_value(params)
66
+ value = definition.extract_value(params)
62
67
  if definition.default_value?(value)
63
68
  {}
64
69
  else
65
70
  definition.with_value(value)
66
71
  end
67
72
  end.reduce(:deep_merge)
68
-
69
- # sticky_param_definitions = param_definitions.values.select(&:sticky)
70
- # params.each_with_object do |k, v|
71
- # definition = sticky_param_definitions.find do |definition|
72
- # definition.keypath == ParamOptions.hash_to_keypaths(k => v)[0]
73
- # end
74
- # binding.pry
75
- # definition && !definition.default_value?(v)
76
- # end
77
73
  end
78
74
 
79
75
  end
@@ -8,18 +8,54 @@ module JSONAPIonify::Api
8
8
  end
9
9
  end
10
10
 
11
- def relates_to_many(name, resource: nil, &block)
12
- define_relationship(name, Relationship::Many, resource: resource, &block)
11
+ def relates_to_many(name, count_attribute: false, include_count: true, **opts, &block)
12
+ define_relationship(name, Relationship::Many, **opts, &block).tap do
13
+ define_relationship_counter(
14
+ name,
15
+ count_attribute === true ? "#{name.to_s.singularize}_count" : count_attribute.to_s,
16
+ include: include_count
17
+ ) if count_attribute
18
+ end
19
+ end
20
+
21
+ def relates_to_one(name, **opts, &block)
22
+ opts[:resource] ||= name.to_s.pluralize.to_sym
23
+ define_relationship(name, Relationship::One, **opts, &block)
13
24
  end
14
25
 
15
- def relates_to_one(name, resource: nil, &block)
16
- define_relationship(name, Relationship::One, resource: resource, &block)
26
+ def define_relationship_counter(rel_name, name, include: true)
27
+ before :response do |context|
28
+ if (context.scope.is_a?(ActiveRecord::Relation) || context.scope.is_a?(ActiveRecord::Base)) && context.scope._reflect_on_association(rel_name)
29
+ context.scope = context.scope.includes(rel_name)
30
+ end if context.fields[type&.to_sym].include? name.to_sym
31
+ end if include
32
+ attribute name.to_sym, types.Integer, "The number of #{rel_name}.", write: false do |_, instance, context|
33
+ rel = context.resource.class.relationship(rel_name)
34
+ blank_fields = context.fields.map { |k, _| [k, {}] }.to_h
35
+ rel_context = rel.new(
36
+ request: context.request,
37
+ context_overrides: {
38
+ owner: instance,
39
+ fields: blank_fields,
40
+ params: {}
41
+ }
42
+ ).exec { |c| c }
43
+ count = rel_context.collection.uniq.count
44
+ case count
45
+ when Hash
46
+ count.values.reduce(:+)
47
+ when Fixnum
48
+ count
49
+ else
50
+ error :internal_server_error
51
+ end
52
+ end
17
53
  end
18
54
 
19
- def define_relationship(name, klass, resource: nil, &block)
55
+ def define_relationship(name, klass, **opts, &block)
20
56
  const_name = name.to_s.camelcase + 'Relationship'
21
57
  remove_const(const_name) if const_defined? const_name
22
- klass.new(self, name, resource: resource, &block).tap do |new_relationship|
58
+ klass.new(self, name, **opts, &block).tap do |new_relationship|
23
59
  relationship_definitions.delete new_relationship
24
60
  relationship_definitions << new_relationship
25
61
  end
@@ -34,7 +70,7 @@ module JSONAPIonify::Api
34
70
  const_name = name.to_s.camelcase + 'Relationship'
35
71
  return const_get(const_name, false) if const_defined? const_name
36
72
  relationship_definition = relationship_definitions.find { |rel| rel.name == name }
37
- raise Errors::RelationshipNotDefined, "Relationship not defined: #{name}" unless relationship_definition
73
+ raise Errors::RelationshipNotFound, "Relationship not found: #{name}" unless relationship_definition
38
74
  const_set const_name, relationship_definition.resource_class
39
75
  end
40
76
 
@@ -6,7 +6,7 @@ module JSONAPIonify::Api
6
6
  extend JSONAPIonify::InheritedAttributes
7
7
  inherited_hash_attribute :request_header_definitions
8
8
 
9
- context(:request_headers) do |context|
9
+ context(:request_headers, persisted: true, readonly: true) do |context|
10
10
  should_error = false
11
11
 
12
12
  # Check for validity
@@ -17,12 +17,15 @@ module JSONAPIonify::Api
17
17
  v.required
18
18
  end
19
19
 
20
- if (missing_keys = required_headers.keys.map(&:downcase) - context.request.headers.keys.map(&:downcase)).present?
20
+ missing_keys =
21
+ required_headers.keys.map(&:downcase) -
22
+ context.request.headers.keys.map(&:downcase)
23
+ if context.root_request? && missing_keys.present?
21
24
  should_error = true
22
25
  error :headers_missing, missing_keys
23
26
  end
24
27
 
25
- raise Errors::RequestError if should_error
28
+ halt if should_error
26
29
 
27
30
  context.request.headers
28
31
  end
@@ -6,7 +6,7 @@ module JSONAPIonify::Api
6
6
  extend JSONAPIonify::InheritedAttributes
7
7
  inherited_hash_attribute :response_header_definitions
8
8
 
9
- context(:response_headers) do |context|
9
+ context(:response_headers, persisted: true) do |context|
10
10
  self.class.response_header_definitions.each_with_object({}) do |(name, block), headers|
11
11
  headers[name.to_s] = instance_exec(context, &block)
12
12
  end