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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +22 -0
  4. data/bin/praxis +6 -0
  5. data/lib/praxis/api_definition.rb +8 -4
  6. data/lib/praxis/collection.rb +11 -0
  7. data/lib/praxis/docs/open_api/response_object.rb +21 -6
  8. data/lib/praxis/extensions/attribute_filtering.rb +14 -1
  9. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +154 -63
  10. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +3 -2
  11. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +46 -43
  12. data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +193 -0
  13. data/lib/praxis/mapper/resource.rb +2 -2
  14. data/lib/praxis/media_type_identifier.rb +11 -1
  15. data/lib/praxis/response_definition.rb +46 -66
  16. data/lib/praxis/responses/http.rb +3 -1
  17. data/lib/praxis/tasks/routes.rb +6 -6
  18. data/lib/praxis/version.rb +1 -1
  19. data/spec/praxis/action_definition_spec.rb +3 -1
  20. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +259 -172
  21. data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +25 -6
  22. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +117 -19
  23. data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +148 -0
  24. data/spec/praxis/mapper/resource_spec.rb +3 -3
  25. data/spec/praxis/media_type_identifier_spec.rb +15 -1
  26. data/spec/praxis/response_definition_spec.rb +37 -129
  27. data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +33 -0
  28. data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +4 -0
  29. data/tasks/thor/templates/generator/example_app/config/environment.rb +1 -1
  30. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +2 -2
  31. 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
- # # parsed_filters is an Array of {name: X, op: , value: } ... exactly the format of the FilteringParams.load method
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
- # rubocop:disable all
2
+ require 'praxis/extensions/attribute_filtering/filters_parser'
3
+
3
4
  #
4
- # Attributor type to define and handlea simple language to express filtering attributes in listings.
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
- VALUE_REGEX = /[^,&]*/
40
- AVAILABLE_OPERATORS = Set.new(['!=', '>=', '<=', '=', '<', '>','!','!!']).freeze
41
- FILTER_REGEX = /(?<attribute>([^=!><])+)(?<operator>!=|>=|<=|!!|=|<|>|!)(?<value>#{VALUE_REGEX}(,#{VALUE_REGEX})*)/
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
- 'Must be a subclass of MediaType'
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
- attr_example = filter_components.inject(mt_example) do |last, name|
138
- # we can safely do sends, since we've verified the components are valid
139
- last.send(name)
140
- end
141
- arr << "#{filter_name}#{op}#{attr_example}"
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
- parsed = filters.split('&').each_with_object([]) do |filter_string, arr|
158
- match = FILTER_REGEX.match(filter_string)
159
- values = CGI.unescape(match[:value]).split(',')
160
- value = if values.size > 1
161
- multimatch = true
162
- values
163
- else
164
- multimatch = false
165
- values.first
166
- end
167
-
168
- attr_name = match[:attribute].to_sym
169
- coerced = if media_type
170
- filter_components = attr_name.to_s.split('.').map(&:to_sym)
171
- attr, _enclosing_type = find_filter_attribute(filter_components, media_type)
172
- if multimatch
173
- attr_coll = Attributor::Collection.of(attr.type)
174
- attr_coll.load(value)
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
- attr.load(value)
183
+ spec[:values]
177
184
  end
178
- else
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(parsed)
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
- value = item[:value]
222
- unless value.empty?
225
+ if item[:value].presence
223
226
  fuzzy_match = attr_filters[:fuzzy_match]
224
- if (value[-1] == '*' || value[0] == '*') && !fuzzy_match
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, **options)
50
- self.properties[name] = options
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
- obj.suffix = suffix || self.suffix || ''
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 @spec[:location] if loc.nil?
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
- def headers(hdrs = nil)
69
- return @spec[:headers] if hdrs.nil?
61
+ header('Location', loc, description: description)
62
+ end
70
63
 
71
- case hdrs
72
- when Array
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(hdr)
86
- case hdr
87
- when String
88
- @spec[:headers][hdr] = true
89
- when Hash
90
- hdr.each do | k, v |
91
- unless v.is_a?(Regexp) || v.is_a?(String)
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 a String (to match the name) or" +
101
- " a Hash (to match both the name and the value). Received: #{hdr.inspect}"
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
- data_type = data.is_a?(Regexp) ? :regexp : :string
177
- data_value = data.is_a?(Regexp) ? data.inspect : data
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
- case value
256
- when String
257
- if response.headers[name] != value
258
- raise Exceptions::Validation.new(
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