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: ConfigurationError, MissingVariableFailure, PlaybackError, 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 validate)
KINDS =

valid kinds, also keys under matchers.

%w(request response)
DELAY_SECONDS_KEY =

route-relative config keys.

'delay_seconds'.freeze
MATCHERS_KEY =
'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.



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
166
167
168
169
170
# File 'lib/right_develop/testing/recording/metadata.rb', line 100

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.
  erck = @effective_route_config[@kind]
  case @mode
  when 'validate'
    # used to validate the fixtures before playback; no variable
    # substitution should be performed.
  else
    if effective_variables = erck && erck[VARIABLES_KEY]
      recursive_replace_variables(
        [@kind, VARIABLES_KEY],
        @typenames_to_values,
        effective_variables,
        erck[TRANSFORM_KEY])
    end
  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

  # 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.



91
92
93
# File 'lib/right_develop/testing/recording/metadata.rb', line 91

def body
  @body
end

#checksum_dataObject (readonly)

Returns the value of attribute checksum_data.



91
92
93
# File 'lib/right_develop/testing/recording/metadata.rb', line 91

def checksum_data
  @checksum_data
end

#effective_route_configObject (readonly)

Returns the value of attribute effective_route_config.



92
93
94
# File 'lib/right_develop/testing/recording/metadata.rb', line 92

def effective_route_config
  @effective_route_config
end

#headersObject (readonly)

Returns the value of attribute headers.



91
92
93
# File 'lib/right_develop/testing/recording/metadata.rb', line 91

def headers
  @headers
end

#http_statusObject (readonly)

Returns the value of attribute http_status.



91
92
93
# File 'lib/right_develop/testing/recording/metadata.rb', line 91

def http_status
  @http_status
end

#loggerObject (readonly)

Returns the value of attribute logger.



92
93
94
# File 'lib/right_develop/testing/recording/metadata.rb', line 92

def logger
  @logger
end

#modeObject (readonly)

Returns the value of attribute mode.



92
93
94
# File 'lib/right_develop/testing/recording/metadata.rb', line 92

def mode
  @mode
end

#typenames_to_valuesObject (readonly)

Returns the value of attribute typenames_to_values.



93
94
95
# File 'lib/right_develop/testing/recording/metadata.rb', line 93

def typenames_to_values
  @typenames_to_values
end

#uriObject (readonly)

Returns the value of attribute uri.



91
92
93
# File 'lib/right_develop/testing/recording/metadata.rb', line 91

def uri
  @uri
end

#variablesObject (readonly)

Returns the value of attribute variables.



92
93
94
# File 'lib/right_develop/testing/recording/metadata.rb', line 92

def variables
  @variables
end

#verbObject (readonly)

Returns the value of attribute verb.



91
92
93
# File 'lib/right_develop/testing/recording/metadata.rb', line 91

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.

Parameters:

  • data (Hash|Array)

    to deep-sort

Returns:

  • (String)

    sorted data



254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/right_develop/testing/recording/metadata.rb', line 254

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…

Parameters:

  • data (Hash|Array)

    to JSONize

Returns:

  • (String)

    sorted JSON



242
243
244
245
# File 'lib/right_develop/testing/recording/metadata.rb', line 242

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.

Parameters:

  • key (String|Symbol)

    to normalize

Returns:

  • (String)

    normalized key



204
205
206
# File 'lib/right_develop/testing/recording/metadata.rb', line 204

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

.normalize_uri(url) ⇒ URI

Returns uri with scheme inserted if necessary.

Parameters:

  • url (String)

    to normalize

Returns:

  • (URI)

    uri with scheme inserted if necessary



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/right_develop/testing/recording/metadata.rb', line 211

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

Returns computed checksum for normalized ‘significant’ data.

Returns:

  • (String)

    computed checksum for normalized ‘significant’ data



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

def checksum
  @checksum ||= compute_checksum
end

#delay_secondsFloat

Returns delay in seconds (of response) from effective configuration or empty.

Returns:

  • (Float)

    delay in seconds (of response) from effective configuration or empty



189
190
191
# File 'lib/right_develop/testing/recording/metadata.rb', line 189

def delay_seconds
  Float((@effective_route_config[@kind] || {})[DELAY_SECONDS_KEY] || 0)
end

#queryString

Returns normalized query string.

Returns:

  • (String)

    normalized query string



173
174
175
176
177
178
179
180
# File 'lib/right_develop/testing/recording/metadata.rb', line 173

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

#timeoutsHash

Returns timeouts from effective configuration or empty.

Returns:

  • (Hash)

    timeouts from effective configuration or empty



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

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