Class: RightDevelop::Testing::Recording::Config

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

Overview

Config file format.

Defined Under Namespace

Classes: ConfigError

Constant Summary collapse

FIXTURES_DIR_NAME =

default relative directories.

'fixtures'.freeze
LOG_DIR_NAME =
'log'.freeze
PID_DIR_NAME =
'pid'.freeze
STOP_TRAVERSAL_KEY =

the empty key is used as a stop traversal signal because any literal value would be ambiguous.

''.freeze
VALID_MODES =
RightSupport::Data::Mash.new(
  :admin    => 'Administrative for changing mode, fixtures, etc. while running.',
  :echo     => 'Echoes request back as response and validates route.',
  :playback => 'Playback a session for one or more stubbed web services.',
  :record   => 'Record a session for one or more proxied web services.'
).freeze
ALLOWED_KINDS =

keys allowed under the deep route configuration.

%w(request response)
ALLOWED_CONFIG_ACTIONS =
%w(delay_seconds significant timeouts transform variables)
ALLOWED_TIMEOUTS =
%w(open_timeout read_timeout)
ALLOWED_VARIABLE_TYPES =
%w(body header query)
METADATA_CLASS =

metadata.

::RightDevelop::Testing::Recording::Metadata
FIXTURE_FILE_NAME_REGEX =

patterns for fixture files.

/^([0-9A-Fa-f]{32}).yml$/
TYPE_NAME_VALUE_REGEX =

typename to value expression for significant/requests/responses configurations.

/^(body|header|query|verb)(?:[:#]([^=]+))?=(.*)$/

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config_hash, options = nil) ⇒ Config

Returns a new instance of Config.



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/right_develop/testing/recording/config.rb', line 71

def initialize(config_hash, options = nil)
  # defaults.
  current_dir = ::Dir.pwd
  defaults = RightSupport::Data::Mash.new(
    'fixtures_dir' => ::File.expand_path(FIXTURES_DIR_NAME, current_dir),
    'log_level'    => :info,
    'log_dir'      => ::File.expand_path(LOG_DIR_NAME, current_dir),
    'pid_dir'      => ::File.expand_path(PID_DIR_NAME, current_dir),
    'throttle'     => 1,
  )
  unless config_hash.kind_of?(::Hash)
    raise ConfigError, 'config_hash must be a hash'
  end

  # shallow merge of hash because the defaults are a shallow hash. deep mash
  # of caller's config to deep clone and normalize keys.
  config_hash = defaults.merge(deep_mash(config_hash))
  if options
    # another deep merge of any additional options.
    ::RightSupport::Data::HashTools.deep_merge!(config_hash, options)
  end
  @config_hash = RightSupport::Data::Mash.new
  mode(config_hash['mode'])
  admin(config_hash['admin'])
  routes(config_hash['routes'])
  log_dir(config_hash['log_dir'])
  pid_dir(config_hash['pid_dir'])
  log_level(config_hash['log_level'])
  fixtures_dir(config_hash['fixtures_dir'])
  cleanup_dirs(config_hash['cleanup_dirs'])
  throttle(config_hash['throttle'])
end

Class Method Details

.deep_mash(any) ⇒ Object

Deeply mashes and duplicates (clones) a hash containing other hashes or arrays of hashes but not other types.

Note that Mash.new(my_mash) will convert child hashes to mashes but not with the guarantee of cloning and detaching the deep mash. In other words. if any part of the hash is already a mash then it is not cloned by invoking Mash.new()

now delegates to RightSupport::Data::HashTools

Returns:

  • (Object)

    depends on input type



294
295
296
# File 'lib/right_develop/testing/recording/config.rb', line 294

def self.deep_mash(any)
  ::RightSupport::Data::HashTools.deep_mash(any)
end

.from_file(path, options = nil) ⇒ Config

Loads the config from given path or a relative location.

Parameters:

  • path (String)

    to configuration

  • options (Hash) (defaults to: nil)

    to merge after loading config hash

Returns:

  • (Config)

    configuration object

Raises:

  • (ArgumentError)

    on failure to load



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/right_develop/testing/recording/config.rb', line 243

def self.from_file(path, options = nil)
  # load
  unless ::File.file?(path)
    raise ConfigError, "Missing expected configuration file: #{path.inspect}"
  end
  config_hash = deep_mash(::YAML.load_file(path))

  # enumerate routes looking for any route-specific config data to load
  # into the config hash from .yml files in subdirectories. this allows
  # the user to spread configuration of specific requests/responses out in
  # the file system instead of having a single monster config .yml
  extension = '.yml'
  (config_hash[:routes] || {}).each do |route_path, route_data|
    if subdir = route_data[:subdir]
      route_subdir = ::File.expand_path(::File.join(path, '..', subdir))
      ::Dir[::File.join(route_subdir, "**/*#{extension}")].each do |route_config_path|
        route_config_data = RightSupport::Data::Mash.new(::YAML.load_file(route_config_path))
        filename = ::File.basename(route_config_path)[0..-(extension.length + 1)]
        hash_path = ::File.dirname(route_config_path)[(route_subdir.length + 1)..-1].split('/')
        unless current_route_data = ::RightSupport::Data::HashTools.deep_get(route_data, hash_path)
          current_route_data = RightSupport::Data::Mash.new
          ::RightSupport::Data::HashTools.deep_set!(route_data, hash_path, current_route_data)
        end

        # inject a 'stop' at the point where the sub-config file data was
        # inserted into the big hash. the 'stop' basically distingishes the
        # 'directory' from the 'file' information because the hash doesn't
        # use classes to distinguish the data it contains; it only uses
        # simple types. use of simple types makes it easy to YAMLize or
        # JSONize or otherwise serialize in round-trip fashion.
        merge_data = { filename => { STOP_TRAVERSAL_KEY => route_config_data } }
        ::RightSupport::Data::HashTools.deep_merge!(current_route_data, merge_data)
      end
    end
  end

  # config
  self.new(config_hash, options)
end

Instance Method Details

#admin(value = nil) ⇒ Hash

Returns admin route configuration.

Returns:

  • (Hash)

    admin route configuration



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
# File 'lib/right_develop/testing/recording/config.rb', line 137

def admin(value = nil)
  if value
    if mode == :admin
      case value
      when ::Hash
        admin_routes = (value['routes'] || {}).inject({}) do |r, (k, v)|
          case v
          when ::String, ::Symbol
            r[normalize_route_prefix(k)] = v.to_s.to_sym
          else
            raise ConfigError, "Invalid admin route target: #{v.inspect}"
          end
          r
        end
        if admin_routes.empty?
          raise ConfigError, "Invalid admin routes: #{value['routes'].inspect}"
        else
          @config_hash['admin'] = deep_mash(routes: admin_routes)
        end
      else
        raise ConfigError,
              "Unexpected type for admin configuration: #{value.class}"
      end
    else
      raise ConfigError,
            "Unexpected admin settings configured for non-admin mode: #{mode}"
    end
  end
  @config_hash['admin'] || {}
end

#cleanup_dirs(value = nil) ⇒ TrueClass|FalseClass

Returns true if cleaning-up fixtures directory on interrupt or configuration change.

Returns:

  • (TrueClass|FalseClass)

    true if cleaning-up fixtures directory on interrupt or configuration change.



118
119
120
121
# File 'lib/right_develop/testing/recording/config.rb', line 118

def cleanup_dirs(value = nil)
  @config_hash['cleanup_dirs'] = Array(value) if value
  @config_hash['cleanup_dirs']
end

#fixtures_dir(value = nil) ⇒ String

Returns location of fixtures used for record/playback.

Returns:

  • (String)

    location of fixtures used for record/playback



111
112
113
114
# File 'lib/right_develop/testing/recording/config.rb', line 111

def fixtures_dir(value = nil)
  @config_hash['fixtures_dir'] = value if value
  @config_hash['fixtures_dir']
end

#log_dir(value = nil) ⇒ Object



214
215
216
217
# File 'lib/right_develop/testing/recording/config.rb', line 214

def log_dir(value = nil)
  @config_hash['log_dir'] = value if value
  @config_hash['log_dir']
end

#log_level(value = nil) ⇒ Object



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/right_develop/testing/recording/config.rb', line 197

def log_level(value = nil)
  if value
    case value
    when Integer
      if value < ::Logger::DEBUG || value >= ::Logger::UNKNOWN
        raise ConfigError, "log_level is out of range: #{value}"
      end
      @config_hash['log_level'] = value
    when String, Symbol
      @config_hash['log_level'] = ::Logger.const_get(value.to_s.upcase)
    else
      raise ConfigError, "log_level is unexpected type: #{log_level}"
    end
  end
  @config_hash['log_level']
end

#mode(value = nil) ⇒ Object



123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/right_develop/testing/recording/config.rb', line 123

def mode(value = nil)
  if value
    value = value.to_s
    if VALID_MODES.has_key?(value)
      @config_hash['mode'] = value.to_sym
    else
      raise ConfigError,
            "mode must be one of #{VALID_MODES.keys.sort.inspect}: #{value.inspect}"
    end
  end
  @config_hash['mode']
end

#pid_dir(value = nil) ⇒ Object



219
220
221
222
# File 'lib/right_develop/testing/recording/config.rb', line 219

def pid_dir(value = nil)
  @config_hash['pid_dir'] = value if value
  @config_hash['pid_dir']
end

#routes(value = nil) ⇒ Object



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/right_develop/testing/recording/config.rb', line 168

def routes(value = nil)
  if value
    case value
    when Hash
      # admin mode requires any playback/record config to be sent as a
      # PUT/POST request to the configured admin route.
      if mode == :admin
        raise ConfigError, 'Preconfigured routes are not allowed in admin mode.'
      end

      # normalize routes for efficient usage but keep them separate from
      # user's config so that .to_hash returns something understandable and
      # JSONizable/YAMLable.
      mutable_routes = value.inject(RightSupport::Data::Mash.new) do |r, (k, v)|
        r[normalize_route_prefix(k)] = normalize_route_data(k, v)
        r
      end

      # deep freeze routes to detect any case where code is unintentionally
      # modifying the route hash.
      @normalized_routes = ::RightSupport::Data::HashTools.deep_freeze!(mutable_routes)
      @config_hash['routes'] = ::RightSupport::Data::HashTools.deep_clone2(value)
    else
      raise ConfigError, 'routes must be a hash'
    end
  end
  @normalized_routes || {}
end

#throttle(value = nil) ⇒ Object



224
225
226
227
228
229
230
231
232
233
# File 'lib/right_develop/testing/recording/config.rb', line 224

def throttle(value = nil)
  if value
    value = Integer(value)
    if value < 0 || value > 100
      raise ConfigError, "throttle is out of range: #{value}"
    end
    @config_hash['throttle'] = value
  end
  @config_hash['throttle']
end

#to_hashString

Returns raw hash representing complete configuration.

Returns:

  • (String)

    raw hash representing complete configuration



105
106
107
108
# File 'lib/right_develop/testing/recording/config.rb', line 105

def to_hash
  # unmash to hash
  ::JSON.load(@config_hash.to_json)
end