class Configurator::Option

Constants

UNDEFINED_OPTION

Attributes

caster[R]
default[R]
name[RW]
parent[RW]
required[R]
type[R]
validations[R]

Public Class Methods

new(name, parent, options={}) click to toggle source
# File lib/configurator/option.rb, line 29
def initialize(name, parent, options={})
  @name     = name.to_sym
  @value    = nil
  @parent   = parent
  @guarding = false

  @default = (options.delete(:default) || UNDEFINED_OPTION).freeze
  @type    = (type = options.delete(:type)).nil? ? compute_type(@default) : type
  @caster  = (cast = options.delete(:cast)).nil? ? Cast::Director[@type] : Cast::Director[cast]

  @required    = determine_if_required?(options)
  @validations = gather_validations(options)

  if options.count > 0
    warn "#{path_name}: encountered unknown options: #{options.inspect}"
  end
rescue StandardError => e
  raise OptionInvalid.new("Failed to add option #{parent.path_name}.#{name}: #{e.class.name}: #{e.message}") { |ve|
    ve.set_backtrace(e.backtrace)
  }
end

Public Instance Methods

compute_type(type) click to toggle source
# File lib/configurator/option.rb, line 56
def compute_type(type)
  case type
    when UNDEFINED_OPTION then :any
    when OptionValue then type.type
    when Bignum, Fixnum then :integer
    when Float then :float
    when Symbol then :symbol
    when FalseClass, TrueClass, /(true|false|yes|no|enabled?|disabled?|on|off)/i then :boolean
    when String then :string
    when Pathname then :path
    when URI then :uri
    when Hash then :hash
    when Array then
      type.size <= 0 ? :array : [compute_type(type.first)]
    when Proc then
      with_loop_guard do
        compute_type(type.call)
      end rescue :any
    else :any
  end
end
deprecated?() click to toggle source
# File lib/configurator/option.rb, line 120
def deprecated?; false; end
empty?() click to toggle source
# File lib/configurator/option.rb, line 115
def empty?; value.nil? || value.empty?; end
include?(data) click to toggle source
# File lib/configurator/option.rb, line 111
def include?(data)
  value.respond_to?(:include?) ? value.include?(data) : false
end
inspect() click to toggle source
# File lib/configurator/option.rb, line 51
def inspect
  _type = type.is_a?(Array) ? "Collection::#{type.first.to_s.capitalize}" : type.to_s.capitalize
  "<Option::#{_type} @name=#{name} @required=#{required.inspect} @default=#{default.inspect} @value=#{value.inspect}>"
end
optional?() click to toggle source
# File lib/configurator/option.rb, line 118
def optional?; !required?; end
path_name() click to toggle source
# File lib/configurator/option.rb, line 119
def path_name; [ parent.path_name, name ].join('.'); end
renamed?() click to toggle source
# File lib/configurator/option.rb, line 121
def renamed?; false; end
required?() click to toggle source
# File lib/configurator/option.rb, line 117
def required?; !!@required; end
valid?() click to toggle source
# File lib/configurator/option.rb, line 116
def valid?; validate(value); end
value() click to toggle source
# File lib/configurator/option.rb, line 83
def value
  return nil if @value.nil? && @default == UNDEFINED_OPTION

  value = (@value || @default)

  begin
    with_loop_guard do
      if value.respond_to? :call
        unless value.arity == 0
          raise OptionInvalidCallableDefault, "#{path_name}: callable defaults must not accept any arguments"
        end
        value = value.call
      end
    end
  rescue OptionLoopError
    raise # bubble up
  rescue NoMethodError => e
    method = e.message.match(/undefined method .([^']+)'.+/)[1]
    raise OptionInvalidCallableDefault, "#{path_name}: bad method/option name #{method.inspect} in callable default."
  rescue StandardError => e
    excp = OptionInvalidCallableDefault.new "#{path_name}: error executing callable default: #{e.class.name}: #{e.message}"
    excp.set_backtrace(e.backtrace)
    raise excp
  end

  @caster.convert(value)
end
value=(v) click to toggle source
# File lib/configurator/option.rb, line 78
def value=(v)
  return nil unless validate(v)
  @value = v
end

Private Instance Methods

determine_if_required?(options) click to toggle source
# File lib/configurator/option.rb, line 125
def determine_if_required?(options)
  if options.key?(:required) && options.key?(:optional)
    unless !!options[:required] != !!options[:optional]
      raise OptionInvalidArgument, "#{path_name}: can't be both required and optional at the same time!"
    else
      options.delete(:optional)
      !!options.delete(:required)
    end
  elsif options.key?(:required)
    !!options.delete(:required)
  elsif options.key?(:optional)
    not !!options.delete(:optional)
  else
    # if there's no default, require option
    default == :__undefined__ ? true : false
  end
end
gather_validations(options) click to toggle source
# File lib/configurator/option.rb, line 143
def gather_validations(options)
  # XXX: create Validation classes (Expection would derive from Validation) and
  # move all validation logic into those classes
  [].tap { |validations|
    validation   = (v = options.delete(:validate)).nil? ? true : v
    validate_msg = options.delete(:validate_message)

    expectations = options.delete(:expect)
    expect_msg   = options.delete(:expect_messgae)

    type_validator     = options.delete(:type_validator)
    type_validator_msg = options.delete(:type_validation_message)

    if !validation
      if expectations
        raise OptionInvalidArgument, "#{path_name}: can't disable validations and set an expectation at the same time!"
      elsif type_validator
        raise OptionInvalidArgument, "#{path_name}: can't disable validations and assign a type validator at the same time!"
      end
    end

    return [] unless validation

    if type_validator
      validations << lambda { |_value|
        unless type_validator.call(_value)
          if type_validator_msg
            raise ValidationError, "#{path_name}: #{_value.inspect} fails to validate as custom type: #{type_validator_msg}"
          else
            raise ValidationError, "#{path_name}: #{_value.inspect} fails to validate as custom type."
          end
        end
        true
      }
    else
      validations << lambda { |_value|
        unless validate_type(_value)
          raise ValidationError, "#{path_name}: #{_value.inspect} fails to validate as #{type.inspect}"
        end
        true
      }
    end

    validations << lambda { |_value|
      unless validation.call(_value)
        if validate_msg
          raise ValidationError, "#{path_name}: #{_value.inspect} fails custom validation rule: #{validate_msg}"
        else
          raise ValidationError, "#{path_name}: #{_value.inspect} fails custom validation rule"
        end
      end
      true
    } if validation.respond_to?(:call)

    unless expectations.nil?
      if expectations.respond_to? :call
        validations << lambda { |_value|
          unless expectations.call(_value)
            if expect_msg
              raise ValidationError, "#{path_name}: #{_value.inspect} fails custom expectation: #{expect_msg}"
            else
              raise ValidationError, "#{path_name}: #{_value.inspect} fails custom expectation"
            end
          end
          true
        }
      else
        validations << lambda { |_value|
          unless expectations.include?(_value)
            raise ValidationError, "#{path_name}: Failed expectation: #{_value.inspect} not in list: #{expectations.collect(&:inspect).join(', ')}"
          end
          true
        }
      end
    end
  }
end
validate(_value) click to toggle source
# File lib/configurator/option.rb, line 221
def validate(_value)
  return true if type == :any && validations.empty?

  begin
    # try on just the raw value first
    validations.all? { |validation| validation.call(_value.freeze) }
  rescue StandardError => initial_exception
    begin
      # now try on the converted value
      cast_value = @caster.convert(_value)
      validations.all? { |validation| validation.call(cast_value) }
    rescue ValidationError => e
      raise ValidationError.new(e.message).tap {|ve| ve.set_backtrace(initial_exception.backtrace) }
    rescue CastError
      raise initial_exception
    end
  end
end
validate_type(_value, validation_type = nil) click to toggle source
# File lib/configurator/option.rb, line 240
def validate_type(_value, validation_type = nil)
  validation_type ||= type

  case validation_type
    when :any; true
    when Array then
      return _value.is_a?(Array) if validation_type.empty?
      [*_value].flatten.all? { |v|
        validate_type(v, validation_type.first)
      }
    when :scalar then
      validate_type(_value, :integer) || validate_type(_value, :float) ||
      validate_type(_value, :symbol) || validate_type(_value, :string) ||
      validate_type(_value, :boolean)
    when :boolean then
      _value.is_a?(FalseClass) || _value.is_a?(TrueClass)
    when :float then
      ((Float(_value) rescue nil) == _value.to_f)
    when :integer then
      ((Float(_value).to_i rescue nil) == _value.to_i)
    when :path then
      _value.is_a?(Pathname)
    when :array then _value.is_a?(Array)
    when :hash then _value.is_a?(Hash)
    when :string then _value.is_a? String
    when :symbol then _value.is_a? Symbol
    when :uri then !!(URI.parse(_value) rescue false)
    else
      warn "unable to validate - no handler for type: #{type.inspect}"
      true # assume valid
  end
end
with_loop_guard() { || ... } click to toggle source
# File lib/configurator/option.rb, line 273
def with_loop_guard(&block)
  begin
    raise OptionLoopError if @guarding
    @guarding = true
    yield
  rescue OptionLoopError => error
    raise error.tap { |e| e.stack << path_name }
  ensure
    @guarding = false
  end
end