praxis 2.0.pre.11 → 2.0.pre.16
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/.ruby-version +1 -1
- data/CHANGELOG.md +22 -0
- data/bin/praxis +6 -0
- data/lib/praxis/api_definition.rb +8 -4
- data/lib/praxis/collection.rb +11 -0
- data/lib/praxis/docs/open_api/response_object.rb +21 -6
- data/lib/praxis/extensions/attribute_filtering.rb +14 -1
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +154 -63
- data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +3 -2
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +46 -43
- data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +193 -0
- data/lib/praxis/mapper/resource.rb +2 -2
- data/lib/praxis/media_type_identifier.rb +11 -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/version.rb +1 -1
- data/spec/praxis/action_definition_spec.rb +3 -1
- data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +259 -172
- data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +25 -6
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +117 -19
- data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +148 -0
- data/spec/praxis/mapper/resource_spec.rb +3 -3
- data/spec/praxis/media_type_identifier_spec.rb +15 -1
- data/spec/praxis/response_definition_spec.rb +37 -129
- 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 -6
@@ -3,7 +3,8 @@ module Praxis
|
|
3
3
|
module AttributeFiltering
|
4
4
|
class FilterTreeNode
|
5
5
|
attr_reader :path, :conditions, :children
|
6
|
-
#
|
6
|
+
# Parsed_filters is an Array of {name: X, op: Y, value: Z} ... exactly the format of the FilteringParams.load method
|
7
|
+
# It can also contain a :node_object
|
7
8
|
def initialize(parsed_filters, path: [])
|
8
9
|
@path = path # Array that marks the tree 'path' to this node (with respect to the absolute root)
|
9
10
|
@conditions = [] # Conditions to apply directly to this node
|
@@ -14,7 +15,7 @@ module Praxis
|
|
14
15
|
if components.empty?
|
15
16
|
return
|
16
17
|
elsif components.size == 1
|
17
|
-
@conditions << hash.slice(:name, :op, :value)
|
18
|
+
@conditions << hash.slice(:name, :op, :value, :fuzzy, :node_object)
|
18
19
|
else
|
19
20
|
children_data[components.first] ||= []
|
20
21
|
children_data[components.first] << hash
|
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
2
|
+
require 'praxis/extensions/attribute_filtering/filters_parser'
|
3
|
+
|
3
4
|
#
|
4
|
-
# Attributor type to define and
|
5
|
+
# Attributor type to define and handle the language to express filtering attributes in listings.
|
5
6
|
# Commonly used in a query string parameter value for listing calls.
|
6
7
|
#
|
7
8
|
# The type allows you to restrict the allowable fields (and their types) based on an existing Mediatype.
|
@@ -14,7 +15,7 @@
|
|
14
15
|
# attribute :filters,
|
15
16
|
# Types::FilteringParams.for(MediaTypes::MyType) do
|
16
17
|
# filter 'user.id', using: ['=', '!=']
|
17
|
-
# filter 'name', using: ['=', '!=']
|
18
|
+
# filter 'name', using: ['=', '!=', '!', '!!]
|
18
19
|
# filter 'children.created_at', using: ['>', '>=', '<', '<=']
|
19
20
|
# filter 'display_name', using: ['=', '!='], fuzzy: true
|
20
21
|
# end
|
@@ -26,6 +27,8 @@ module Praxis
|
|
26
27
|
include Attributor::Type
|
27
28
|
include Attributor::Dumpable
|
28
29
|
|
30
|
+
attr_reader :parsed_array
|
31
|
+
|
29
32
|
class DSLCompiler < Attributor::DSLCompiler
|
30
33
|
# "account.id": { operators: ["=", "!="] },
|
31
34
|
# name: { operators: ["=", "!="], fuzzy_match: true },
|
@@ -36,9 +39,9 @@ module Praxis
|
|
36
39
|
end
|
37
40
|
end
|
38
41
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
+
VALUE_OPERATORS = Set.new(['!=', '>=', '<=', '=', '<', '>']).freeze
|
43
|
+
NOVALUE_OPERATORS = Set.new(['!','!!']).freeze
|
44
|
+
AVAILABLE_OPERATORS = Set.new(VALUE_OPERATORS+NOVALUE_OPERATORS).freeze
|
42
45
|
|
43
46
|
# Abstract class, which needs to be used by subclassing it through the .for method, to set the allowed filters
|
44
47
|
# definition should be a hash, keyed by field name, which contains a hash that can have two pieces of metadata
|
@@ -52,7 +55,7 @@ module Praxis
|
|
52
55
|
def for(media_type, **_opts)
|
53
56
|
unless media_type < Praxis::MediaType
|
54
57
|
raise ArgumentError, "Invalid type: #{media_type.name} for Filters. " \
|
55
|
-
'
|
58
|
+
'Using the .for method for defining a filter, requires passing a subclass of a MediaType'
|
56
59
|
end
|
57
60
|
|
58
61
|
::Class.new(self) do
|
@@ -77,9 +80,7 @@ module Praxis
|
|
77
80
|
}
|
78
81
|
end
|
79
82
|
end
|
80
|
-
|
81
|
-
attr_reader :parsed_array
|
82
|
-
|
83
|
+
|
83
84
|
def self.native_type
|
84
85
|
self
|
85
86
|
end
|
@@ -134,11 +135,15 @@ module Praxis
|
|
134
135
|
raise "filter with name #{filter_name} does not correspond to an existing field inside " \
|
135
136
|
" MediaType #{media_type.name}"
|
136
137
|
end
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
138
|
+
if NOVALUE_OPERATORS.include?(op)
|
139
|
+
arr << "#{filter_name}#{op}" # Do not add a value for the operators that don't take it
|
140
|
+
else
|
141
|
+
attr_example = filter_components.inject(mt_example) do |last, name|
|
142
|
+
# we can safely do sends, since we've verified the components are valid
|
143
|
+
last.send(name)
|
144
|
+
end
|
145
|
+
arr << "#{filter_name}#{op}#{attr_example}"
|
146
|
+
end
|
142
147
|
end.join('&')
|
143
148
|
else
|
144
149
|
'name=Joe&date>2017-01-01'
|
@@ -153,34 +158,33 @@ module Praxis
|
|
153
158
|
|
154
159
|
def self.load(filters, _context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
|
155
160
|
return filters if filters.is_a?(native_type)
|
156
|
-
return new if filters.nil?
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
161
|
+
return new if filters.nil? || filters.blank?
|
162
|
+
|
163
|
+
parsed = Parser.new.parse(filters)
|
164
|
+
|
165
|
+
tree = ConditionGroup.load(parsed)
|
166
|
+
|
167
|
+
rr = tree.flattened_conditions
|
168
|
+
accum = []
|
169
|
+
rr.each do |spec|
|
170
|
+
attr_name = spec[:name]
|
171
|
+
# TODO: Do we need to CGI.unescape things? here or even before??...
|
172
|
+
coerced = \
|
173
|
+
if media_type
|
174
|
+
filter_components = attr_name.to_s.split('.').map(&:to_sym)
|
175
|
+
attr, _enclosing_type = find_filter_attribute(filter_components, media_type)
|
176
|
+
if spec[:values].is_a?(Array)
|
177
|
+
attr_coll = Attributor::Collection.of(attr.type)
|
178
|
+
attr_coll.load(spec[:values])
|
179
|
+
else
|
180
|
+
attr.load(spec[:values])
|
181
|
+
end
|
175
182
|
else
|
176
|
-
|
183
|
+
spec[:values]
|
177
184
|
end
|
178
|
-
|
179
|
-
value
|
180
|
-
end
|
181
|
-
arr.push(name: attr_name, op: match[:operator], value: coerced )
|
185
|
+
accum.push(name: attr_name, op: spec[:op], value: coerced , fuzzy: spec[:fuzzies], node_object: spec[:node_object])
|
182
186
|
end
|
183
|
-
new(
|
187
|
+
new(accum)
|
184
188
|
end
|
185
189
|
|
186
190
|
def self.dump(value, **_opts)
|
@@ -218,10 +222,9 @@ module Praxis
|
|
218
222
|
value_type = attr_filters[:value_type]
|
219
223
|
next unless value_type == Attributor::String
|
220
224
|
|
221
|
-
|
222
|
-
unless value.empty?
|
225
|
+
if item[:value].presence
|
223
226
|
fuzzy_match = attr_filters[:fuzzy_match]
|
224
|
-
if
|
227
|
+
if item[:fuzzy] && !item[:fuzzy].empty? && !fuzzy_match
|
225
228
|
errors << "Fuzzy matching for #{attr_name} is not allowed (yet '*' was found in the value)"
|
226
229
|
end
|
227
230
|
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
|
3
|
+
module Praxis
|
4
|
+
module Extensions
|
5
|
+
module AttributeFiltering
|
6
|
+
class FilteringParams
|
7
|
+
class Condition
|
8
|
+
attr_reader :name, :op, :values
|
9
|
+
attr_accessor :parent_group
|
10
|
+
|
11
|
+
# For operands with a single or no values: Incoming data is a hash with name and op
|
12
|
+
# For Operands with multiple values: Incoming data is an array of hashes
|
13
|
+
# First hash has the spec (i.e., name and op)
|
14
|
+
# The rest of the hashes contain a value each (a hash with value: X each).
|
15
|
+
# Example: [{:name=>"multi"@0, :op=>"="@5}, {:value=>"1"@6}, {:value=>"2"@8}]
|
16
|
+
def initialize(triad:, parent_group:)
|
17
|
+
@parent_group = parent_group
|
18
|
+
if triad.is_a? Array # several values coming in
|
19
|
+
spec, *values = triad
|
20
|
+
@name = spec[:name].to_sym
|
21
|
+
@op = spec[:op].to_s
|
22
|
+
|
23
|
+
if values.empty?
|
24
|
+
@values = ""
|
25
|
+
@fuzzies = nil
|
26
|
+
elsif values.size == 1
|
27
|
+
raw_val = values.first[:value].to_s
|
28
|
+
@values, @fuzzies = _compute_fuzzy(values.first[:value].to_s)
|
29
|
+
else
|
30
|
+
@values = []
|
31
|
+
@fuzzies = []
|
32
|
+
results = values.each do|e|
|
33
|
+
val, fuz = _compute_fuzzy(e[:value].to_s)
|
34
|
+
@values.push val
|
35
|
+
@fuzzies.push fuz
|
36
|
+
end
|
37
|
+
end
|
38
|
+
else # No values for the operand
|
39
|
+
@name = triad[:name].to_sym
|
40
|
+
@op = triad[:op].to_s
|
41
|
+
if ['!','!!'].include?(@op)
|
42
|
+
@values, @fuzzies = [nil, nil]
|
43
|
+
else
|
44
|
+
# Value operand without value? => convert it to empty string
|
45
|
+
raise "Interesting, didn't know this could happen. Oops!" if triad[:value].is_a?(Array) && !triad[:value].empty?
|
46
|
+
if triad[:value] == []
|
47
|
+
@values, @fuzzies = ['', nil]
|
48
|
+
else
|
49
|
+
@values, @fuzzies = _compute_fuzzy(triad[:value].to_s)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
# Takes a raw val, and spits out the output val (unescaped), and the fuzzy definition
|
55
|
+
def _compute_fuzzy(raw_val)
|
56
|
+
starting = raw_val[0] == '*'
|
57
|
+
ending = raw_val[-1] == '*'
|
58
|
+
newval, fuzzy = if starting && ending
|
59
|
+
[raw_val[1..-2], :start_end]
|
60
|
+
elsif starting
|
61
|
+
[raw_val[1..-1], :start]
|
62
|
+
elsif ending
|
63
|
+
[raw_val[0..-2], :end]
|
64
|
+
else
|
65
|
+
[raw_val,nil]
|
66
|
+
end
|
67
|
+
newval = CGI.unescape(newval) if newval
|
68
|
+
[newval,fuzzy]
|
69
|
+
end
|
70
|
+
def flattened_conditions
|
71
|
+
[{name: @name, op: @op, values: @values, fuzzies: @fuzzies, node_object: self}]
|
72
|
+
end
|
73
|
+
|
74
|
+
# Dumps the value, marking where the fuzzy might be, and removing the * to differentiate from literals
|
75
|
+
def _dump_value(val,fuzzy)
|
76
|
+
case fuzzy
|
77
|
+
when nil
|
78
|
+
val
|
79
|
+
when :start_end
|
80
|
+
'{*}' + val + '{*}'
|
81
|
+
when :start
|
82
|
+
'{*}' + val
|
83
|
+
when :end
|
84
|
+
val +'{*}'
|
85
|
+
end
|
86
|
+
end
|
87
|
+
def dump
|
88
|
+
vals = if values.is_a? Array
|
89
|
+
dumped = values.map.with_index{|val,i| _dump_value(val, @fuzzies[i])}
|
90
|
+
"[#{dumped.join(',')}]" # Purposedly enclose in brackets to make sure we differentiate
|
91
|
+
else
|
92
|
+
(values == '') ? '""' : _dump_value(values,@fuzzies) # Dump the empty string explicitly with quotes if we've converted no value to empty string
|
93
|
+
end
|
94
|
+
"#{name}#{op}#{vals}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# An Object that represents an AST tree for either an OR or an AND conditions
|
99
|
+
# to be applied to its items children
|
100
|
+
class ConditionGroup
|
101
|
+
attr_reader :items, :type
|
102
|
+
attr_accessor :parent_group
|
103
|
+
attr_accessor :associated_query # Metadata to be used by whomever is manipulating this
|
104
|
+
|
105
|
+
def self.load(node)
|
106
|
+
unless node[:o]
|
107
|
+
loaded = Condition.new(triad: node[:triad], parent_group: nil)
|
108
|
+
else
|
109
|
+
compactedl = compress_tree(node: node[:l], op: node[:o])
|
110
|
+
compactedr = compress_tree(node: node[:r], op: node[:o])
|
111
|
+
compacted = {op: node[:o], items: compactedl + compactedr }
|
112
|
+
|
113
|
+
loaded = ConditionGroup.new(**compacted, parent_group: nil)
|
114
|
+
end
|
115
|
+
loaded
|
116
|
+
end
|
117
|
+
|
118
|
+
def initialize(op:, items:, parent_group:)
|
119
|
+
@type = (op.to_s == '&') ? :and : :or
|
120
|
+
@items = items.map do |item|
|
121
|
+
if item[:op]
|
122
|
+
ConditionGroup.new(**item, parent_group: self)
|
123
|
+
else
|
124
|
+
Condition.new(triad: item[:triad], parent_group: self)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
@parent_group = parent_group
|
128
|
+
end
|
129
|
+
|
130
|
+
def dump
|
131
|
+
"( " + @items.map(&:dump).join(" #{type.upcase} ") + " )"
|
132
|
+
end
|
133
|
+
|
134
|
+
# Returns an array with flat conditions from all child triad conditions
|
135
|
+
def flattened_conditions
|
136
|
+
@items.inject([]) do |accum, item|
|
137
|
+
accum + item.flattened_conditions
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Given a binary tree of operand conditions, transform it to a multi-leaf tree
|
142
|
+
# where a single condition node has potentially multiple subtrees for the same operation (instead of 2)
|
143
|
+
# For example (&, (&, a, b), (|, c, d)) => (&, a, b, (|, c, d))
|
144
|
+
def self.compress_tree(node:, op:)
|
145
|
+
if node[:triad]
|
146
|
+
return [node]
|
147
|
+
end
|
148
|
+
|
149
|
+
# It is an op node
|
150
|
+
if node[:o] == op
|
151
|
+
# compatible op as parent, collect my compacted children and return them up skipping my op
|
152
|
+
resultl = compress_tree(node: node[:l], op: op)
|
153
|
+
resultr = compress_tree(node: node[:r], op: op)
|
154
|
+
resultl+resultr
|
155
|
+
else
|
156
|
+
collected = compress_tree(node: node, op: node[:o])
|
157
|
+
[{op: node[:o], items: collected }]
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
class Parser < Parslet::Parser
|
163
|
+
root :expression
|
164
|
+
rule(:lparen) { str('(') }
|
165
|
+
rule(:rparen) { str(')') }
|
166
|
+
rule(:comma) { str(',') }
|
167
|
+
rule(:val_operator) { str('!=') | str('>=') | str('<=') | str('=') | str('<') | str('>')}
|
168
|
+
rule(:noval_operator) { str('!!') | str('!')}
|
169
|
+
rule(:and_kw) { str('&') }
|
170
|
+
rule(:or_kw) { str('|') }
|
171
|
+
|
172
|
+
def infix *args
|
173
|
+
Infix.new(*args)
|
174
|
+
end
|
175
|
+
|
176
|
+
rule(:name) { match('[a-zA-Z0-9_\.]').repeat(1) } # TODO: are these the only characters that we allow for names?
|
177
|
+
rule(:chars) { match('[^&|(),]').repeat(0).as(:value) }
|
178
|
+
rule(:value) { chars >> (comma >> chars ).repeat }
|
179
|
+
|
180
|
+
rule(:triad) {
|
181
|
+
(name.as(:name) >> val_operator.as(:op) >> value).as(:triad) |
|
182
|
+
(name.as(:name) >> noval_operator.as(:op)).as(:triad) |
|
183
|
+
lparen >> expression >> rparen
|
184
|
+
}
|
185
|
+
|
186
|
+
rule(:expression) {
|
187
|
+
infix_expression(triad, [and_kw, 2, :left], [or_kw, 1, :right])
|
188
|
+
}
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -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!
|
@@ -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
|
@@ -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
|
|