ConfigMapper

Gem Version Build Status

ConfigMapper maps configuration data onto Ruby objects.

Usage

Imagine you have some Ruby objects:

class Position

  attr_reader :x
  attr_reader :y

  def x=(arg); @x = Integer(arg); end
  def y=(arg); @y = Integer(arg); end

end

class State

  def initialize
    @position = Position.new
  end

  attr_reader :position
  attr_accessor :orientation

end

state = State.new

and wish to populate/modify it, based on plain data:

config_data = {
  "orientation" => "North",
  "position" => {
    "x" => 2,
    "y" => 4
  }
}

ConfigMapper will help you out:

require 'config_mapper'

errors = ConfigMapper.configure_with(config_data, state)
state.orientation              #=> "North"
state.position.x               #=> 2

It can even populate Hashes of objects, e.g.

positions = Hash.new { |h,k| h[k] = Position.new }

config_data = {
  "fred" => { "x" => 2, "y" => 4 },
  "mary" => { "x" => 3, "y" => 5 }
}

ConfigMapper.configure_with(config_data, positions)
positions["fred"].x            #=> 2
positions["mary"].y            #=> 5

Target object

Given

ConfigMapper.configure_with(config_data, target)

the target object is expected provide accessor-methods corresponding to the attributes that you want to make configurable. For example, with:

config_data = {
  "orientation" => "North",
  "position" => { "x" => 2, "y" => 4 }
}

it should have a orientiation= method, and a position method that returns a Position object, which should in turn have x= and y= methods.

ConfigMapper cannot and will not create objects for you.

Errors

ConfigMapper.configure_with returns a Hash of errors encountered while mapping data onto objects. The errors are Exceptions (typically ArgumentError or NoMethodError), keyed by the path to the offending data. e.g.

config_data = {
  "position" => {
    "bogus" => "flibble"
  }
}

errors = ConfigMapper.configure_with(config_data, state)
errors    #=> { ".position.bogus" => #<NoMethodError> }

ConfigStruct

ConfigMapper works pretty well with plain old Ruby objects, but we provide a base-class, ConfigMapper::ConfigStruct, with a DSL that makes it even easier to declare configuration data-structures.

Attributes

The attribute method is similar to attr_accessor, defining both reader and writer methods for the named attribute.

require "config_mapper/config_struct"

class State < ConfigMapper::ConfigStruct

  attribute :orientation

end

Type validation/coercion

If you specify a block when declaring an attribute, it will be invoked as part of the attribute's writer-method, to validate values when they are set. It should expect a single argument, and raise ArgumentError to signal invalid input. As the return value will be used as the value of the attribute, it's also an opportunity coerce values into canonical form.

class Server < ConfigMapper::ConfigStruct

  attribute :host do |arg|
    unless arg =~ /^\w+(\.\w+)+$/
      raise ArgumentError, "invalid hostname: #{arg}"
    end
    arg
  end

  attribute :port do |arg|
    Integer(arg)
  end

end

Alternatively, specify a "validator" as a second argument to attribute. It should be an object that responds to #call, with the same semantics described above. Good choices include Proc or Method objects, or type-objects from the dry-types project.

class Server < ConfigMapper::ConfigStruct

  attribute :host, Types::Strict::String.constrained(format: /^\w+(\.\w+)+$/)
  attribute :port, method(:Integer)

end

For convenience, primitive Ruby types such as Integer and Float can be used as shorthand for their namesake type-coercion methods on Kernel:

class Server < ConfigMapper::ConfigStruct

  attribute :port, Integer

end

Defaults

Attributes can be given default values, e.g.

class Address < ConfigMapper::ConfigStruct
  attribute :host
  attribute :port, :default => 80
  attribute :path, :default => nil
end

Specify a default value of nil to mark an attribute as optional. Attributes without a default are treated as "required".

Sub-components

The component method defines a nested component object, itself a ConfigStruct.

class State < ConfigMapper::ConfigStruct

  component :position do
    attribute :x
    attribute :y
  end

end

component_list declares a nested list of configurable objects, indexed by position, and component_dict declares a dictionary (map) of configurable objects, indexed by an arbitrary key.

class Polygon < ConfigMapper::ConfigStruct

  component_list :points do
    attribute :x
    attribute :y
  end

end

class Cargo < ConfigMapper::ConfigStruct

  component_dict :packages do
    attribute :contents
    attribute :weight, Float
  end

end

In both cases, new collection entries pop into existance the first time they are accessed.

Semantic errors

ConfigStruct#config_errors returns errors for each unset mandatory attribute.

state = State.new
state.position.x = 3
state.position.y = 4
state.config_errors
#=> { ".orientation" => #<ConfigMapper::ConfigStruct::NoValueProvided: no value provided> }

#config_errors can be overridden to provide custom semantic validation.

ConfigStruct#configure_with maps data into the object, and combines mapping errors and semantic errors (returned by #config_errors) into a single Hash:

data = {
  "position" => { "x" => 3, "y" => "fore" },
  "bogus" => "foobar"
}
state.configure_with(data)
#=> {
#=>   ".orientation" => "no value provided",
#=>   ".position.y" => #<ArgumentError: invalid value for Integer(): "fore">,
#=>   ".bogus" => #<NoMethodError: undefined method `bogus=' for #<State:0x007fc8e9b12a60>>
#=> }

ConfigStruct.from_data instantiates an object from data, raising an exception if errors are encountered:

state = State.from_data(data)

License

The gem is available as open source under the terms of the MIT License.

Contributing

It's on GitHub; you know the drill.

See also

  • ConfigHound is a great way to load raw config-data, before throwing it to ConfigMapper.