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.
- checksums.yaml +13 -5
- data/.rubocop.yml +1 -0
- data/.ruby-version +1 -1
- data/.travis.yml +8 -0
- data/README.md +85 -3
- data/Rakefile +14 -0
- data/jsonapionify.gemspec +3 -0
- data/lib/jsonapionify/api/action.rb +84 -121
- data/lib/jsonapionify/api/attribute.rb +97 -20
- data/lib/jsonapionify/api/base/class_methods.rb +5 -4
- data/lib/jsonapionify/api/base/delegation.rb +20 -4
- data/lib/jsonapionify/api/base/doc_helper.rb +3 -3
- data/lib/jsonapionify/api/base/reloader.rb +1 -1
- data/lib/jsonapionify/api/base/resource_definitions.rb +28 -15
- data/lib/jsonapionify/api/base.rb +6 -0
- data/lib/jsonapionify/api/context.rb +18 -5
- data/lib/jsonapionify/api/context_delegate.rb +24 -7
- data/lib/jsonapionify/api/errors.rb +2 -0
- data/lib/jsonapionify/api/errors_object.rb +6 -5
- data/lib/jsonapionify/api/relationship/blocks.rb +1 -1
- data/lib/jsonapionify/api/relationship/many.rb +35 -11
- data/lib/jsonapionify/api/relationship/one.rb +17 -7
- data/lib/jsonapionify/api/relationship.rb +20 -6
- data/lib/jsonapionify/api/resource/builders.rb +81 -30
- data/lib/jsonapionify/api/resource/caching.rb +28 -0
- data/lib/jsonapionify/api/resource/caller.rb +61 -0
- data/lib/jsonapionify/api/resource/class_methods.rb +6 -2
- data/lib/jsonapionify/api/resource/defaults/actions.rb +47 -0
- data/lib/jsonapionify/api/resource/defaults/errors.rb +61 -15
- data/lib/jsonapionify/api/resource/defaults/hooks.rb +68 -0
- data/lib/jsonapionify/api/resource/defaults/options.rb +16 -28
- data/lib/jsonapionify/api/resource/defaults/params.rb +3 -0
- data/lib/jsonapionify/api/resource/defaults/request_contexts.rb +80 -32
- data/lib/jsonapionify/api/resource/defaults/response_contexts.rb +13 -6
- data/lib/jsonapionify/api/resource/defaults.rb +1 -1
- data/lib/jsonapionify/api/resource/definitions/actions.rb +81 -55
- data/lib/jsonapionify/api/resource/definitions/attributes.rb +46 -10
- data/lib/jsonapionify/api/resource/definitions/contexts.rb +6 -2
- data/lib/jsonapionify/api/resource/definitions/helpers.rb +1 -1
- data/lib/jsonapionify/api/resource/definitions/pagination.rb +47 -56
- data/lib/jsonapionify/api/resource/definitions/params.rb +11 -15
- data/lib/jsonapionify/api/resource/definitions/relationships.rb +43 -7
- data/lib/jsonapionify/api/resource/definitions/request_headers.rb +6 -3
- data/lib/jsonapionify/api/resource/definitions/response_headers.rb +1 -1
- data/lib/jsonapionify/api/resource/definitions/scopes.rb +5 -5
- data/lib/jsonapionify/api/resource/definitions/sorting.rb +12 -11
- data/lib/jsonapionify/api/resource/definitions.rb +1 -1
- data/lib/jsonapionify/api/resource/error_handling.rb +92 -20
- data/lib/jsonapionify/api/resource/exec.rb +11 -0
- data/lib/jsonapionify/api/resource/includer.rb +89 -1
- data/lib/jsonapionify/api/resource.rb +55 -8
- data/lib/jsonapionify/api/response.rb +43 -14
- data/lib/jsonapionify/api/server/media_type.rb +36 -0
- data/lib/jsonapionify/api/server/request.rb +25 -11
- data/lib/jsonapionify/api/server.rb +8 -4
- data/lib/jsonapionify/api/sort_field.rb +18 -0
- data/lib/jsonapionify/api/sort_field_set.rb +1 -1
- data/lib/jsonapionify/api/test_helper.rb +46 -0
- data/lib/jsonapionify/documentation/template.erb +2 -2
- data/lib/jsonapionify/documentation.rb +10 -0
- data/lib/jsonapionify/structure/collections/base.rb +10 -3
- data/lib/jsonapionify/structure/helpers/object_defaults.rb +5 -10
- data/lib/jsonapionify/structure/maps/relationships.rb +4 -0
- data/lib/jsonapionify/structure/objects/attributes.rb +4 -0
- data/lib/jsonapionify/structure/objects/base.rb +22 -9
- data/lib/jsonapionify/structure/objects/error.rb +2 -0
- data/lib/jsonapionify/structure/objects/jsonapi.rb +1 -0
- data/lib/jsonapionify/structure/objects/link.rb +1 -0
- data/lib/jsonapionify/structure/objects/relationship.rb +2 -0
- data/lib/jsonapionify/structure/objects/resource.rb +2 -0
- data/lib/jsonapionify/structure/objects/resource_identifier.rb +12 -4
- data/lib/jsonapionify/structure/objects/top_level.rb +4 -2
- data/lib/jsonapionify/types/array_type.rb +16 -11
- data/lib/jsonapionify/types/boolean_type.rb +9 -4
- data/lib/jsonapionify/types/date_string_type.rb +7 -10
- data/lib/jsonapionify/types/float_type.rb +13 -0
- data/lib/jsonapionify/types/integer_type.rb +12 -0
- data/lib/jsonapionify/types/object_type.rb +7 -2
- data/lib/jsonapionify/types/string_type.rb +12 -0
- data/lib/jsonapionify/types/time_string_type.rb +8 -10
- data/lib/jsonapionify/types.rb +43 -5
- data/lib/jsonapionify/version.rb +1 -1
- data/lib/jsonapionify.rb +36 -1
- metadata +121 -74
|
@@ -52,7 +52,11 @@ module JSONAPIonify::Api
|
|
|
52
52
|
|
|
53
53
|
def documented_actions_in_order
|
|
54
54
|
indexes = %i{list create read update delete add replace remove}
|
|
55
|
-
documented_actions.
|
|
55
|
+
documented_actions.reject do |a, *|
|
|
56
|
+
['HEAD', 'OPTIONS'].include? a.request_method
|
|
57
|
+
end.sort_by do |action, *|
|
|
58
|
+
indexes.index(action.name) || indexes.length
|
|
59
|
+
end
|
|
56
60
|
end
|
|
57
61
|
|
|
58
62
|
def documentation_object(base_url)
|
|
@@ -60,7 +64,7 @@ module JSONAPIonify::Api
|
|
|
60
64
|
name: type,
|
|
61
65
|
description: JSONAPIonify::Documentation.render_markdown(@description || ''),
|
|
62
66
|
relationships: relationships.map { |r| r.documentation_object },
|
|
63
|
-
attributes: attributes.map(&:documentation_object),
|
|
67
|
+
attributes: attributes.sort_by(&:name).map(&:documentation_object),
|
|
64
68
|
actions: documented_actions_in_order.map do |action, base, args|
|
|
65
69
|
action.documentation_object File.join(base_url, base), *args
|
|
66
70
|
end
|
|
@@ -3,6 +3,53 @@ module JSONAPIonify::Api
|
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
5
|
included do
|
|
6
|
+
context :path_actions, readonly: true, persisted: true do |context|
|
|
7
|
+
self.class.path_actions(context.request)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
context :http_allow, readonly: true, persisted: true do |context|
|
|
11
|
+
context.path_actions.map(&:request_method)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
context(:action_name, persisted: true) {}
|
|
15
|
+
define_action(:options, 'OPTIONS', '*', cacheable: true, callbacks: false) do
|
|
16
|
+
cache 'options-request'
|
|
17
|
+
end.response(status: 200) do |context|
|
|
18
|
+
response_headers['Allow'] = context.http_allow.join(', ')
|
|
19
|
+
requests = context.path_actions.each_with_object({}) do |action, h|
|
|
20
|
+
request_attributes_json = attributes.select do |attr|
|
|
21
|
+
attr.supports_write_for_action? action.name, context
|
|
22
|
+
end.map do |attr|
|
|
23
|
+
attr.options_json_for_action(action.name, context)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
response_attributes_json = attributes.select do |attr|
|
|
27
|
+
attr.supports_read_for_action? action.name, context
|
|
28
|
+
end.map do |attr|
|
|
29
|
+
attr.options_json_for_action(action.name, context)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
h['path'] = context.request.path
|
|
33
|
+
h['url'] = context.request.url
|
|
34
|
+
action_options = h[action.request_method] = {}
|
|
35
|
+
|
|
36
|
+
if ['GET', 'POST', 'PUT', 'PATCH'].include? action.request_method
|
|
37
|
+
action_options[:response_attributes] = response_attributes_json
|
|
38
|
+
action_options[:relationships] = self.class.relationships.map(&:options_json)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if ['POST', 'PUT', 'PATCH'].include? action.request_method
|
|
42
|
+
action_options[:request_attributes] = request_attributes_json
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
JSONAPIonify.new_object(
|
|
46
|
+
meta: {
|
|
47
|
+
type: self.class.type,
|
|
48
|
+
requests: requests
|
|
49
|
+
}
|
|
50
|
+
).to_json
|
|
51
|
+
end
|
|
52
|
+
|
|
6
53
|
before(:create) { |context| context.instance = context.new_instance }
|
|
7
54
|
read
|
|
8
55
|
end
|
|
@@ -5,8 +5,11 @@ module JSONAPIonify::Api
|
|
|
5
5
|
extend ActiveSupport::Concern
|
|
6
6
|
|
|
7
7
|
included do
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
register_exception 'ActiveRecord::RecordInvalid', error: :invalid_record
|
|
9
|
+
register_exception 'ActiveRecord::RecordNotUnique', error: :duplicate_record
|
|
10
|
+
register_exception 'ActiveRecord::RecordNotFound', error: :not_found
|
|
11
|
+
register_exception JSONAPIonify::Structure::ValidationError, error: :jsonapi_validation_error
|
|
12
|
+
register_exception Oj::ParseError, error: :json_parse_error
|
|
10
13
|
|
|
11
14
|
Rack::Utils::SYMBOL_TO_STATUS_CODE.reject { |_, v| v < 400 }.each do |symbol, code|
|
|
12
15
|
message = Rack::Utils::HTTP_STATUS_CODES[code]
|
|
@@ -18,74 +21,100 @@ module JSONAPIonify::Api
|
|
|
18
21
|
|
|
19
22
|
error :data_missing do
|
|
20
23
|
pointer ''
|
|
21
|
-
title '
|
|
24
|
+
title 'missing Member'
|
|
22
25
|
detail 'missing data member'
|
|
23
26
|
status '422'
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
error :json_parse_error do
|
|
27
|
-
title '
|
|
28
|
-
detail '
|
|
30
|
+
title 'parse Error'
|
|
31
|
+
detail 'could not parse json object'
|
|
29
32
|
status '422'
|
|
30
33
|
end
|
|
31
34
|
|
|
32
35
|
error :field_not_permitted do |type, field|
|
|
33
36
|
parameter "fields[#{type}]"
|
|
34
|
-
title '
|
|
37
|
+
title 'invalid field'
|
|
35
38
|
detail "type: `#{type}`, does not have field: `#{field}`"
|
|
36
39
|
status '400'
|
|
37
40
|
end
|
|
38
41
|
|
|
42
|
+
error :attribute_type_error do |attribute|
|
|
43
|
+
pointer "data/attributes/#{attribute}"
|
|
44
|
+
status '500'
|
|
45
|
+
title "attribute type error"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
error :attribute_required do |attribute|
|
|
49
|
+
pointer "data/attributes/#{attribute}"
|
|
50
|
+
title 'attribute required'
|
|
51
|
+
detail "attribute required: #{attribute}"
|
|
52
|
+
status '422'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
error :attribute_cannot_be_null do |attribute|
|
|
56
|
+
pointer "data/attributes/#{attribute}"
|
|
57
|
+
title "attribute cannot be null: #{attribute}"
|
|
58
|
+
status '500'
|
|
59
|
+
end
|
|
60
|
+
|
|
39
61
|
error :attribute_not_permitted do |attribute|
|
|
40
62
|
pointer "data/attributes/#{attribute}"
|
|
41
|
-
title '
|
|
42
|
-
|
|
63
|
+
title 'attribute not permitted'
|
|
64
|
+
status '422'
|
|
65
|
+
detail "attribute not permitted: #{attribute}"
|
|
43
66
|
end
|
|
44
67
|
|
|
45
68
|
error :attributes_missing do
|
|
46
69
|
pointer 'data'
|
|
47
|
-
title '
|
|
70
|
+
title 'missing Member'
|
|
48
71
|
detail 'missing attributes member'
|
|
49
72
|
status '422'
|
|
50
73
|
end
|
|
51
74
|
|
|
52
75
|
error :include_parameter_invalid do
|
|
53
76
|
parameter 'sort'
|
|
54
|
-
title '
|
|
77
|
+
title 'include parameter is invalid'
|
|
55
78
|
status '400'
|
|
56
79
|
end
|
|
57
80
|
|
|
58
81
|
error :parameters_missing do |parameters|
|
|
59
|
-
title '
|
|
82
|
+
title 'missing required parameters'
|
|
60
83
|
detail "missing: #{parameters.to_sentence}"
|
|
61
84
|
status '400'
|
|
62
85
|
end
|
|
63
86
|
|
|
64
87
|
error :parameter_invalid do |param|
|
|
65
88
|
parameter param
|
|
66
|
-
title '
|
|
89
|
+
title 'parameter Invalid'
|
|
67
90
|
detail "parameter invalid: #{param}"
|
|
68
91
|
status '400'
|
|
69
92
|
end
|
|
70
93
|
|
|
71
94
|
error :headers_missing do |headers|
|
|
72
|
-
title '
|
|
95
|
+
title 'missing required headers'
|
|
73
96
|
detail "missing: #{headers.to_sentence}"
|
|
74
97
|
status '400'
|
|
75
98
|
end
|
|
76
99
|
|
|
77
100
|
error :sort_parameter_invalid do
|
|
78
101
|
parameter 'sort'
|
|
79
|
-
title '
|
|
102
|
+
title 'sort parameter is invalid'
|
|
80
103
|
status '400'
|
|
81
104
|
end
|
|
82
105
|
|
|
83
106
|
error :page_parameter_invalid do |*paths|
|
|
84
107
|
parameter ParamOptions.keypath_to_string(*paths)
|
|
85
|
-
title '
|
|
108
|
+
title 'page parameter invalid'
|
|
86
109
|
status '400'
|
|
87
110
|
end
|
|
88
111
|
|
|
112
|
+
error :relationship_not_includable do |name|
|
|
113
|
+
parameter 'include'
|
|
114
|
+
title "relationship not includable: #{name}"
|
|
115
|
+
status '406'
|
|
116
|
+
end
|
|
117
|
+
|
|
89
118
|
error :request_object_invalid do |context, request_object|
|
|
90
119
|
context.errors.set request_object.errors.as_collection
|
|
91
120
|
end
|
|
@@ -94,6 +123,23 @@ module JSONAPIonify::Api
|
|
|
94
123
|
title 'Invalid Resource'
|
|
95
124
|
status '404'
|
|
96
125
|
end
|
|
126
|
+
|
|
127
|
+
error :duplicate_record do
|
|
128
|
+
title 'Duplicate Record'
|
|
129
|
+
status "409"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
error :invalid_attribute do |attr, message|
|
|
133
|
+
title attr.present? ? 'Invalid Attribute' : 'Invalid Record'
|
|
134
|
+
detail [attr, message].reject(&:empty?).join(' ')
|
|
135
|
+
pointer "data/attributes/#{attr}" if attr.present?
|
|
136
|
+
status "422"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
error :invalid_record do
|
|
140
|
+
title 'Invalid Record'
|
|
141
|
+
status "422"
|
|
142
|
+
end
|
|
97
143
|
end
|
|
98
144
|
end
|
|
99
145
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
module Resource::Defaults::Hooks
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
before :commit_create, :commit_update do |context|
|
|
7
|
+
# Assign the attributes
|
|
8
|
+
context.request_attributes.each do |key, value|
|
|
9
|
+
context.instance.send "#{key}=", value
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Assign the relationships
|
|
13
|
+
context.request_relationships.each do |key, value|
|
|
14
|
+
context.instance.send "#{key}=", value
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
after :commit_create, :commit_update do |context|
|
|
20
|
+
try_commit(context.instance)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
after :commit_delete do |context|
|
|
24
|
+
if defined?(ActiveRecord) && context.instance.is_a?(ActiveRecord::Base)
|
|
25
|
+
context.instance.destroy
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
before :commit_add do |context|
|
|
30
|
+
context.scope.concat context.request_instances
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
before :commit_remove do |context|
|
|
34
|
+
context.request_instances.each { |instance| context.scope.delete(instance) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
before :commit_replace do |context|
|
|
38
|
+
case self.class.rel
|
|
39
|
+
when Relationship::One
|
|
40
|
+
context.owner.send "#{self.class.rel.name}=", context.request_instance
|
|
41
|
+
try_commit(context.owner)
|
|
42
|
+
when Relationship::Many
|
|
43
|
+
instances_to_add = context.request_instances - context.scope
|
|
44
|
+
instances_to_delete = context.scope - context.request_instances
|
|
45
|
+
instances_to_delete.each { |instance| context.scope.delete(instance) }
|
|
46
|
+
context.scope.append instances_to_add
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def try_commit(instance)
|
|
52
|
+
if defined?(ActiveRecord) && instance.is_a?(ActiveRecord::Base)
|
|
53
|
+
commit_active_record(instance)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def commit_active_record(instance)
|
|
58
|
+
instance.save
|
|
59
|
+
if instance.errors.present?
|
|
60
|
+
instance.errors.messages.each do |attr, messages|
|
|
61
|
+
messages.each do |message|
|
|
62
|
+
error :invalid_attribute, attr, message
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -3,40 +3,28 @@ module JSONAPIonify::Api
|
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
included do
|
|
5
5
|
id :id
|
|
6
|
-
scope {
|
|
7
|
-
collection
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
begin
|
|
13
|
-
!!context.scope
|
|
14
|
-
rescue NotImplementedError
|
|
15
|
-
false
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
context :collection_defined? do |context|
|
|
20
|
-
begin
|
|
21
|
-
!!context.collection
|
|
22
|
-
rescue NotImplementedError
|
|
23
|
-
false
|
|
6
|
+
scope { self.type.classify.constantize }
|
|
7
|
+
collection do |scope|
|
|
8
|
+
if defined?(ActiveRecord) && scope < ActiveRecord::Base && scope.is_a?(Class)
|
|
9
|
+
scope.all
|
|
10
|
+
else
|
|
11
|
+
scope
|
|
24
12
|
end
|
|
25
13
|
end
|
|
26
14
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
15
|
+
instance do |scope, key|
|
|
16
|
+
if defined?(ActiveRecord) && scope < ActiveRecord::Base
|
|
17
|
+
scope.find_by! id_attribute => key
|
|
18
|
+
else
|
|
19
|
+
raise NotImplementedError, 'instance not implemented'
|
|
32
20
|
end
|
|
33
21
|
end
|
|
34
22
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
23
|
+
new_instance do |scope|
|
|
24
|
+
if defined?(ActiveRecord) && scope < ActiveRecord::Base
|
|
25
|
+
scope.new
|
|
26
|
+
else
|
|
27
|
+
raise NotImplementedError, 'scope not implemented'
|
|
40
28
|
end
|
|
41
29
|
end
|
|
42
30
|
|
|
@@ -3,74 +3,122 @@ module JSONAPIonify::Api
|
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
5
|
included do
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
context(:request_body, readonly: true, persisted: true) do |context|
|
|
7
8
|
context.request.body.read
|
|
8
9
|
end
|
|
9
10
|
|
|
10
|
-
context(:request_object, readonly: true) do |context|
|
|
11
|
+
context(:request_object, readonly: true, persisted: true) do |context|
|
|
11
12
|
JSONAPIonify.parse(context.request_body).as(:client).tap do |input|
|
|
12
13
|
error_now(:request_object_invalid, context, input) unless input.validate
|
|
13
14
|
end
|
|
14
15
|
end
|
|
15
16
|
|
|
16
|
-
context(:id, readonly: true) do |context|
|
|
17
|
+
context(:id, readonly: true, persisted: true) do |context|
|
|
17
18
|
context.request.env['jsonapionify.id']
|
|
18
19
|
end
|
|
19
20
|
|
|
20
|
-
context(:
|
|
21
|
-
|
|
21
|
+
context(:request_id, readonly: true, persisted: true) do |context|
|
|
22
|
+
context.request_data[:id]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
context(:request_attributes, readonly: true, persisted: true) do |context|
|
|
26
|
+
should_error = false
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
request_attributes =
|
|
29
|
+
context.request_data.fetch(:attributes) do
|
|
30
|
+
error_now :attributes_missing
|
|
31
|
+
end
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
# Check for required attributes
|
|
34
|
+
self.attributes.each do |attr|
|
|
35
|
+
next unless attr.required_for_action?(action_name, context)
|
|
36
|
+
if attr.read? || context.id
|
|
37
|
+
example_id = self.build_id(context.instance)
|
|
38
|
+
next unless attr.resolve(
|
|
39
|
+
context.instance, context, example_id: example_id
|
|
40
|
+
).nil?
|
|
41
|
+
end
|
|
42
|
+
unless request_attributes.has_key?(attr.name)
|
|
43
|
+
error :attribute_required, attr.name
|
|
44
|
+
should_error = true
|
|
45
|
+
end
|
|
29
46
|
end
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
47
|
+
|
|
48
|
+
request_attributes.each_with_object({}) do |(attr, v), attributes|
|
|
49
|
+
resource_attribute = self.attributes.find { |a| a.name == attr }
|
|
50
|
+
is_actionable = !!resource_attribute&.supports_write_for_action?(action_name, context)
|
|
51
|
+
unless is_actionable
|
|
52
|
+
error :attribute_not_permitted, attr
|
|
53
|
+
should_error = true
|
|
36
54
|
end
|
|
37
|
-
end.to_hash
|
|
38
|
-
end
|
|
39
55
|
|
|
40
|
-
context(:request_instances, readonly: true) do |context|
|
|
41
|
-
should_error = false
|
|
42
|
-
data = context.request_data
|
|
43
|
-
instances = data.map.each_with_index do |item, i|
|
|
44
56
|
begin
|
|
45
|
-
|
|
46
|
-
rescue
|
|
57
|
+
attributes[attr] = resource_attribute.type.load(v)
|
|
58
|
+
rescue JSONAPIonify::Types::LoadError
|
|
59
|
+
error :attribute_type_error, attr
|
|
47
60
|
should_error = true
|
|
48
61
|
end
|
|
62
|
+
end.tap do
|
|
63
|
+
halt if should_error
|
|
49
64
|
end
|
|
50
|
-
raise Errors::RequestError if should_error
|
|
51
|
-
instances
|
|
52
65
|
end
|
|
53
66
|
|
|
54
|
-
context(:
|
|
67
|
+
context(:request_relationships, readonly: true, persisted: true) do |context|
|
|
68
|
+
data = context.request_data
|
|
69
|
+
if data[:relationships]
|
|
70
|
+
data[:relationships].each_with_object({}) do |(name, rel), obj|
|
|
71
|
+
pointer = "data/relationships/#{name}/data"
|
|
72
|
+
case rel[:data]
|
|
73
|
+
when JSONAPIonify::Structure::Collections::Base
|
|
74
|
+
obj[name] = find_instances(rel[:data], pointer: pointer)
|
|
75
|
+
when JSONAPIonify::Structure::Objects::Base
|
|
76
|
+
obj[name] = find_instance(rel[:data], pointer: pointer)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
else
|
|
80
|
+
{}
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
context(:request_instances, readonly: true, persisted: true) do |context|
|
|
85
|
+
data = context.request_data
|
|
86
|
+
(data ? find_instances(data, pointer: '/data') : [])
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
context(:request_instance, readonly: true, persisted: true) do |context|
|
|
55
90
|
find_instance(context.request_data, pointer: 'data')
|
|
56
91
|
end
|
|
57
92
|
|
|
58
|
-
context(:request_resource, readonly: true) do |context|
|
|
93
|
+
context(:request_resource, readonly: true, persisted: true) do |context|
|
|
59
94
|
item = context.request_data
|
|
60
95
|
find_resource item, pointer: 'data'
|
|
61
96
|
end
|
|
62
97
|
|
|
63
|
-
context(:request_data) do |context|
|
|
98
|
+
context(:request_data, readonly: true, persisted: true) do |context|
|
|
64
99
|
context.request_object.fetch(:data) {
|
|
65
100
|
error_now(:data_missing)
|
|
66
101
|
}
|
|
67
102
|
end
|
|
68
103
|
|
|
69
|
-
context(:authentication, readonly: true) do
|
|
104
|
+
context(:authentication, readonly: true, persisted: true) do
|
|
70
105
|
OpenStruct.new
|
|
71
106
|
end
|
|
72
107
|
end
|
|
73
108
|
|
|
109
|
+
def find_instances(items, pointer:)
|
|
110
|
+
should_error = false
|
|
111
|
+
instances = items.map.each_with_index do |item, i|
|
|
112
|
+
begin
|
|
113
|
+
find_instance item, pointer: "#{pointer}/#{i}"
|
|
114
|
+
rescue Errors::RequestError
|
|
115
|
+
should_error = true
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
halt if should_error
|
|
119
|
+
instances
|
|
120
|
+
end
|
|
121
|
+
|
|
74
122
|
def find_instance(item, pointer:)
|
|
75
123
|
should_error = false
|
|
76
124
|
resource = find_resource(item, pointer: pointer)
|
|
@@ -81,7 +129,7 @@ module JSONAPIonify::Api
|
|
|
81
129
|
self.detail "could not find resource: `#{item[:type]}` with id: #{item[:id]}"
|
|
82
130
|
end
|
|
83
131
|
end
|
|
84
|
-
|
|
132
|
+
halt if should_error
|
|
85
133
|
instance
|
|
86
134
|
end
|
|
87
135
|
|
|
@@ -94,7 +142,7 @@ module JSONAPIonify::Api
|
|
|
94
142
|
self.detail "could not find resource: `#{item[:type]}`"
|
|
95
143
|
end
|
|
96
144
|
end
|
|
97
|
-
|
|
145
|
+
halt if should_error
|
|
98
146
|
resource
|
|
99
147
|
end
|
|
100
148
|
|
|
@@ -3,27 +3,34 @@ module JSONAPIonify::Api
|
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
5
|
included do
|
|
6
|
+
before(:response) { |context| context.clear }
|
|
7
|
+
|
|
8
|
+
context(:invalidate_cache?, readonly: true, persisted: true) { |c| c.includes.present? }
|
|
6
9
|
|
|
7
10
|
# Response Objects
|
|
8
|
-
context(:links, readonly: true) do |context|
|
|
11
|
+
context(:links, readonly: true, persisted: true) do |context|
|
|
9
12
|
context.response_object[:links]
|
|
10
13
|
end
|
|
11
14
|
|
|
12
|
-
context(:meta, readonly: true) do |context|
|
|
15
|
+
context(:meta, readonly: true, persisted: true) do |context|
|
|
13
16
|
JSONAPIonify::Structure::Helpers::MetaDelegate.new context.response_object
|
|
14
17
|
end
|
|
15
18
|
|
|
16
|
-
context(:response_object) do |context|
|
|
19
|
+
context(:response_object, readonly: true, persisted: true) do |context|
|
|
17
20
|
JSONAPIonify.parse(links: { self: context.request.url })
|
|
18
21
|
end
|
|
19
22
|
|
|
20
|
-
context(:response_collection) do |context|
|
|
21
|
-
|
|
23
|
+
context(:response_collection, readonly: true) do |context|
|
|
24
|
+
if context.root_request?
|
|
25
|
+
collections = %i{
|
|
22
26
|
paginated_collection
|
|
23
27
|
sorted_collection
|
|
24
28
|
collection
|
|
25
29
|
}
|
|
26
|
-
|
|
30
|
+
context.public_send collections.find { |c| context.respond_to? c }
|
|
31
|
+
else
|
|
32
|
+
context.collection
|
|
33
|
+
end
|
|
27
34
|
end
|
|
28
35
|
|
|
29
36
|
end
|