praxis 2.0.pre.14 → 2.0.pre.18
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/bin/praxis +6 -0
- data/lib/praxis/action_definition.rb +2 -2
- data/lib/praxis/api_definition.rb +8 -4
- data/lib/praxis/blueprint.rb +22 -7
- data/lib/praxis/collection.rb +11 -0
- data/lib/praxis/dispatcher.rb +3 -3
- data/lib/praxis/docs/open_api/response_object.rb +21 -6
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +63 -16
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +1 -2
- data/lib/praxis/mapper/resource.rb +2 -2
- data/lib/praxis/mapper/selector_generator.rb +2 -2
- data/lib/praxis/media_type_identifier.rb +11 -1
- data/lib/praxis/request.rb +5 -0
- data/lib/praxis/request_stages/validate_params_and_headers.rb +0 -6
- data/lib/praxis/request_stages/validate_payload.rb +0 -1
- data/lib/praxis/response_definition.rb +46 -66
- data/lib/praxis/responses/http.rb +3 -1
- data/lib/praxis/tasks/routes.rb +6 -6
- data/lib/praxis/types/multipart_array.rb +14 -5
- data/lib/praxis/version.rb +1 -1
- data/praxis.gemspec +1 -1
- data/spec/praxis/action_definition_spec.rb +6 -3
- data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +92 -34
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +17 -2
- data/spec/praxis/extensions/support/spec_resources_active_model.rb +2 -0
- data/spec/praxis/mapper/resource_spec.rb +3 -3
- data/spec/praxis/mapper/selector_generator_spec.rb +34 -0
- data/spec/praxis/media_type_identifier_spec.rb +15 -1
- data/spec/praxis/request_spec.rb +3 -3
- data/spec/praxis/response_definition_spec.rb +37 -129
- data/spec/praxis/trait_spec.rb +3 -2
- data/spec/spec_app/design/media_types/instance.rb +1 -1
- data/spec/spec_app/design/resources/instances.rb +2 -2
- data/spec/spec_helper.rb +1 -0
- data/spec/support/spec_blueprints.rb +3 -3
- data/spec/support/spec_resources.rb +4 -0
- data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +33 -0
- data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +4 -0
- data/tasks/thor/templates/generator/example_app/config/environment.rb +1 -1
- data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +2 -2
- metadata +9 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 269b2403359c2a9ec0b591d695a5146ff648d16046d82ec52162086d0006bbb7
|
4
|
+
data.tar.gz: 3f28eebb4e3ceecd39a7ba5969df7ece5feafe93c3fd6b5d0dfb17df92f22fcd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 800cf53e02f7322a8db37212c16b3e53827bc50acbf630a58d2027397120c2c9044c81ae919a2855b7c3635786113526712cb6b882e505fbc0c1f46042ed8f5c
|
7
|
+
data.tar.gz: bd0969b6aa8618b78d3a2f42b9e14d5c5f5d776019bd1b697a66e3dbbbe3a6535c475d806edb64f82d16b370ba76cf8effd4fe73d03adc5934cd48446e3096b5
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,19 @@
|
|
2
2
|
|
3
3
|
## next
|
4
4
|
|
5
|
+
## 2.0.pre.18
|
6
|
+
* Upgraded to newest Attributor, which cleans up the required: true semantics to only work on keys, and introduces null: true for nullability of values (independent from presence of keys or not)
|
7
|
+
* Fixed a selector generator bug that would occur when using deep nested resource dependencies as strings 'foo.bar.baz.bam'. In this cases only partial tracking of relationships would be built, which could cause to not fully eager load DB queries.
|
8
|
+
## 2.0.pre.17
|
9
|
+
* Changed the Parameter Filtering to use left outer joins (and extra conditions), to allow for the proper results when OR clauses are involved in certain configurations.
|
10
|
+
* Built support for allowing filtering directly on associations using `!` and `!!` operators. This allows to filter results where
|
11
|
+
there are no associated rows (`!!`) or if there are some associated rows (`!`)
|
12
|
+
* Allow implicit definition of `filters_mapping` for filter names that match top-level associations of the model (i.e., like we do for the columns)
|
13
|
+
## 2.0.pre.16
|
14
|
+
|
15
|
+
* Updated `Resource.property` signature to only accept known named arguments (`dependencies` and `though` at this time) to spare anyone else from going insane wondering why their `depednencies` aren't working.
|
16
|
+
* Fixed issue with Filtering Params, that occurred with using the ! or !! operators on String-typed fields.
|
17
|
+
|
5
18
|
## 2.0.pre.14
|
6
19
|
|
7
20
|
* More encoding/decoding robustness for filters.
|
data/bin/praxis
CHANGED
@@ -9,6 +9,12 @@ rescue Bundler::GemfileNotFound
|
|
9
9
|
# no-op: we might be installed as a system gem
|
10
10
|
end
|
11
11
|
|
12
|
+
if ARGV[0] == "version"
|
13
|
+
require 'praxis/version'
|
14
|
+
puts "Praxis version #{Praxis::VERSION}"
|
15
|
+
exit 0
|
16
|
+
end
|
17
|
+
|
12
18
|
if ["routes","docs","console"].include? ARGV[0]
|
13
19
|
require 'rake'
|
14
20
|
require 'praxis'
|
@@ -124,8 +124,8 @@ module Praxis
|
|
124
124
|
def payload(type=Attributor::Struct, **opts, &block)
|
125
125
|
return @payload if !block && ( opts.nil? || opts.empty? ) && type == Attributor::Struct
|
126
126
|
|
127
|
-
unless
|
128
|
-
opts
|
127
|
+
unless opts.key?(:required)
|
128
|
+
opts = {required: true, null: false}.merge(opts) # Make the payload required and non-nullable by default
|
129
129
|
end
|
130
130
|
|
131
131
|
if @payload
|
@@ -97,8 +97,10 @@ module Praxis
|
|
97
97
|
description( description || 'Standard response for successful HTTP requests.' )
|
98
98
|
|
99
99
|
media_type media_type
|
100
|
-
location location
|
101
|
-
headers
|
100
|
+
location if location
|
101
|
+
headers&.each do |(name, value)|
|
102
|
+
header(name: name, value: value)
|
103
|
+
end
|
102
104
|
end
|
103
105
|
|
104
106
|
api.response_template :created do |media_type: nil, location: nil, headers: nil, description: nil|
|
@@ -106,8 +108,10 @@ module Praxis
|
|
106
108
|
description( description || 'The request has been fulfilled and resulted in a new resource being created.' )
|
107
109
|
|
108
110
|
media_type media_type if media_type
|
109
|
-
location location
|
110
|
-
headers
|
111
|
+
location if location
|
112
|
+
headers&.each do |(name, value)|
|
113
|
+
header(name: name, value: value)
|
114
|
+
end
|
111
115
|
end
|
112
116
|
end
|
113
117
|
|
data/lib/praxis/blueprint.rb
CHANGED
@@ -321,18 +321,33 @@ module Praxis
|
|
321
321
|
@validating = true
|
322
322
|
|
323
323
|
errors = []
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
324
|
+
keys_provided = []
|
325
|
+
|
326
|
+
self.class.attributes.each do |key, attribute|
|
327
|
+
sub_context = self.class.generate_subcontext(context, key)
|
328
|
+
value = _get_attr(key)
|
329
|
+
keys_provided << key if @object.key?(key)
|
328
330
|
|
329
331
|
if value.respond_to?(:validating) # really, it's a thing with sub-attributes
|
330
332
|
next if value.validating
|
331
333
|
end
|
332
|
-
|
334
|
+
|
335
|
+
# Isn't this handled by the requirements validation? NO! we might want to combine
|
336
|
+
if attribute.options[:required] && !@object.key?(key)
|
337
|
+
errors.concat ["Attribute #{Attributor.humanize_context(sub_context)} is required."]
|
338
|
+
end
|
339
|
+
if @object[key].nil?
|
340
|
+
if !Attributor::Attribute.nullable_attribute?(attribute.options) && @object.key?(key) # It is only nullable if there's an explicite null: true (undefined defaults to false)
|
341
|
+
errors.concat ["Attribute #{Attributor.humanize_context(sub_context)} is not nullable."]
|
342
|
+
end
|
343
|
+
# No need to validate the attribute further if the key wasn't passed...(or we would get nullable errors etc..cause the attribute has no
|
344
|
+
# context if its containing key was even passed (and there might not be a containing key for a top level attribute anyways))
|
345
|
+
else
|
346
|
+
errors.concat attribute.validate(value, sub_context)
|
347
|
+
end
|
333
348
|
end
|
334
|
-
self.class.attribute.type.requirements.each do |
|
335
|
-
validation_errors =
|
349
|
+
self.class.attribute.type.requirements.each do |requirement|
|
350
|
+
validation_errors = requirement.validate(keys_provided, context)
|
336
351
|
errors.concat(validation_errors) unless validation_errors.empty?
|
337
352
|
end
|
338
353
|
errors
|
data/lib/praxis/collection.rb
CHANGED
@@ -32,5 +32,16 @@ module Praxis
|
|
32
32
|
@member_type.domain_model
|
33
33
|
end
|
34
34
|
|
35
|
+
def self.json_schema_type
|
36
|
+
:array
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.as_json_schema(**args)
|
40
|
+
the_type = @attribute && @attribute.type || member_type
|
41
|
+
{
|
42
|
+
type: json_schema_type,
|
43
|
+
items: { '$ref': "#/components/schemas/#{the_type.id}" }
|
44
|
+
}
|
45
|
+
end
|
35
46
|
end
|
36
47
|
end
|
data/lib/praxis/dispatcher.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
module Praxis
|
2
2
|
|
3
3
|
CONTEXT_FOR = {
|
4
|
-
params: [Attributor::
|
5
|
-
headers: [Attributor::
|
6
|
-
payload: [Attributor::
|
4
|
+
params: [Attributor::ROOT_PREFIX, "params".freeze],
|
5
|
+
headers: [Attributor::ROOT_PREFIX, "headers".freeze],
|
6
|
+
payload: [Attributor::ROOT_PREFIX, "payload".freeze]
|
7
7
|
}.freeze
|
8
8
|
|
9
9
|
class Dispatcher
|
@@ -16,12 +16,27 @@ module Praxis
|
|
16
16
|
|
17
17
|
def dump_response_headers_object( headers )
|
18
18
|
headers.each_with_object({}) do |(name,data),accum|
|
19
|
-
#
|
20
|
-
#
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
19
|
+
# each header comes from Praxis::ResponseDefinition
|
20
|
+
# the keys are the header names, and value can be:
|
21
|
+
# "true" => means it only needs to exist
|
22
|
+
# String => which means that it has to fully match
|
23
|
+
# Regex => which means it has to regexp match it
|
24
|
+
|
25
|
+
# Get the schema from the type (defaulting to string in case the type doesn't have the as_json_schema defined)
|
26
|
+
schema = data[:attribute].type.as_json_schema rescue { type: :string }
|
27
|
+
hash = { description: data[:description] || '', schema: schema }
|
28
|
+
# Note, our Headers in response definition are not full types...they're basically only
|
29
|
+
# strings, which can either match anything, match the exact word or match a regex
|
30
|
+
# they don't even have a description...
|
31
|
+
data_value = data[:value]
|
32
|
+
if data_value.is_a? String
|
33
|
+
hash[:pattern] = "^#{data_value}$" # Exact String match
|
34
|
+
elsif data_value.is_a? Regexp
|
35
|
+
sanitized_pattern = data_value.inspect[1..-2] #inspect returns enclosing '/' characters
|
36
|
+
hash[:pattern] = sanitized_pattern
|
37
|
+
end
|
38
|
+
|
39
|
+
accum[name] = hash
|
25
40
|
end
|
26
41
|
end
|
27
42
|
|
@@ -23,7 +23,8 @@ module Praxis
|
|
23
23
|
end
|
24
24
|
|
25
25
|
class ActiveRecordFilterQueryBuilder
|
26
|
-
|
26
|
+
REFERENCES_STRING_SEPARATOR = '/'
|
27
|
+
attr_reader :model, :filters_map
|
27
28
|
|
28
29
|
# Base query to build upon
|
29
30
|
def initialize(query: , model:, filters_map:, debug: false)
|
@@ -48,9 +49,8 @@ module Praxis
|
|
48
49
|
end
|
49
50
|
|
50
51
|
def craft_filter_query(nodetree, for_model:)
|
51
|
-
result = _compute_joins_and_conditions_data(nodetree, model: for_model)
|
52
|
+
result = _compute_joins_and_conditions_data(nodetree, model: for_model, parent_reflection: nil)
|
52
53
|
return @initial_query if result[:conditions].empty?
|
53
|
-
|
54
54
|
|
55
55
|
# Find the root group (usually an AND group) but can be an OR group, or nil if there's only 1 condition
|
56
56
|
root_parent_group = result[:conditions].first[:node_object].parent_group || result[:conditions].first[:node_object]
|
@@ -59,21 +59,35 @@ module Praxis
|
|
59
59
|
end
|
60
60
|
|
61
61
|
# Process the joins
|
62
|
-
query_with_joins = result[:associations_hash].empty? ? @initial_query : @initial_query.
|
62
|
+
query_with_joins = result[:associations_hash].empty? ? @initial_query : @initial_query.left_outer_joins(result[:associations_hash])
|
63
63
|
|
64
64
|
# Proc to apply a single condition
|
65
65
|
apply_single_condition = Proc.new do |condition, associated_query|
|
66
66
|
colo = condition[:model].columns_hash[condition[:name].to_s]
|
67
67
|
column_prefix = condition[:column_prefix]
|
68
|
-
|
69
|
-
|
68
|
+
association_key_column = \
|
69
|
+
if ref = condition[:parent_reflection]
|
70
|
+
# get the target model of the association(where the assoc pk is)
|
71
|
+
target_model = condition[:parent_reflection].klass
|
72
|
+
target_model.columns_hash[condition[:parent_reflection].association_primary_key]
|
73
|
+
else
|
74
|
+
nil
|
75
|
+
end
|
76
|
+
|
77
|
+
# Mark where clause referencing the appropriate alias IF it's not the root table, as there is no association to reference
|
78
|
+
# If we added root table as a reference, we better make sure it is not quoted, as it actually makes AR to see it as an
|
79
|
+
# unmatched reference and eager loads the whole association (it means eager load ALL the things). Not good.
|
80
|
+
unless for_model.table_name == column_prefix
|
81
|
+
associated_query = associated_query.references(build_reference_value(column_prefix, query: associated_query))
|
82
|
+
end
|
70
83
|
self.class.add_clause(
|
71
84
|
query: associated_query,
|
72
85
|
column_prefix: column_prefix,
|
73
86
|
column_object: colo,
|
74
87
|
op: condition[:op],
|
75
88
|
value: condition[:value],
|
76
|
-
fuzzy: condition[:fuzzy]
|
89
|
+
fuzzy: condition[:fuzzy],
|
90
|
+
association_key_column: association_key_column,
|
77
91
|
)
|
78
92
|
end
|
79
93
|
|
@@ -138,7 +152,8 @@ module Praxis
|
|
138
152
|
def _mapped_filter(name)
|
139
153
|
target = @filters_map[name]
|
140
154
|
unless target
|
141
|
-
|
155
|
+
filter_name = name.to_s
|
156
|
+
if (@model.attribute_names + @model.reflections.keys).include?(filter_name)
|
142
157
|
# Cache it in the filters mapping (to avoid later lookups), and return it.
|
143
158
|
@filters_map[name] = name
|
144
159
|
target = name
|
@@ -176,32 +191,64 @@ module Praxis
|
|
176
191
|
end
|
177
192
|
|
178
193
|
# Calculate join tree and conditions array for the nodetree object and its children
|
179
|
-
def _compute_joins_and_conditions_data(nodetree, model:)
|
194
|
+
def _compute_joins_and_conditions_data(nodetree, model:, parent_reflection:)
|
180
195
|
h = {}
|
181
196
|
conditions = []
|
182
197
|
nodetree.children.each do |name, child|
|
183
|
-
|
184
|
-
result = _compute_joins_and_conditions_data(child, model:
|
185
|
-
h[name] = result[:associations_hash]
|
198
|
+
child_reflection = model.reflections[name.to_s]
|
199
|
+
result = _compute_joins_and_conditions_data(child, model: child_reflection.klass, parent_reflection: child_reflection)
|
200
|
+
h[name] = result[:associations_hash]
|
201
|
+
|
186
202
|
conditions += result[:conditions]
|
187
203
|
end
|
188
|
-
|
189
|
-
|
204
|
+
|
205
|
+
column_prefix = nodetree.path == [ALIAS_TABLE_PREFIX] ? model.table_name : nodetree.path.join(REFERENCES_STRING_SEPARATOR)
|
190
206
|
nodetree.conditions.each do |condition|
|
191
|
-
|
207
|
+
# If it's a final ! or !! operation on an association from the parent, it means we need to add a condition
|
208
|
+
# on the existence (or lack of) of the whole associated table
|
209
|
+
ref = model.reflections[condition[:name].to_s]
|
210
|
+
if ref && ['!','!!'].include?(condition[:op])
|
211
|
+
cp = (nodetree.path + [condition[:name].to_s]).join(REFERENCES_STRING_SEPARATOR)
|
212
|
+
conditions += [condition.merge(column_prefix: cp, model: model, parent_reflection: ref)]
|
213
|
+
h[condition[:name]] = {}
|
214
|
+
else
|
215
|
+
# Save the parent reflection where the condition applies as well (used later to get assoc keys)
|
216
|
+
conditions += [condition.merge(column_prefix: column_prefix, model: model, parent_reflection: parent_reflection)]
|
217
|
+
end
|
218
|
+
|
192
219
|
end
|
193
220
|
{associations_hash: h, conditions: conditions}
|
194
221
|
end
|
195
222
|
|
196
|
-
def self.add_clause(query:, column_prefix:, column_object:, op:, value:,fuzzy:)
|
223
|
+
def self.add_clause(query:, column_prefix:, column_object:, op:, value:,fuzzy:, association_key_column:)
|
197
224
|
likeval = get_like_value(value,fuzzy)
|
225
|
+
|
226
|
+
association_op = nil
|
198
227
|
case op
|
199
228
|
when '!' # name! means => name IS NOT NULL (and the incoming value is nil)
|
200
229
|
op = '!='
|
201
230
|
value = nil # Enforce it is indeed nil (should be)
|
231
|
+
association_op = :not_null if association_key_column && !column_object
|
202
232
|
when '!!'
|
203
233
|
op = '='
|
204
234
|
value = nil # Enforce it is indeed nil (should be)
|
235
|
+
association_op = :null if association_key_column && !column_object
|
236
|
+
end
|
237
|
+
|
238
|
+
if association_op
|
239
|
+
neg = association_op == :not_null ? true : false
|
240
|
+
qr = quote_right_part(query: query, value: nil, column_object: association_key_column, negative: neg)
|
241
|
+
return query.where("#{quote_column_path(query: query, prefix: column_prefix, column_object: association_key_column)} #{qr}")
|
242
|
+
end
|
243
|
+
|
244
|
+
# Add an AND along with the condition, which ensures the left outter join 'exists' for it
|
245
|
+
# Normally this wouldn't be necessary as a condition on a given value mathing would imply the related row was there
|
246
|
+
# but this is not the case for NULL conditions, as the foreign column would match a NULL value, but not because the related column
|
247
|
+
# is NULL, but because the whole missing related row would appear with all fields null
|
248
|
+
# NOTE: we don't need to do it for conditions applying to the root of the tree (there isn't a join to it)
|
249
|
+
if association_key_column
|
250
|
+
qr = quote_right_part(query: query, value: nil, column_object: association_key_column, negative: true)
|
251
|
+
query = query.where("#{quote_column_path(query: query, prefix: column_prefix, column_object: association_key_column)} #{qr}")
|
205
252
|
end
|
206
253
|
|
207
254
|
case op
|
@@ -222,8 +222,7 @@ module Praxis
|
|
222
222
|
value_type = attr_filters[:value_type]
|
223
223
|
next unless value_type == Attributor::String
|
224
224
|
|
225
|
-
|
226
|
-
unless value.empty?
|
225
|
+
if item[:value].presence
|
227
226
|
fuzzy_match = attr_filters[:fuzzy_match]
|
228
227
|
if item[:fuzzy] && !item[:fuzzy].empty? && !fuzzy_match
|
229
228
|
errors << "Fuzzy matching for #{attr_name} is not allowed (yet '*' was found in the value)"
|
@@ -46,8 +46,8 @@ module Praxis::Mapper
|
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
49
|
-
def self.property(name,
|
50
|
-
self.properties[name] =
|
49
|
+
def self.property(name, dependencies: nil, through: nil)
|
50
|
+
self.properties[name] = {dependencies: dependencies, through: through}
|
51
51
|
end
|
52
52
|
|
53
53
|
def self._finalize!
|
@@ -98,10 +98,10 @@ module Praxis::Mapper
|
|
98
98
|
when Symbol
|
99
99
|
map_property(dependency, true)
|
100
100
|
when String
|
101
|
-
head, tail = dependency.split('.').collect(&:to_sym)
|
101
|
+
head, *tail = dependency.split('.').collect(&:to_sym)
|
102
102
|
raise "String dependencies can not be singular" if tail.nil?
|
103
103
|
|
104
|
-
add_association(head, {
|
104
|
+
add_association(head, tail.reverse.inject({}) { |hash, dep| { dep => hash } })
|
105
105
|
end
|
106
106
|
end
|
107
107
|
|
@@ -198,10 +198,20 @@ module Praxis
|
|
198
198
|
obj = self.class.new
|
199
199
|
obj.type = self.type
|
200
200
|
obj.subtype = self.subtype
|
201
|
-
|
201
|
+
target_suffix = suffix || self.suffix
|
202
|
+
obj.suffix = redundant_suffix(target_suffix) ? '' : target_suffix
|
202
203
|
obj.parameters = self.parameters.merge(parameters)
|
203
204
|
|
204
205
|
obj
|
205
206
|
end
|
207
|
+
|
208
|
+
def redundant_suffix(suffix)
|
209
|
+
# application/json does not need to be suffixed with +json (same for application/xml)
|
210
|
+
# we're supporting text/json and text/xml for older formats as well
|
211
|
+
if (self.type == 'application' || self.type == 'text') && self.subtype == suffix
|
212
|
+
return true
|
213
|
+
end
|
214
|
+
false
|
215
|
+
end
|
206
216
|
end
|
207
217
|
end
|
data/lib/praxis/request.rb
CHANGED
@@ -142,18 +142,23 @@ module Praxis
|
|
142
142
|
def validate_headers(context)
|
143
143
|
return [] unless action.headers
|
144
144
|
|
145
|
+
return ["Attribute #{Attributor.humanize_context(context)} is required."] if action.headers.options[:required] == true && self.headers.nil?
|
146
|
+
|
145
147
|
action.headers.validate(self.headers, context)
|
146
148
|
end
|
147
149
|
|
148
150
|
def validate_params(context)
|
149
151
|
return [] unless action.params
|
150
152
|
|
153
|
+
return ["Attribute #{Attributor.humanize_context(context)} is required."] if action.params.options[:required] == true && self.params.nil?
|
154
|
+
|
151
155
|
action.params.validate(self.params, context)
|
152
156
|
end
|
153
157
|
|
154
158
|
def validate_payload(context)
|
155
159
|
return [] unless action.payload
|
156
160
|
|
161
|
+
return ["Attribute #{Attributor.humanize_context(context)} is required."] if action.payload.options[:required] == true && self.payload.nil?
|
157
162
|
action.payload.validate(self.payload, context)
|
158
163
|
end
|
159
164
|
|
@@ -38,12 +38,6 @@ module Praxis
|
|
38
38
|
)
|
39
39
|
end
|
40
40
|
|
41
|
-
attribute_resolver = Attributor::AttributeResolver.new
|
42
|
-
Attributor::AttributeResolver.current = attribute_resolver
|
43
|
-
|
44
|
-
attribute_resolver.register("headers",request.headers)
|
45
|
-
attribute_resolver.register("params",request.params)
|
46
|
-
|
47
41
|
errors = request.validate_headers(CONTEXT_FOR[:headers])
|
48
42
|
errors += request.validate_params(CONTEXT_FOR[:params])
|
49
43
|
if errors.any?
|
@@ -55,52 +55,36 @@ module Praxis
|
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
|
-
def location(loc=nil)
|
59
|
-
return
|
60
|
-
unless ( loc.is_a?(Regexp) || loc.is_a?(String) )
|
61
|
-
raise Exceptions::InvalidConfiguration.new(
|
62
|
-
"Invalid location specification. Location in response must be either a regular expression or a string."
|
63
|
-
)
|
64
|
-
end
|
65
|
-
@spec[:location] = loc
|
66
|
-
end
|
58
|
+
def location(loc=nil, description: nil)
|
59
|
+
return headers.dig('Location',:value) if loc.nil?
|
67
60
|
|
68
|
-
|
69
|
-
|
61
|
+
header('Location', loc, description: description)
|
62
|
+
end
|
70
63
|
|
71
|
-
|
72
|
-
|
73
|
-
hdrs.each {|header_name| header(header_name) }
|
74
|
-
when Hash
|
75
|
-
header(hdrs)
|
76
|
-
when String
|
77
|
-
header(hdrs)
|
78
|
-
else
|
79
|
-
raise Exceptions::InvalidConfiguration.new(
|
80
|
-
"Invalid headers specification: Arrays, Hash, or String must be used. Received: #{hdrs.inspect}"
|
81
|
-
)
|
82
|
-
end
|
64
|
+
def headers
|
65
|
+
@spec[:headers]
|
83
66
|
end
|
84
67
|
|
85
|
-
def header(
|
86
|
-
case
|
87
|
-
when String
|
88
|
-
|
89
|
-
when
|
90
|
-
|
91
|
-
|
92
|
-
raise Exceptions::InvalidConfiguration.new(
|
93
|
-
"Header definitions for #{k.inspect} can only match values of type String or Regexp. Received: #{v.inspect}"
|
94
|
-
)
|
95
|
-
end
|
96
|
-
@spec[:headers][k] = v
|
97
|
-
end
|
68
|
+
def header(name, value, description: nil)
|
69
|
+
the_type, args = case value
|
70
|
+
when nil,String
|
71
|
+
[String, {}]
|
72
|
+
when Regexp
|
73
|
+
# A regexp means it's gonna be a String typed, attached to a regexp
|
74
|
+
[String, { regexp: value }]
|
98
75
|
else
|
99
76
|
raise Exceptions::InvalidConfiguration.new(
|
100
|
-
"A header definition can only take
|
101
|
-
"
|
77
|
+
"A header definition for a response can only take String, Regexp or nil values (to match anything)." +
|
78
|
+
"Received the following value for header name #{name}: #{value}"
|
102
79
|
)
|
103
80
|
end
|
81
|
+
|
82
|
+
info = {
|
83
|
+
value: value,
|
84
|
+
attribute: Attributor::Attribute.new(the_type, **args)
|
85
|
+
}
|
86
|
+
info[:description] = description if description
|
87
|
+
@spec[:headers][name] = info
|
104
88
|
end
|
105
89
|
|
106
90
|
def example(context=nil)
|
@@ -123,13 +107,14 @@ module Praxis
|
|
123
107
|
:status => status,
|
124
108
|
:headers => {}
|
125
109
|
}
|
126
|
-
content[:location] = _describe_header(location) unless location == nil
|
127
110
|
|
128
111
|
unless headers == nil
|
129
112
|
headers.each do |name, value|
|
130
113
|
content[:headers][name] = _describe_header(value)
|
131
114
|
end
|
132
115
|
end
|
116
|
+
content[:location] = content[:headers]['Location']
|
117
|
+
|
133
118
|
|
134
119
|
if self.media_type
|
135
120
|
payload = media_type.describe(true)
|
@@ -173,14 +158,14 @@ module Praxis
|
|
173
158
|
end
|
174
159
|
|
175
160
|
def _describe_header(data)
|
176
|
-
|
177
|
-
|
161
|
+
|
162
|
+
data_type = data[:value].is_a?(Regexp) ? :regexp : :string
|
163
|
+
data_value = data[:value].is_a?(Regexp) ? data[:value].inspect : data[:value]
|
178
164
|
{ :value => data_value, :type => data_type }
|
179
165
|
end
|
180
166
|
|
181
167
|
def validate(response, validate_body: false)
|
182
168
|
validate_status!(response)
|
183
|
-
validate_location!(response)
|
184
169
|
validate_headers!(response)
|
185
170
|
validate_content_type!(response)
|
186
171
|
validate_parts!(response)
|
@@ -222,23 +207,13 @@ module Praxis
|
|
222
207
|
end
|
223
208
|
end
|
224
209
|
|
225
|
-
|
226
|
-
# Validates 'Location' header
|
227
|
-
#
|
228
|
-
# @raise [Exceptions::Validation] When location header does not match to the defined one.
|
229
|
-
#
|
230
|
-
def validate_location!(response)
|
231
|
-
return if location.nil? || location === response.headers['Location']
|
232
|
-
raise Exceptions::Validation.new("LOCATION does not match #{location.inspect}")
|
233
|
-
end
|
234
|
-
|
235
|
-
|
236
210
|
# Validates Headers
|
237
211
|
#
|
238
212
|
# @raise [Exceptions::Validation] When there is a missing required header..
|
239
213
|
#
|
240
214
|
def validate_headers!(response)
|
241
215
|
return unless headers
|
216
|
+
|
242
217
|
headers.each do |name, value|
|
243
218
|
if name.is_a? Symbol
|
244
219
|
raise Exceptions::Validation.new(
|
@@ -252,20 +227,25 @@ module Praxis
|
|
252
227
|
)
|
253
228
|
end
|
254
229
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
"Header #{name.inspect}, with value #{value.inspect} does not match #{response.headers[name]}."
|
260
|
-
)
|
261
|
-
end
|
262
|
-
when Regexp
|
263
|
-
if response.headers[name] !~ value
|
264
|
-
raise Exceptions::Validation.new(
|
265
|
-
"Header #{name.inspect}, with value #{value.inspect} does not match #{response.headers[name].inspect}."
|
266
|
-
)
|
267
|
-
end
|
230
|
+
errors = value[:attribute].validate(response.headers[name])
|
231
|
+
|
232
|
+
unless errors.empty?
|
233
|
+
raise Exceptions::Validation.new("Header #{name.inspect}, with value #{value.inspect} does not match #{response.headers[name]}.")
|
268
234
|
end
|
235
|
+
# case value
|
236
|
+
# when String
|
237
|
+
# if response.headers[name] != value
|
238
|
+
# raise Exceptions::Validation.new(
|
239
|
+
# "Header #{name.inspect}, with value #{value.inspect} does not match #{response.headers[name]}."
|
240
|
+
# )
|
241
|
+
# end
|
242
|
+
# when Regexp
|
243
|
+
# if response.headers[name] !~ value
|
244
|
+
# raise Exceptions::Validation.new(
|
245
|
+
# "Header #{name.inspect}, with value #{value.inspect} does not match #{response.headers[name].inspect}."
|
246
|
+
# )
|
247
|
+
# end
|
248
|
+
# end
|
269
249
|
end
|
270
250
|
end
|
271
251
|
|