ConfigMapper maps configuration data onto Ruby objects.
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
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.
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> }
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.
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
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
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".
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.
class Polygon < ConfigMapper::ConfigStruct
component_list :points do
attribute :x
attribute :y
end
end
component_dict
declares a dictionary (map) of configurable objects, indexed by an arbitrary key.
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.
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)
The gem is available as open source under the terms of the MIT License.
It's on GitHub; you know the drill.
- ConfigHound is a great way to load raw config-data, before throwing it to ConfigMapper.