Class: EnvParser

Inherits:
Object
  • Object
show all
Defined in:
lib/env_parser.rb,
lib/env_parser/errors.rb,
lib/env_parser/version.rb

Overview

The EnvParser class simplifies parsing of environment variables as different data types.

Defined Under Namespace

Modules: Types Classes: AutoregisterFileNotFound, Error, TypeAlreadyDefinedError, UnknownTypeError, UnparseableAutoregisterSpec, ValueNotAllowedError, ValueNotConvertibleError

Constant Summary collapse

AUTOREGISTER_FILE =

The default filename to use for autoregister requests.

'.env_parser.yml'.freeze
VERSION =
'1.6.1'.freeze

Class Method Summary collapse

Class Method Details

.add_env_bindingsENV

Creates ENV bindings for parse and register proxy methods.

The sole difference between these proxy methods and their EnvParser counterparts is that ENV.parse will interpret any value given as an ENV key (as a String), not the given value itself. i.e. ENV.parse(‘XYZ’, …) is equivalent to EnvParser.parse(ENV[‘XYZ’], …)

Returns:

  • (ENV)

    This generates no usable value.



232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/env_parser.rb', line 232

def add_env_bindings
  ENV.instance_eval do
    def parse(name, options = {}, &validation_block)
      EnvParser.parse(self[name.to_s], options, &validation_block)
    end

    def register(*args)
      EnvParser.register(*args)
    end
  end

  ENV
end

.autoregister(filename = nil) ⇒ Hash

Reads an “autoregister” file and registers the ENV constants defined therein.

The “autoregister” file is read, parsed as YAML, sanitized for use as a parameter to register_all, and then passed along for processing. The return value from that register_all call is passed through.

Parameters:

  • filename (String) (defaults to: nil)

    A path for the autoregister file to parse and process. Defaults to AUTOREGISTER_FILE if unset.

Returns:

  • (Hash)

    The return value from the register_all call that handles the actual registration.

Raises:



261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/env_parser.rb', line 261

def autoregister(filename = nil)
  filename ||= AUTOREGISTER_FILE
  autoregister_spec = Psych.load_file(filename)

  autoregister_spec.deep_symbolize_keys!
  autoregister_spec.transform_values! do |spec|
    sanitized = spec.slice(:as, :named, :within, :if_unset, :from_set)
    sanitized[:as] = sanitized[:as].to_sym if sanitized.key? :as
    sanitized[:within] = sanitized[:within].constantize if sanitized.key? :within

    sanitized
  end

  register_all autoregister_spec

# Psych raises an Errno::ENOENT on file-not-found.
#
rescue Errno::ENOENT
  raise EnvParser::AutoregisterFileNotFound, %(file not found: "#{filename}")

# Psych raises a Psych::SyntaxError on unparseable YAML.
#
rescue Psych::SyntaxError => e
  raise EnvParser::UnparseableAutoregisterSpec, "malformed YAML in spec file: #{e.message}"
end

.define_type(name, options = {}) {|value| ... } ⇒ nil

Defines a new type for use as the “as” option on a subsequent parse or register call.

Parameters:

  • name (Symbol)

    The name to assign to the type.

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • aliases (Array<Symbol>)

    An array of additional names you’d like to see refer to this same type.

  • if_unset (Object) — default: nil

    Specifies a “sensible default” to return for this type if the value being parsed (via parse or register) is either unset (nil) or blank (''). Note this may be overridden by the user via the parse/register “if_unset” option.

Yields:

  • (value)

    A block to act as the parser for the this type. If no block is given, an ArgumentError is raised.

    When the type defined is used via a parse/register call, this block is invoked with the value to be parsed. Said value is guaranteed to be a non-empty String (the “if_unset” check will have already run), but no other assurances as to content are given. The block should return the final output of parsing the given String value as the type being defined.

    If the value given cannot be sensibly parsed into the type defined, the block should raise an ValueNotConvertibleError.

Returns:

  • (nil)

    This generates no usable value.

Raises:



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/env_parser.rb', line 44

def define_type(name, options = {}, &parser)
  raise(ArgumentError, 'no parsing block given') unless block_given?

  given_types = (Array(name) + Array(options[:aliases])).map(&:to_s).map(&:to_sym)
  given_types.each do |type|
    raise(TypeAlreadyDefinedError, "cannot redefine #{type.inspect}") if known_types.key?(type)

    known_types[type] = {
      parser: parser,
      if_unset: options[:if_unset]
    }
  end

  nil
end

.parse(value, options = {}) {|value| ... } ⇒ Object

Interprets the given value as the specified type.

Parameters:

  • value (String, Symbol)

    The value to parse/interpret. If a String is given, the value will be used as-is. If a Symbol is given, the ENV value for the matching string key will be used.

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • as (Symbol)

    The expected return type. A best-effort attempt is made to convert the source String to the requested type.

    If no “as” option is given, an ArgumentError is raised. If the “as” option given is unknown (the given type has not been previously defined via define_type), an UnknownTypeError is raised.

  • if_unset (Object)

    Specifies the default value to return if the given “value” is either unset (nil) or blank (''). Any “if_unset” value given will be returned as-is, with no type conversion or other change having been made. If unspecified, the “default” value for nil/'' input will depend on the “as” type.

  • from_set (Array, Range)

    Gives a limited set of allowed values (after type conversion). If, after parsing, the final value is not included in the “from_set” list/range, an ValueNotAllowedError is raised.

    Note that if the “if_unset” option is given and the value to parse is nil/'', the “if_unset” value will be returned, even if it is not part of the “from_set” list/range.

    Also note that, due to the nature of the lookup, the “from_set” option is only available for scalar values (i.e. not arrays, hashes, or other enumerables). An attempt to use the “from_set” option with a non-scalar value will raise an ArgumentError.

  • validated_by (Proc)

    If given, the “validated_by” Proc is called with the parsed value (after type conversion) as its sole argument. This allows for user-defined validation of the parsed value beyond what can be enforced by use of the “from_set” option alone. If the Proc’s return value is #blank?, an ValueNotAllowedError is raised. To accomodate your syntax of choice, this validation Proc may be given as a block instead.

    Note that this option is intended to provide an inspection mechanism only – no mutation of the parsed value should occur within the given Proc. To that end, the argument passed is a frozen duplicate of the parsed value.

Yields:

  • (value)

    A block (if given) is treated exactly as the “validated_by” Proc would.

    Although there is no compelling reason to provide both a “validated_by” Proc and a validation block, there is no technical limitation preventing this. If both are given, both validation checks must pass.

Raises:



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/env_parser.rb', line 112

def parse(value, options = {}, &validation_block)
  value = ENV[value.to_s] if value.is_a? Symbol
  value = value.to_s

  type = known_types[options[:as]]
  raise(ArgumentError, 'missing `as` parameter') unless options.key?(:as)
  raise(UnknownTypeError, "invalid `as` parameter: #{options[:as].inspect}") unless type

  return (options.key?(:if_unset) ? options[:if_unset] : type[:if_unset]) if value.blank?

  value = type[:parser].call(value)
  check_for_set_inclusion(value, set: options[:from_set]) if options.key?(:from_set)
  check_user_defined_validations(value, proc: options[:validated_by], block: validation_block)

  value
end

.register(name, options = {}) {|value| ... } ⇒ Object

Parses the referenced value and creates a matching constant in the requested context.

Multiple calls to register may be shortcutted by passing in a Hash whose keys are the variable names and whose values are the options set for each variable’s register call.


  # Example shortcut usage:

  EnvParser.register :A, from: one_hash, as: :integer
  EnvParser.register :B, from: another_hash, as: :string, if_unset: 'none'

  # ... is equivalent to ...

  EnvParser.register(
    A: { from: one_hash, as: :integer }
    B: { from: another_hash, as: :string, if_unset: 'none' }
  )

Parameters:

  • name

    The name of the value to parse/interpret from the “from” Hash. If the “from” value is ENV, you may give a Symbol and the corresponding String key will be used instead.

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • from (Hash) — default: ENV

    The source Hash from which to pull the value referenced by the “name” key.

  • named (Symbol)

    The name the constant should be given. Valid only when a “within” value is explicitly given. This allows for decoupling ENV variable names from the constant name defined within its target class or module, allowing for the ENV variables to be namespaced in some way.

    
      EnvParser.register(
        CUSTOM_CLIENT_DEFAULT_HOSTNAME: { as: :string, named: :DEFAULT_HOSTNAME, within: CustomClient },
        CUSTOM_CLIENT_DEFAULT_PORT: { as: :integer, named: :DEFAULT_PORT, within: CustomClient }
      )
    
  • within (Module, Class) — default: Kernel

    The module or class in which the constant should be created. Creates global constants by default.

  • as (Symbol)

    See parse.

  • if_unset (Object)

    See parse.

  • from_set (Array, Range)

    See parse.

  • validated_by (Proc)

    See parse.

Yields:

  • (value)

    A block (if given) is treated exactly as in parse. Note, however, that a single block cannot be used to register multiple constants simultaneously – each value needing validation must give its own “validated_by” Proc.

Raises:

  • (ArgumentError)


191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/env_parser.rb', line 191

def register(name, options = {}, &validation_block)
  # Allow for registering multiple variables simultaneously via a single call.
  if name.is_a? Hash
    raise(ArgumentError, 'cannot register multiple values with one block') if block_given?
    return register_all(name)
  end

  from = options.fetch(:from, ENV)
  within = options.fetch(:within, Kernel)

  named = name
  named = options.fetch(:named, name) if options.key? :within

  # ENV *seems* like a Hash and it does *some* Hash-y things, but it is NOT a Hash and that can
  # bite you in some cases. Making sure we're working with a straight-up Hash saves a lot of
  # sanity checks later on. This is also a good place to make sure we're working with a String
  # key.
  if from == ENV
    from = from.to_h
    name = name.to_s
  end

  raise ArgumentError, "invalid `from` parameter: #{from.class}" unless from.is_a? Hash
  raise ArgumentError, "invalid `within` parameter: #{within.inspect}" unless within.is_a?(Module) || within.is_a?(Class)

  value = from[name]
  value = parse(value, options, &validation_block)
  within.const_set(named.upcase.to_sym, value.dup.freeze)

  value
end