Class: RightDevelop::Testing::Recording::Metadata

Inherits:
Object
  • Object
show all
Defined in:
lib/right_develop/testing/recording/metadata.rb

Overview

Metadata for record and playback.

Defined Under Namespace

Classes: MissingVariableFailure, RecordingError, RetryableFailure

Constant Summary collapse

HIDDEN_CREDENTIAL_VALUE =

value used for obfuscation.

'HIDDEN_CREDENTIAL'.freeze
VERBS =

common API verbs.

%w(DELETE GET HEAD PATCH POST PUT).freeze
MODES =

valid modes, determines how variables are substituted, etc.

%w(echo record playback)
KINDS =

valid kinds, also keys under matchers.

%w(request response)
MATCHERS_KEY =

route-relative config keys.

'matchers'.freeze
SIGNIFICANT_KEY =
'significant'.freeze
TIMEOUTS_KEY =
'timeouts'.freeze
TRANSFORM_KEY =
'transform'.freeze
VARIABLES_KEY =
'variables'.freeze
VARIABLE_INDEX_REGEX =

finds the value index for a recorded variable, if any.

/\[(\d+)\]$/
HALT =

throw/catch signals.

:halt_recording_metadata_generator

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options) ⇒ Metadata

Computes the metadata used to identify where the request/response should be stored-to/retrieved-from. Recording the full request is not strictly necessary (because the request maps to a MD5 used for response-only) but it adds human-readability and the ability to manually customize some or all responses.



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/right_develop/testing/recording/metadata.rb', line 96

def initialize(options)
  unless (@logger = options[:logger])
    raise ::ArgumentError, "options[:logger] is required: #{@logger.inspect}"
  end
  unless (@mode = options[:mode].to_s) && MODES.include?(@mode)
    raise ::ArgumentError, "options[:mode] must be one of #{MODES.inspect}: #{@mode.inspect}"
  end
  unless (@kind = options[:kind].to_s) && KINDS.include?(@kind)
    raise ::ArgumentError, "options[:kind] must be one of #{KINDS.inspect}: #{@kind.inspect}"
  end
  unless (@uri = options[:uri]) && @uri.respond_to?(:path)
    raise ::ArgumentError, "options[:uri] must be a valid parsed URI: #{@uri.inspect}"
  end
  unless (@verb = options[:verb]) && VERBS.include?(@verb)
    raise ::ArgumentError, "options[:verb] must be one of #{VERBS.inspect}: #{@verb.inspect}"
  end
  unless (@headers = options[:headers]).kind_of?(::Hash)
    raise ::ArgumentError, "options[:headers] must be a hash: #{@headers.inspect}"
  end
  unless (@route_data = options[:route_data]).kind_of?(::Hash)
    raise ::ArgumentError, "options[:route_data] must be a hash: #{@route_data.inspect}"
  end
  @http_status = options[:http_status]
  if @kind == 'response'
    @http_status = Integer(@http_status)
  elsif !@http_status.nil?
    raise ::ArgumentError, "options[:http_status] is unexpected for #{@kind}."
  end
  unless (@variables = options[:variables]).kind_of?(::Hash)
    raise ::ArgumentError, "options[:variables] must be a hash: #{@variables.inspect}"
  end
  if (@effective_route_config = options[:effective_route_config]) && !@effective_route_config.kind_of?(::Hash)
    raise ::ArgumentError, "options[:effective_route_config] is not a hash: #{@effective_route_config.inspect}"
  end
  @body = options[:body]  # not required

  # merge one or more wildcard configurations matching the current uri and
  # parameters.
  @headers = normalize_headers(@headers)
  @typenames_to_values = compute_typenames_to_values

  # effective route config may already have been computed for request
  # (on record) or not (on playback).
  @effective_route_config ||= compute_effective_route_config

  # apply the configuration by substituting for variables in the request and
  # by obfuscating wherever a variable name is nil.
  case @mode
  when 'echo'
    # do nothing; used to validate the fixtures before playback, etc.
  else
    erck = @effective_route_config[@kind]
    if effective_variables = erck && erck[VARIABLES_KEY]
      recursive_replace_variables(
        [@kind, VARIABLES_KEY],
        @typenames_to_values,
        effective_variables,
        erck[TRANSFORM_KEY])
    end
    if logger.debug?
      logger.debug("#{@kind} effective_route_config = #{@effective_route_config[@kind].inspect}")
      logger.debug("#{@kind} typenames_to_values = #{@typenames_to_values.inspect}")
    end
  end

  # recreate headers and body from data using variable substitutions and
  # obfuscations.
  @headers = @typenames_to_values[:header]
  @body = normalize_body(@headers, @typenames_to_values[:body] || @body)
end

Instance Attribute Details

#bodyObject (readonly)

Returns the value of attribute body.



88
89
90
# File 'lib/right_develop/testing/recording/metadata.rb', line 88

def body
  @body
end

#effective_route_configObject (readonly)

Returns the value of attribute effective_route_config.



89
90
91
# File 'lib/right_develop/testing/recording/metadata.rb', line 89

def effective_route_config
  @effective_route_config
end

#headersObject (readonly)

Returns the value of attribute headers.



88
89
90
# File 'lib/right_develop/testing/recording/metadata.rb', line 88

def headers
  @headers
end

#http_statusObject (readonly)

Returns the value of attribute http_status.



88
89
90
# File 'lib/right_develop/testing/recording/metadata.rb', line 88

def http_status
  @http_status
end

#loggerObject (readonly)

Returns the value of attribute logger.



89
90
91
# File 'lib/right_develop/testing/recording/metadata.rb', line 89

def logger
  @logger
end

#modeObject (readonly)

Returns the value of attribute mode.



89
90
91
# File 'lib/right_develop/testing/recording/metadata.rb', line 89

def mode
  @mode
end

#uriObject (readonly)

Returns the value of attribute uri.



88
89
90
# File 'lib/right_develop/testing/recording/metadata.rb', line 88

def uri
  @uri
end

#variablesObject (readonly)

Returns the value of attribute variables.



89
90
91
# File 'lib/right_develop/testing/recording/metadata.rb', line 89

def variables
  @variables
end

#verbObject (readonly)

Returns the value of attribute verb.



88
89
90
# File 'lib/right_develop/testing/recording/metadata.rb', line 88

def verb
  @verb
end

Class Method Details

.deep_sorted_data(data) ⇒ String

Duplicates and sorts hash keys for a consistent appearance (in JSON). Traverses arrays to sort hash elements. Note this only works for Ruby 1.9+ due to hashes now preserving insertion order.



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/right_develop/testing/recording/metadata.rb', line 243

def self.deep_sorted_data(data)
  case data
  when ::Hash
    data = data.map { |k, v| [k.to_s, v] }.sort.inject({}) do |h, (k, v)|
      h[k] = deep_sorted_data(v)
      h
    end
  when Array
    data.map { |e| deep_sorted_data(e) }
  else
    if data.respond_to?(:to_hash)
      deep_sorted_data(data.to_hash)
    else
      data
    end
  end
end

.deep_sorted_json(data, pretty = false) ⇒ String

Sorts data for a consistent appearance in JSON.

HACK: replacement for ::RightSupport::Data::HashTools.deep_sorted_json method that can underflow the @state.depth field as -1 probably due to some (1.9.3+?) logic that resets the depth to zero when JSON data gets too deep or else @state.depth doesn’t mean what it used to mean in Ruby 1.8. need to fix the utility…



231
232
233
234
# File 'lib/right_develop/testing/recording/metadata.rb', line 231

def self.deep_sorted_json(data, pretty = false)
  data = deep_sorted_data(data)
  pretty ? ::JSON.pretty_generate(data) : ::JSON.dump(data)
end

.normalize_header_key(key) ⇒ String

Establishes a normal header key form for agreement between configuration and metadata pieces.



193
194
195
# File 'lib/right_develop/testing/recording/metadata.rb', line 193

def self.normalize_header_key(key)
  key.to_s.downcase.gsub('-', '_')
end

.normalize_uri(url) ⇒ URI



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/right_develop/testing/recording/metadata.rb', line 200

def self.normalize_uri(url)
  # the following logic is borrowed from RestClient::Request#parse_url
  url = "http://#{url}" unless url.match(/^http/)
  uri = ::URI.parse(url)

  # need at least a (leading) forward-slash in path for any subsequent route
  # matching.
  uri.path = '/' if uri.path.empty?

  # strip proxied server details not needed for playback.
  # strip any basic authentication, which is never recorded.
  uri = ::URI.parse(url)
  uri.scheme = nil
  uri.host = nil
  uri.port = nil
  uri.user = nil
  uri.password = nil
  uri
end

Instance Method Details

#checksumString



178
179
180
# File 'lib/right_develop/testing/recording/metadata.rb', line 178

def checksum
  @checksum ||= compute_checksum
end

#queryString



168
169
170
171
172
173
174
175
# File 'lib/right_develop/testing/recording/metadata.rb', line 168

def query
  q = @typenames_to_values[:query]
  if q && !q.empty?
    build_query_string(q)
  else
    nil
  end
end

#timeoutsHash



183
184
185
# File 'lib/right_develop/testing/recording/metadata.rb', line 183

def timeouts
  (@effective_route_config[@kind] || {})[TIMEOUTS_KEY] || {}
end