jsonapionify 0.0.1.pre → 0.9.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.
- checksums.yaml +4 -4
- data/.editorconfig +35 -0
- data/.ruby-version +1 -1
- data/.travis.yml +0 -2
- data/Guardfile +1 -1
- data/README.md +13 -8
- data/Rakefile +10 -0
- data/config.ru +3 -3
- data/index.html +1 -0
- data/jsonapionify.gemspec +13 -8
- data/lib/jsonapionify/api/action.rb +60 -50
- data/lib/jsonapionify/api/attribute.rb +13 -2
- data/lib/jsonapionify/api/base/app_builder.rb +17 -2
- data/lib/jsonapionify/api/base/class_methods.rb +33 -17
- data/lib/jsonapionify/api/base/delegation.rb +4 -1
- data/lib/jsonapionify/api/base/doc_helper.rb +13 -4
- data/lib/jsonapionify/api/base/resource_definitions.rb +13 -2
- data/lib/jsonapionify/api/base.rb +22 -6
- data/lib/jsonapionify/api/context_delegate.rb +2 -2
- data/lib/jsonapionify/api/errors.rb +7 -2
- data/lib/jsonapionify/api/errors_object.rb +1 -1
- data/lib/jsonapionify/api/header_options.rb +6 -5
- data/lib/jsonapionify/api/param_options.rb +49 -7
- data/lib/jsonapionify/api/relationship/many.rb +0 -5
- data/lib/jsonapionify/api/relationship/one.rb +10 -9
- data/lib/jsonapionify/api/relationship.rb +17 -5
- data/lib/jsonapionify/api/resource/builders.rb +39 -10
- data/lib/jsonapionify/api/resource/class_methods.rb +17 -6
- data/lib/jsonapionify/api/resource/defaults/actions.rb +0 -1
- data/lib/jsonapionify/api/resource/defaults/errors.rb +11 -11
- data/lib/jsonapionify/api/resource/defaults/options.rb +53 -0
- data/lib/jsonapionify/api/resource/defaults/params.rb +9 -0
- data/lib/jsonapionify/api/resource/defaults/request_contexts.rb +17 -11
- data/lib/jsonapionify/api/resource/definitions/actions.rb +51 -45
- data/lib/jsonapionify/api/resource/definitions/attributes.rb +2 -2
- data/lib/jsonapionify/api/resource/definitions/helpers.rb +18 -0
- data/lib/jsonapionify/api/resource/definitions/pagination.rb +183 -53
- data/lib/jsonapionify/api/resource/definitions/params.rb +43 -12
- data/lib/jsonapionify/api/resource/definitions/request_headers.rb +1 -67
- data/lib/jsonapionify/api/resource/definitions/scopes.rb +2 -13
- data/lib/jsonapionify/api/resource/definitions/sorting.rb +71 -58
- data/lib/jsonapionify/api/resource/error_handling.rb +2 -2
- data/lib/jsonapionify/api/resource/includer.rb +6 -0
- data/lib/jsonapionify/api/resource.rb +14 -3
- data/lib/jsonapionify/api/response.rb +2 -2
- data/lib/jsonapionify/api/server/mock_response.rb +2 -2
- data/lib/jsonapionify/api/server/request.rb +11 -7
- data/lib/jsonapionify/api/server.rb +1 -1
- data/lib/jsonapionify/api/sort_field.rb +59 -0
- data/lib/jsonapionify/api/sort_field_set.rb +36 -0
- data/lib/jsonapionify/callbacks.rb +3 -3
- data/lib/jsonapionify/continuation.rb +1 -0
- data/lib/jsonapionify/deep_sort_collection.rb +22 -0
- data/lib/jsonapionify/documentation/template.erb +196 -77
- data/lib/jsonapionify/documentation.rb +9 -9
- data/lib/jsonapionify/indented_string.rb +1 -0
- data/lib/jsonapionify/inherited_attributes.rb +4 -3
- data/lib/jsonapionify/structure/collections/base.rb +2 -1
- data/lib/jsonapionify/structure/helpers/errors.rb +1 -1
- data/lib/jsonapionify/structure/helpers/object_defaults.rb +2 -1
- data/lib/jsonapionify/structure/helpers/validations.rb +2 -1
- data/lib/jsonapionify/structure/objects/base.rb +4 -3
- data/lib/jsonapionify/structure/objects/top_level.rb +1 -1
- data/lib/jsonapionify/types/boolean_type.rb +2 -2
- data/lib/jsonapionify/types/date_string_type.rb +1 -1
- data/lib/jsonapionify/types/time_string_type.rb +1 -1
- data/lib/jsonapionify/version.rb +1 -1
- data/lib/jsonapionify.rb +16 -2
- metadata +69 -10
- data/fixtures/documentation.json +0 -364
- data/lib/jsonapionify/api/resource/http.rb +0 -11
- data/lib/jsonapionify/enumerable_observer.rb +0 -91
- data/lib/jsonapionify/unstrict_proc.rb +0 -28
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'possessive'
|
1
2
|
require 'active_support/core_ext/array/wrap'
|
2
3
|
|
3
4
|
module JSONAPIonify::Api
|
@@ -7,15 +8,10 @@ module JSONAPIonify::Api
|
|
7
8
|
def self.extended(klass)
|
8
9
|
klass.class_eval do
|
9
10
|
extend JSONAPIonify::InheritedAttributes
|
11
|
+
include JSONAPIonify::Callbacks
|
12
|
+
define_callbacks :request, :list, :create, :read, :update, :delete,
|
13
|
+
:show, :add, :remove, :replace, :exception
|
10
14
|
inherited_array_attribute :action_definitions
|
11
|
-
inherited_hash_attribute :callbacks
|
12
|
-
|
13
|
-
def self.inherited(subclass)
|
14
|
-
super
|
15
|
-
callbacks.each do |action_name, klass|
|
16
|
-
subclass.callbacks[action_name] = Class.new klass
|
17
|
-
end
|
18
|
-
end
|
19
15
|
end
|
20
16
|
end
|
21
17
|
|
@@ -27,7 +23,6 @@ module JSONAPIonify::Api
|
|
27
23
|
context.response_collection,
|
28
24
|
fields: context.fields
|
29
25
|
)
|
30
|
-
context.meta[:total_count] = context.collection.count
|
31
26
|
context.response_object.to_json
|
32
27
|
end
|
33
28
|
end
|
@@ -42,6 +37,7 @@ module JSONAPIonify::Api
|
|
42
37
|
define_action(:create, 'POST', **options, &block).tap do |action|
|
43
38
|
action.response status: 201 do |context|
|
44
39
|
context.response_object[:data] = build_resource(context.request, context.instance, fields: context.fields)
|
40
|
+
response_headers['Location'] = build_url(context.request, context.instance)
|
45
41
|
context.response_object.to_json
|
46
42
|
end
|
47
43
|
end
|
@@ -74,9 +70,19 @@ module JSONAPIonify::Api
|
|
74
70
|
def process(request)
|
75
71
|
path_actions = self.path_actions(request)
|
76
72
|
if request.options? && path_actions.present?
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
80
86
|
end.call(self, request)
|
81
87
|
elsif (action = find_supported_action(request))
|
82
88
|
action.call(self, request)
|
@@ -87,39 +93,28 @@ module JSONAPIonify::Api
|
|
87
93
|
end
|
88
94
|
end
|
89
95
|
|
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
|
+
{}
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
90
107
|
def before(action_name = nil, &block)
|
91
108
|
if action_name == :index
|
92
109
|
warn 'the `index` action will soon be deprecated, use `list` instead!'
|
93
110
|
action_name = :list
|
94
111
|
end
|
95
|
-
return
|
96
|
-
|
97
|
-
end
|
98
|
-
|
99
|
-
def base_callbacks
|
100
|
-
resource = self
|
101
|
-
callbacks['*'] ||= Class.new do
|
102
|
-
def self.context(*)
|
103
|
-
end
|
104
|
-
|
105
|
-
include Resource::ErrorHandling
|
106
|
-
|
107
|
-
define_singleton_method(:error_definitions) do
|
108
|
-
resource.error_definitions
|
109
|
-
end
|
110
|
-
|
111
|
-
include JSONAPIonify::Callbacks
|
112
|
-
define_callbacks :request
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
def callbacks_for(action_name)
|
117
|
-
resource = self
|
118
|
-
callbacks[action_name] ||= Class.new(base_callbacks)
|
112
|
+
return before_request &block if action_name == nil
|
113
|
+
send("before_#{action_name}", &block)
|
119
114
|
end
|
120
115
|
|
121
|
-
def define_action(*args, **options, &block)
|
122
|
-
Action.new(*args, **options, &block).tap do |new_action|
|
116
|
+
def define_action(name, *args, **options, &block)
|
117
|
+
Action.new(name, *args, **options, &block).tap do |new_action|
|
123
118
|
action_definitions.delete new_action
|
124
119
|
action_definitions << new_action
|
125
120
|
end
|
@@ -133,14 +128,11 @@ module JSONAPIonify::Api
|
|
133
128
|
|
134
129
|
def no_action_response(request)
|
135
130
|
if request_method_actions(request).present?
|
136
|
-
Action.
|
137
|
-
elsif
|
138
|
-
Action.
|
139
|
-
headers['Allow'] = path_actions.map(&:request_method).join(', ')
|
140
|
-
error_now :method_not_allowed
|
141
|
-
end
|
131
|
+
Action.error :unsupported_media_type
|
132
|
+
elsif self.path_actions(request).present?
|
133
|
+
Action.error :forbidden
|
142
134
|
else
|
143
|
-
Action.
|
135
|
+
Action.error :not_found
|
144
136
|
end
|
145
137
|
end
|
146
138
|
|
@@ -173,12 +165,26 @@ module JSONAPIonify::Api
|
|
173
165
|
end
|
174
166
|
|
175
167
|
def actions
|
168
|
+
return if action_definitions.blank?
|
176
169
|
action_definitions.select do |action|
|
177
170
|
action.only_associated == false ||
|
178
171
|
(respond_to?(:rel) && action.only_associated == true)
|
179
172
|
end
|
180
173
|
end
|
181
174
|
|
175
|
+
def documented_actions
|
176
|
+
api.eager_load
|
177
|
+
relationships = descendants.select { |descendant| descendant.respond_to? :rel }
|
178
|
+
rels = relationships.each_with_object([]) do |rel, ary|
|
179
|
+
rel.actions.each do |action|
|
180
|
+
ary << [action, "#{rel.rel.owner.type}/:id", [rel, rel.rel.name, false, "#{action.name} #{rel.rel.owner.type.singularize.possessive} #{rel.rel.name}"]]
|
181
|
+
end
|
182
|
+
end
|
183
|
+
actions.map do |action|
|
184
|
+
[action, '', [self, type, true, "#{action.name} #{type}"]]
|
185
|
+
end + rels
|
186
|
+
end
|
187
|
+
|
182
188
|
private
|
183
189
|
|
184
190
|
def base_path
|
@@ -6,7 +6,7 @@ module JSONAPIonify::Api
|
|
6
6
|
extend JSONAPIonify::InheritedAttributes
|
7
7
|
extend JSONAPIonify::Types
|
8
8
|
inherited_array_attribute :attributes
|
9
|
-
delegate :attributes, to: :class
|
9
|
+
delegate :id_attribute, :attributes, to: :class
|
10
10
|
|
11
11
|
context(:fields, readonly: true) do |context|
|
12
12
|
should_error = false
|
@@ -20,7 +20,7 @@ module JSONAPIonify::Api
|
|
20
20
|
attribute ? field_list << attribute.name : error(:field_not_permitted, type, field) && (should_error = true)
|
21
21
|
end
|
22
22
|
end
|
23
|
-
raise
|
23
|
+
raise Errors::RequestError if should_error
|
24
24
|
fields
|
25
25
|
end
|
26
26
|
end
|
@@ -5,5 +5,23 @@ module JSONAPIonify::Api
|
|
5
5
|
define_method(name, &block)
|
6
6
|
end
|
7
7
|
|
8
|
+
def authentication(&block)
|
9
|
+
context :authentication, readonly: true do |context|
|
10
|
+
OpenStruct.new.tap do |authentication_object|
|
11
|
+
if instance_exec(context.request, authentication_object, &block) == false
|
12
|
+
error_now :forbidden
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
before do |context|
|
18
|
+
context.authentication
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def on_exception(&block)
|
23
|
+
before_exception &block
|
24
|
+
end
|
25
|
+
|
8
26
|
end
|
9
27
|
end
|
@@ -1,79 +1,209 @@
|
|
1
1
|
module JSONAPIonify::Api
|
2
2
|
module Resource::Definitions::Pagination
|
3
|
-
|
4
3
|
class PaginationLinksDelegate
|
5
4
|
|
6
|
-
def initialize(
|
7
|
-
@
|
8
|
-
@
|
5
|
+
def initialize(url, params, links)
|
6
|
+
@url = url
|
7
|
+
@params = params
|
8
|
+
@links = links
|
9
9
|
end
|
10
10
|
|
11
11
|
%i{first last next prev}.each do |method|
|
12
12
|
define_method method do |**options|
|
13
|
-
@links[method] = URI.parse(@
|
13
|
+
@links[method] = URI.parse(@url).tap do |uri|
|
14
14
|
page_params = { page: options }.deep_stringify_keys
|
15
|
-
uri.query = @
|
15
|
+
uri.query = @params.deep_merge(page_params).to_param
|
16
16
|
end.to_s
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
20
|
end
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
22
|
+
def self.extended(klass)
|
23
|
+
klass.class_eval do
|
24
|
+
include InstanceMethods
|
25
|
+
|
26
|
+
inherited_hash_attribute :pagination_strategies
|
27
|
+
|
28
|
+
define_pagination_strategy 'Object' do |collection|
|
29
|
+
collection
|
30
|
+
end
|
31
|
+
|
32
|
+
define_pagination_strategy 'Enumerable' do |collection, params, links, per, context|
|
33
|
+
size = Integer(params['first'] || params['last'] || per)
|
34
|
+
|
35
|
+
slice =
|
36
|
+
if (params['before'] && params['first']) || (params['after'] && params['last'])
|
37
|
+
error :forbidden do
|
38
|
+
message 'Illegal combination of parameters'
|
39
|
+
end
|
40
|
+
elsif (after = params['after'])
|
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)
|
47
|
+
elsif (before = params['before'])
|
48
|
+
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)
|
54
|
+
elsif params['last']
|
55
|
+
collection.last(size)
|
56
|
+
else
|
57
|
+
collection.first(size)
|
58
|
+
end
|
59
|
+
|
60
|
+
links.first first: size
|
61
|
+
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
|
64
|
+
|
65
|
+
slice
|
66
|
+
end
|
67
|
+
|
68
|
+
define_pagination_strategy 'ActiveRecord::Relation' do |collection, params, links, per, context|
|
69
|
+
size = Integer(params['first'] || params['last'] || per)
|
70
|
+
|
71
|
+
slice =
|
72
|
+
if (params['before'] && params['first']) || (params['after'] && params['last'])
|
73
|
+
error :forbidden do
|
74
|
+
message 'Illegal combination of parameters'
|
75
|
+
end
|
76
|
+
elsif (after = params['after'])
|
77
|
+
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)
|
83
|
+
elsif (before = params['before'])
|
84
|
+
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)
|
90
|
+
collection.where(id_attribute => ids)
|
91
|
+
elsif params['last']
|
92
|
+
ids = collection.reverse_order.limit(size).pluck(id_attribute)
|
93
|
+
collection.where(id_attribute => ids).limit(size)
|
94
|
+
else
|
95
|
+
collection.limit(size)
|
96
|
+
end
|
97
|
+
|
98
|
+
links.first first: size
|
99
|
+
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
|
102
|
+
|
103
|
+
slice
|
104
|
+
end
|
56
105
|
end
|
57
|
-
|
58
|
-
|
59
|
-
|
106
|
+
end
|
107
|
+
|
108
|
+
def define_pagination_strategy(mod, &block)
|
109
|
+
pagination_strategies[mod.to_s] = block
|
110
|
+
end
|
60
111
|
|
61
|
-
def
|
62
|
-
|
63
|
-
|
112
|
+
def enable_pagination(per: 50)
|
113
|
+
param :page, :after, actions: %i{list}
|
114
|
+
param :page, :before, actions: %i{list}
|
115
|
+
param :page, :first, actions: %i{list}
|
116
|
+
param :page, :last, actions: %i{list}
|
64
117
|
context :paginated_collection do |context|
|
65
|
-
|
66
|
-
|
67
|
-
|
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)
|
68
121
|
end
|
69
|
-
|
70
|
-
|
122
|
+
|
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,
|
71
131
|
context.request.params['page'] || {},
|
72
|
-
|
73
|
-
|
132
|
+
links_delegate,
|
133
|
+
per,
|
134
|
+
context,
|
135
|
+
&block
|
74
136
|
)
|
75
137
|
end
|
76
138
|
end
|
77
139
|
|
140
|
+
module InstanceMethods
|
141
|
+
|
142
|
+
def array_select_past_cursor(collection, sort_params, key_values)
|
143
|
+
sort_params.length.times.map do |i|
|
144
|
+
set = sort_params[0..i]
|
145
|
+
*contains_fields, outside_field = set
|
146
|
+
|
147
|
+
# Collect the contains results
|
148
|
+
contains_results = contains_fields.map do |field|
|
149
|
+
collection.select do |item|
|
150
|
+
value = item.send(field.name)
|
151
|
+
expected_value = key_values[field.name]
|
152
|
+
value && value.send(field.contains_operator, expected_value)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Collect the outside results
|
157
|
+
outside_results = collection.select do |item|
|
158
|
+
value = item.send(outside_field.name)
|
159
|
+
expected_value = key_values[outside_field.name.to_s]
|
160
|
+
value && value.send(outside_field.outside_operator, expected_value)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Finish the query
|
164
|
+
[*contains_results, outside_results].reduce(:&)
|
165
|
+
end.reduce(:|) || []
|
166
|
+
end
|
167
|
+
|
168
|
+
def arel_select_past_cursor(collection, sort_params, key_values)
|
169
|
+
subselect = sort_params.length.times.map do |i|
|
170
|
+
set = sort_params[0..i]
|
171
|
+
*contains_fields, outside_field = set
|
172
|
+
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}")
|
181
|
+
end
|
182
|
+
|
183
|
+
def parse_and_validate_cursor(param, cursor, context)
|
184
|
+
should_error = false
|
185
|
+
options = JSON.parse(Base64.urlsafe_decode64(cursor))
|
186
|
+
|
187
|
+
# Validate Type
|
188
|
+
unless options['t'] == self.class.type
|
189
|
+
should_error = true
|
190
|
+
error(:page_parameter_invalid, :page, param) do
|
191
|
+
detail 'The cursor type does not match the resource'
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# Validate Sort
|
196
|
+
unless options['s'] == context.params['sort']
|
197
|
+
should_error = true
|
198
|
+
error(:page_parameter_invalid, :page, param) do
|
199
|
+
detail 'The cursor sort does not match the request sort'
|
200
|
+
end
|
201
|
+
end
|
202
|
+
raise Errors::RequestError if should_error
|
203
|
+
|
204
|
+
options['a']
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
78
208
|
end
|
79
209
|
end
|
@@ -6,24 +6,30 @@ module JSONAPIonify::Api
|
|
6
6
|
extend JSONAPIonify::InheritedAttributes
|
7
7
|
inherited_hash_attribute :param_definitions
|
8
8
|
|
9
|
-
before do |context|
|
10
|
-
context.params # pull params so they verify
|
11
|
-
end
|
12
|
-
|
13
9
|
context(:params, readonly: true) do |context|
|
14
|
-
should_error
|
10
|
+
should_error = false
|
15
11
|
|
16
|
-
|
17
|
-
params = self.class.param_definitions.select do |_, v|
|
12
|
+
params = self.class.param_definitions.select do |_, v|
|
18
13
|
v.actions.blank? || v.actions.include?(action_name)
|
19
14
|
end
|
15
|
+
|
16
|
+
context.request.params.replace(
|
17
|
+
[*params.values.select(&:has_default?).map(&:default), context.request.params].reduce(:deep_merge)
|
18
|
+
)
|
19
|
+
|
20
20
|
required_params = params.select do |_, v|
|
21
21
|
v.required
|
22
22
|
end
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
23
|
+
|
24
|
+
# Check for validity
|
25
|
+
context.request.params.each do |k, v|
|
26
|
+
keypath = ParamOptions.hash_to_keypaths(k => v)[0]
|
27
|
+
reserved = ParamOptions.reserved?(k)
|
28
|
+
allowed = params.keys.include? keypath
|
29
|
+
valid = ParamOptions.valid?(k) || v.is_a?(Hash)
|
30
|
+
unless reserved || (allowed && valid)
|
31
|
+
should_error = true
|
32
|
+
error :parameter_invalid, ParamOptions.keypath_to_string(*keypath)
|
27
33
|
end
|
28
34
|
end
|
29
35
|
|
@@ -32,7 +38,7 @@ module JSONAPIonify::Api
|
|
32
38
|
error :parameters_missing, missing_params
|
33
39
|
end
|
34
40
|
|
35
|
-
raise
|
41
|
+
raise Errors::RequestError if should_error
|
36
42
|
|
37
43
|
# Return the params
|
38
44
|
context.request.params
|
@@ -45,5 +51,30 @@ module JSONAPIonify::Api
|
|
45
51
|
param_definitions[keypath] = ParamOptions.new(*keypath, **options)
|
46
52
|
end
|
47
53
|
|
54
|
+
def sticky_params(params)
|
55
|
+
sticky_param_definitions = param_definitions.values.select(&:sticky)
|
56
|
+
ParamOptions.hash_to_keypaths(params).map do |keypath|
|
57
|
+
definition = sticky_param_definitions.find do |definition|
|
58
|
+
definition.keypath == keypath
|
59
|
+
end
|
60
|
+
next {} unless definition
|
61
|
+
value = definition.extract_value(params)
|
62
|
+
if definition.default_value?(value)
|
63
|
+
{}
|
64
|
+
else
|
65
|
+
definition.with_value(value)
|
66
|
+
end
|
67
|
+
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
|
+
end
|
78
|
+
|
48
79
|
end
|
49
80
|
end
|
@@ -6,65 +6,6 @@ module JSONAPIonify::Api
|
|
6
6
|
extend JSONAPIonify::InheritedAttributes
|
7
7
|
inherited_hash_attribute :request_header_definitions
|
8
8
|
|
9
|
-
# Standard HTTP Headers
|
10
|
-
# https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields
|
11
|
-
request_header 'accept'
|
12
|
-
request_header 'accept-charset'
|
13
|
-
request_header 'accept-encoding'
|
14
|
-
request_header 'accept-language'
|
15
|
-
request_header 'accept-datetime'
|
16
|
-
request_header 'authorization'
|
17
|
-
request_header 'cache-control'
|
18
|
-
request_header 'connection'
|
19
|
-
request_header 'cookie'
|
20
|
-
request_header 'content-length'
|
21
|
-
request_header 'content-md5'
|
22
|
-
request_header 'content-type'
|
23
|
-
request_header 'date'
|
24
|
-
request_header 'expect'
|
25
|
-
request_header 'from'
|
26
|
-
request_header 'host'
|
27
|
-
request_header 'if-match'
|
28
|
-
request_header 'if-modified-since'
|
29
|
-
request_header 'if-none-match'
|
30
|
-
request_header 'if-range'
|
31
|
-
request_header 'if-unmodified-since'
|
32
|
-
request_header 'max-forwards'
|
33
|
-
request_header 'origin'
|
34
|
-
request_header 'pragma'
|
35
|
-
request_header 'proxy-authorization'
|
36
|
-
request_header 'range'
|
37
|
-
request_header 'referer'
|
38
|
-
request_header 'te'
|
39
|
-
request_header 'user-agent'
|
40
|
-
request_header 'upgrade'
|
41
|
-
request_header 'via'
|
42
|
-
request_header 'warning'
|
43
|
-
|
44
|
-
# Non-Standard, but widely used HTTP headers
|
45
|
-
request_header 'x-requested-with'
|
46
|
-
request_header 'dnt'
|
47
|
-
request_header 'x-forwarded-for'
|
48
|
-
request_header 'x-forwarded-host'
|
49
|
-
request_header 'x-forwarded-proto'
|
50
|
-
request_header 'front-end-https'
|
51
|
-
request_header 'x-att-device-id'
|
52
|
-
request_header 'x-wap-profile'
|
53
|
-
request_header 'proxy-connection'
|
54
|
-
request_header 'x-uidh'
|
55
|
-
request_header 'upgrade-insecure-requests'
|
56
|
-
|
57
|
-
# Don't allow method overrides
|
58
|
-
# request_header 'x-http-method-override'
|
59
|
-
|
60
|
-
# Don't allow CSRF tokens, as they should not be used
|
61
|
-
# in the api by default
|
62
|
-
# request_header 'x-csrf-token'
|
63
|
-
|
64
|
-
before do |context|
|
65
|
-
context.request_headers # pull request_headers so they verify
|
66
|
-
end
|
67
|
-
|
68
9
|
context(:request_headers) do |context|
|
69
10
|
should_error = false
|
70
11
|
|
@@ -76,19 +17,12 @@ module JSONAPIonify::Api
|
|
76
17
|
v.required
|
77
18
|
end
|
78
19
|
|
79
|
-
if (invalid_keys = context.request.headers.keys.map(&:downcase) - headers.keys.map(&:downcase)).present?
|
80
|
-
should_error = true
|
81
|
-
invalid_keys.each do |key|
|
82
|
-
error :header_not_permitted, key
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
20
|
if (missing_keys = required_headers.keys.map(&:downcase) - context.request.headers.keys.map(&:downcase)).present?
|
87
21
|
should_error = true
|
88
22
|
error :headers_missing, missing_keys
|
89
23
|
end
|
90
24
|
|
91
|
-
raise
|
25
|
+
raise Errors::RequestError if should_error
|
92
26
|
|
93
27
|
context.request.headers
|
94
28
|
end
|
@@ -1,17 +1,6 @@
|
|
1
1
|
module JSONAPIonify::Api
|
2
2
|
module Resource::Definitions::Scopes
|
3
3
|
|
4
|
-
def self.extended(klass)
|
5
|
-
klass.class_eval do
|
6
|
-
id :id
|
7
|
-
scope { raise NotImplementedError, 'scope not implemented' }
|
8
|
-
collection { raise NotImplementedError, 'collection not implemented' }
|
9
|
-
instance { raise NotImplementedError, 'instance not implemented' }
|
10
|
-
new_instance { raise NotImplementedError, 'new instance not implemented' }
|
11
|
-
param :include
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
4
|
def scope(&block)
|
16
5
|
define_singleton_method(:current_scope) do
|
17
6
|
Object.new.instance_eval(&block)
|
@@ -25,10 +14,10 @@ module JSONAPIonify::Api
|
|
25
14
|
|
26
15
|
def instance(&block)
|
27
16
|
define_singleton_method(:find_instance) do |id|
|
28
|
-
|
17
|
+
instance_exec(current_scope, id, OpenStruct.new, &block)
|
29
18
|
end
|
30
19
|
context :instance do |context|
|
31
|
-
|
20
|
+
instance_exec(context.scope, context.id, context, &block)
|
32
21
|
end
|
33
22
|
end
|
34
23
|
|