config_mapper 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a927800ba901de2ba648d012936b19b591d24c5e
4
- data.tar.gz: c93aace56fd8a8a9662f547ef72e5c5f1d0a9396
3
+ metadata.gz: b3eceb23aa75978730fec0b82c9a01185852d056
4
+ data.tar.gz: 155083970129df5b800a6085957a21c98e869f1e
5
5
  SHA512:
6
- metadata.gz: 5da46b8fb08d97ccfe985cb2a0da607de151f2d70167c0edd03c66a5771d9e5e24fda1a2183e58552103aa451d9016c8d45991ba87d1c3a47fc0b3e1e7b0e8e1
7
- data.tar.gz: 3b40a4cc5c9049f681bbeaf17cacad8817dffd48c53b5ec46314a1e49f0410053f335feee942357b8265dbbb660468751f2de1657994af68bca3b4514399faaf
6
+ metadata.gz: 4c959618d4f898cc3e49c3512042c043b5b5a4b26cb7a9287e6620832fc53be5de77950fc7e7d551695c203ef8a8235a361e20db5f5d557bad0834f2e63075c1
7
+ data.tar.gz: 128a784d48f8933c27c83d55e754889b43f9eb6e61451ca6a4e23162e88175a6b53ad8e1a8cb27f156bac796767e6715d08b07c82f632b50433825b172842837
data/Gemfile CHANGED
@@ -1,4 +1,9 @@
1
1
  source "https://rubygems.org"
2
2
 
3
+ group :development do
4
+ gem "guard-rspec", :require => false
5
+ gem "rubocop", :require => false
6
+ end
7
+
3
8
  # Specify your gem's dependencies in config_mapper.gemspec
4
9
  gemspec
data/Guardfile ADDED
@@ -0,0 +1,43 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ ## Uncomment and set this to only include directories you want to watch
5
+ # directories %w(app lib config test spec features) \
6
+ # .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
7
+
8
+ ## Note: if you are using the `directories` clause above and you are not
9
+ ## watching the project directory ('.'), then you will want to move
10
+ ## the Guardfile to a watched dir and symlink it back, e.g.
11
+ #
12
+ # $ mkdir config
13
+ # $ mv Guardfile config/
14
+ # $ ln -s config/Guardfile .
15
+ #
16
+ # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
17
+
18
+ # Note: The cmd option is now required due to the increasing number of ways
19
+ # rspec may be run, below are examples of the most common uses.
20
+ # * bundler: 'bundle exec rspec'
21
+ # * bundler binstubs: 'bin/rspec'
22
+ # * spring: 'bin/rspec' (This will use spring if running and you have
23
+ # installed the spring binstubs per the docs)
24
+ # * zeus: 'zeus rspec' (requires the server to be started separately)
25
+ # * 'just' rspec: 'rspec'
26
+
27
+ guard :rspec, :cmd => "bundle exec rspec" do
28
+ require "guard/rspec/dsl"
29
+ dsl = Guard::RSpec::Dsl.new(self)
30
+
31
+ # Feel free to open issues for suggestions and improvements
32
+
33
+ # RSpec files
34
+ rspec = dsl.rspec
35
+ watch(rspec.spec_helper) { rspec.spec_dir }
36
+ watch(rspec.spec_support) { rspec.spec_dir }
37
+ watch(rspec.spec_files)
38
+
39
+ # Ruby files
40
+ ruby = dsl.ruby
41
+ dsl.watch_spec_files_for(ruby.lib_files)
42
+
43
+ end
data/README.md CHANGED
@@ -20,7 +20,7 @@ end
20
20
  class State
21
21
 
22
22
  def initialize
23
- @position = Position.new
23
+ @position = Position.new
24
24
  end
25
25
 
26
26
  attr_reader :position
@@ -37,8 +37,8 @@ and wish to populate/modify it, based on plain data:
37
37
  config_data = {
38
38
  "orientation" => "North",
39
39
  "position" => {
40
- "x" => 2,
41
- "y" => 4
40
+ "x" => 2,
41
+ "y" => 4
42
42
  }
43
43
  }
44
44
  ```
@@ -48,7 +48,7 @@ ConfigMapper will help you out:
48
48
  ```ruby
49
49
  require 'config_mapper'
50
50
 
51
- errors = ConfigMapper.set(config_data, state)
51
+ errors = ConfigMapper.configure_with(config_data, state)
52
52
  state.orientation #=> "North"
53
53
  state.position.x #=> 2
54
54
  ```
@@ -63,16 +63,38 @@ config_data = {
63
63
  "mary" => { "x" => 3, "y" => 5 }
64
64
  }
65
65
 
66
- ConfigMapper.set(config_data, positions)
66
+ ConfigMapper.configure_with(config_data, positions)
67
67
  positions["fred"].x #=> 2
68
68
  positions["mary"].y #=> 5
69
69
  ```
70
70
 
71
+ ### Target object
72
+
73
+ Given
74
+
75
+ ```ruby
76
+ ConfigMapper.configure_with(config_data, target)
77
+ ```
78
+
79
+ the `target` object is expected provide accessor-methods corresponding
80
+ to the attributes that you want to make configurable. For example, with:
81
+
82
+ ```ruby
83
+ config_data = {
84
+ "orientation" => "North",
85
+ "position" => { "x" => 2, "y" => 4 }
86
+ }
87
+ ```
88
+
89
+ it should have a `orientiation=` method, and a `position` method that
90
+ returns a `Position` object, which should in turn have `x=` and `y=`
91
+ methods.
92
+
93
+ ConfigMapper cannot and will not _create_ objects for you.
94
+
71
95
  ### Errors
72
96
 
73
- `ConfigMapper.set` returns a Hash of errors encountered while mapping data
74
- onto objects. The errors are Exceptions (typically ArgumentError or NoMethodError),
75
- keyed by a Array representing the path to the offending data. e.g.
97
+ `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.
76
98
 
77
99
  ```ruby
78
100
  config_data = {
@@ -81,8 +103,72 @@ config_data = {
81
103
  }
82
104
  }
83
105
 
84
- errors = ConfigMapper.set(config_data, state)
85
- errors #=> { ["position", "bogus"] => #<NoMethodError> }
106
+ errors = ConfigMapper.configure_with(config_data, state)
107
+ errors #=> { ".position.bogus" => #<NoMethodError> }
108
+ ```
109
+
110
+ ## ConfigStruct
111
+
112
+ ConfigMapper works pretty well with plain old Ruby objects, but we
113
+ provide a base-class, `ConfigMapper::ConfigStruct`, with a DSL that
114
+ makes it even easier to declare configuration data-structures.
115
+
116
+ ```ruby
117
+ require "config_mapper/config_struct"
118
+
119
+ class State < ConfigMapper::ConfigStruct
120
+
121
+ component :position do
122
+ attribute(:x) { |arg| Integer(arg) }
123
+ attribute(:y) { |arg| Integer(arg) }
124
+ end
125
+
126
+ attribute :orientation
127
+
128
+ end
129
+ ```
130
+
131
+ By default, declared attributes are assumed to be mandatory. The
132
+ `ConfigStruct#config_errors` method returns errors for each unset mandatory
133
+ attribute.
134
+
135
+ ```ruby
136
+ state = State.new
137
+ state.position.x = 3
138
+ state.position.y = 4
139
+ state.config_errors
140
+ #=> { ".orientation" => "no value provided" }
141
+ ```
142
+
143
+ `#config_errors` can be overridden to provide custom semantic validation.
144
+
145
+ Attributes can be given default values. Provide an explicit `nil` default to
146
+ mark an attribute as optional, e.g.
147
+
148
+ ```ruby
149
+ class Address < ConfigMapper::ConfigStruct
150
+
151
+ attribute :host
152
+ attribute :port, :default => 80
153
+ attribute :path, :default => nil
154
+
155
+ end
156
+ ```
157
+
158
+ `ConfigStruct#configure_with` maps data into the object, and combines mapping errors and
159
+ semantic errors (returned by `#config_errors`) into a single Hash:
160
+
161
+ ```ruby
162
+ data = {
163
+ "position" => { "x" => 3, "y" => "fore" },
164
+ "bogus" => "foobar"
165
+ }
166
+ state.configure_with(data)
167
+ #=> {
168
+ #=> ".orientation" => "no value provided",
169
+ #=> ".position.y" => #<ArgumentError: invalid value for Integer(): "fore">,
170
+ #=> ".bogus" => #<NoMethodError: undefined method `bogus=' for #<State:0x007fc8e9b12a60>>
171
+ #=> }
86
172
  ```
87
173
 
88
174
  ## License
@@ -92,3 +178,8 @@ The gem is available as open source under the terms of the [MIT License](http://
92
178
  ## Contributing
93
179
 
94
180
  It's on GitHub; you know the drill.
181
+
182
+ ## See also
183
+
184
+ * [ConfigHound](https://github.com/mdub/config_hound) is a great way to
185
+ load raw config-data, before throwing it to ConfigMapper.
@@ -0,0 +1,189 @@
1
+ require "config_mapper"
2
+ require "forwardable"
3
+
4
+ module ConfigMapper
5
+
6
+ # A set of configurable attributes.
7
+ #
8
+ class ConfigStruct
9
+
10
+ class << self
11
+
12
+ # Defines reader and writer methods for the specified attribute.
13
+ #
14
+ # A `:default` value may be specified; otherwise, the attribute is
15
+ # considered mandatory.
16
+ #
17
+ # If a block is provided, it will invoked in the writer-method to
18
+ # validate the argument.
19
+ #
20
+ # @param name [Symbol] attribute name
21
+ # @options options [String] :default (nil) default value
22
+ # @yield type-coercion block
23
+ #
24
+ def attribute(name, options = {}, &coerce_block)
25
+ name = name.to_sym
26
+ if options.key?(:default)
27
+ default_value = options.fetch(:default).freeze
28
+ attribute_initializers[name] = proc { default_value }
29
+ else
30
+ required_attributes << name
31
+ end
32
+ attr_reader(name)
33
+ if coerce_block
34
+ define_method("#{name}=") do |arg|
35
+ instance_variable_set("@#{name}", coerce_block.call(arg))
36
+ end
37
+ else
38
+ attr_writer(name)
39
+ end
40
+ end
41
+
42
+ # Defines a sub-component.
43
+ #
44
+ # If a block is be provided, it will be `class_eval`ed to define the
45
+ # sub-components class.
46
+ #
47
+ # @param name [Symbol] component name
48
+ # @options options [String] :type (ConfigMapper::ConfigStruct)
49
+ # component base-class
50
+ #
51
+ def component(name, options = {}, &block)
52
+ name = name.to_sym
53
+ declared_components << name
54
+ type = options.fetch(:type, ConfigStruct)
55
+ type = Class.new(type, &block) if block
56
+ type = type.method(:new) if type.respond_to?(:new)
57
+ attribute_initializers[name] = type
58
+ attr_reader name
59
+ end
60
+
61
+ # Defines an associative array of sub-components.
62
+ #
63
+ # If a block is be provided, it will be `class_eval`ed to define the
64
+ # sub-components class.
65
+ #
66
+ # @param name [Symbol] dictionary attribute name
67
+ # @options options [Proc] :key_type
68
+ # function used to validate keys
69
+ # @options options [String] :type (ConfigMapper::ConfigStruct)
70
+ # base-class for sub-component values
71
+ #
72
+ def component_dict(name, options = {}, &block)
73
+ name = name.to_sym
74
+ declared_component_dicts << name
75
+ type = options.fetch(:type, ConfigStruct)
76
+ type = Class.new(type, &block) if block
77
+ type = type.method(:new) if type.respond_to?(:new)
78
+ key_type = options[:key_type]
79
+ key_type = key_type.method(:new) if key_type.respond_to?(:new)
80
+ attribute_initializers[name] = lambda do
81
+ ConfigDict.new(type, key_type)
82
+ end
83
+ attr_reader name
84
+ end
85
+
86
+ def required_attributes
87
+ @required_attributes ||= []
88
+ end
89
+
90
+ def attribute_initializers
91
+ @attribute_initializers ||= {}
92
+ end
93
+
94
+ def declared_components
95
+ @declared_components ||= []
96
+ end
97
+
98
+ def declared_component_dicts
99
+ @declared_component_dicts ||= []
100
+ end
101
+
102
+ end
103
+
104
+ def initialize
105
+ self.class.attribute_initializers.each do |name, initializer|
106
+ instance_variable_set("@#{name}", initializer.call)
107
+ end
108
+ end
109
+
110
+ def immediate_config_errors
111
+ missing_required_attribute_errors
112
+ end
113
+
114
+ def config_errors
115
+ immediate_config_errors.merge(component_config_errors)
116
+ end
117
+
118
+ # Configure with data.
119
+ #
120
+ # @param attribute_values [Hash] attribute values
121
+ # @return [Hash] errors encountered, keyed by attribute path
122
+ #
123
+ def configure_with(attribute_values)
124
+ errors = ConfigMapper.configure_with(attribute_values, self)
125
+ config_errors.merge(errors)
126
+ end
127
+
128
+ private
129
+
130
+ def components
131
+ {}.tap do |result|
132
+ self.class.declared_components.each do |name|
133
+ result[".#{name}"] = instance_variable_get("@#{name}")
134
+ end
135
+ self.class.declared_component_dicts.each do |name|
136
+ instance_variable_get("@#{name}").each do |key, value|
137
+ result[".#{name}[#{key.inspect}]"] = value
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ NOT_SET = "no value provided".freeze
144
+
145
+ def missing_required_attribute_errors
146
+ {}.tap do |errors|
147
+ self.class.required_attributes.each do |name|
148
+ unless instance_variable_defined?("@#{name}")
149
+ errors[".#{name}"] = NOT_SET
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ def component_config_errors
156
+ {}.tap do |errors|
157
+ components.each do |component_name, component_value|
158
+ next unless component_value.respond_to?(:config_errors)
159
+ component_value.config_errors.each do |key, value|
160
+ errors["#{component_name}#{key}"] = value
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ end
167
+
168
+ class ConfigDict
169
+
170
+ def initialize(entry_type, key_type = nil)
171
+ @entry_type = entry_type
172
+ @key_type = key_type
173
+ @entries = {}
174
+ end
175
+
176
+ def [](key)
177
+ key = @key_type.call(key) if @key_type
178
+ @entries[key] ||= @entry_type.call
179
+ end
180
+
181
+ extend Forwardable
182
+
183
+ def_delegators :@entries, :each, :empty?, :keys, :map, :size
184
+
185
+ include Enumerable
186
+
187
+ end
188
+
189
+ end
@@ -0,0 +1,27 @@
1
+ require "config_mapper/target"
2
+
3
+ module ConfigMapper
4
+
5
+ # Configuration proxy for a Hash.
6
+ #
7
+ class HashTarget < Target
8
+
9
+ def initialize(hash)
10
+ @hash = hash
11
+ end
12
+
13
+ def path(key)
14
+ "[#{key.inspect}]"
15
+ end
16
+
17
+ def get(key)
18
+ @hash[key]
19
+ end
20
+
21
+ def set(key, value)
22
+ @hash[key] = value
23
+ end
24
+
25
+ end
26
+
27
+ end
@@ -0,0 +1,27 @@
1
+ require "config_mapper/target"
2
+
3
+ module ConfigMapper
4
+
5
+ # Configuration proxy for an Object.
6
+ #
7
+ class ObjectTarget < Target
8
+
9
+ def initialize(object)
10
+ @object = object
11
+ end
12
+
13
+ def path(key)
14
+ ".#{key}"
15
+ end
16
+
17
+ def get(key)
18
+ @object.public_send(key)
19
+ end
20
+
21
+ def set(key, value)
22
+ @object.public_send("#{key}=", value)
23
+ end
24
+
25
+ end
26
+
27
+ end
@@ -0,0 +1,39 @@
1
+ module ConfigMapper
2
+
3
+ # Something that accepts configuration.
4
+ #
5
+ class Target
6
+
7
+ # Map configuration data onto the target.
8
+ #
9
+ # @return [Hash] exceptions encountered
10
+ #
11
+ def with(data)
12
+ errors = {}
13
+ data.each do |key, value|
14
+ configure_attribute(key, value, errors)
15
+ end
16
+ errors
17
+ end
18
+
19
+ private
20
+
21
+ # Set a single attribute.
22
+ #
23
+ def configure_attribute(key, value, errors)
24
+ attribute_path = path(key)
25
+ if value.is_a?(Hash) && !get(key).nil?
26
+ nested_errors = ConfigMapper.configure_with(value, get(key))
27
+ nested_errors.each do |nested_path, error|
28
+ errors["#{attribute_path}#{nested_path}"] = error
29
+ end
30
+ else
31
+ set(key, value)
32
+ end
33
+ rescue NoMethodError, ArgumentError => e
34
+ errors[attribute_path] = e
35
+ end
36
+
37
+ end
38
+
39
+ end
@@ -1,5 +1,5 @@
1
1
  module ConfigMapper
2
2
 
3
- VERSION = "1.0.0"
3
+ VERSION = "1.1.0"
4
4
 
5
5
  end
data/lib/config_mapper.rb CHANGED
@@ -1,23 +1,39 @@
1
- require "config_mapper/attribute_sink"
1
+ require "config_mapper/hash_target"
2
+ require "config_mapper/object_target"
2
3
 
3
4
  # Supports marshalling of plain-old data (e.g. loaded from
4
5
  # YAML files) onto strongly-typed objects.
5
6
  #
6
7
  module ConfigMapper
7
8
 
8
- # Attempt to set attributes on a target object.
9
- #
10
- # For simple, scalar values, set the attribute by calling the
11
- # named writer-method on the target object.
12
- #
13
- # For Hash values, set attributes of the named sub-component.
14
- #
15
- # @return [Hash] exceptions encountered
16
- #
17
- def self.set(data, target)
18
- mapper = AttributeSink.new(target)
19
- mapper.set_attributes(data)
20
- mapper.errors
9
+ class << self
10
+
11
+ # Set attributes of a target object based on configuration data.
12
+ #
13
+ # For simple, scalar values, set the attribute by calling the
14
+ # named writer-method on the target object.
15
+ #
16
+ # For Hash values, set attributes of the named sub-component.
17
+ #
18
+ # @param data configuration data
19
+ # @param [Object, Hash] target the object to configure
20
+ #
21
+ # @return [Hash] exceptions encountered
22
+ #
23
+ def configure_with(data, target)
24
+ target(target).with(data)
25
+ end
26
+
27
+ alias_method :set, :configure_with
28
+
29
+ def target(target)
30
+ if target.is_a?(Hash)
31
+ HashTarget.new(target)
32
+ else
33
+ ObjectTarget.new(target)
34
+ end
35
+ end
36
+
21
37
  end
22
38
 
23
39
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: config_mapper
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Williams
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-09-04 00:00:00.000000000 Z
11
+ date: 2016-02-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -64,14 +64,16 @@ files:
64
64
  - ".rubocop.yml"
65
65
  - ".travis.yml"
66
66
  - Gemfile
67
+ - Guardfile
67
68
  - LICENSE.txt
68
69
  - README.md
69
70
  - Rakefile
70
71
  - config_mapper.gemspec
71
72
  - lib/config_mapper.rb
72
- - lib/config_mapper/attribute_sink.rb
73
- - lib/config_mapper/error_proxy.rb
74
- - lib/config_mapper/object_as_hash.rb
73
+ - lib/config_mapper/config_struct.rb
74
+ - lib/config_mapper/hash_target.rb
75
+ - lib/config_mapper/object_target.rb
76
+ - lib/config_mapper/target.rb
75
77
  - lib/config_mapper/version.rb
76
78
  homepage: https://github.com/mdub/config_mapper
77
79
  licenses:
@@ -93,7 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
95
  version: '0'
94
96
  requirements: []
95
97
  rubyforge_project:
96
- rubygems_version: 2.4.8
98
+ rubygems_version: 2.5.0
97
99
  signing_key:
98
100
  specification_version: 4
99
101
  summary: Maps config data onto plain old objects
@@ -1,42 +0,0 @@
1
- require "config_mapper/error_proxy"
2
- require "config_mapper/object_as_hash"
3
-
4
- module ConfigMapper
5
-
6
- # Sets attributes on an object, collecting errors
7
- #
8
- class AttributeSink
9
-
10
- def initialize(target, errors = {})
11
- @target = ObjectAsHash[target]
12
- @errors = errors
13
- end
14
-
15
- attr_reader :target
16
- attr_reader :errors
17
-
18
- # Set multiple attributes from a Hash.
19
- #
20
- def set_attributes(data)
21
- data.each do |key, value|
22
- set_attribute(key, value)
23
- end
24
- end
25
-
26
- # Set a single attribute.
27
- #
28
- def set_attribute(key, value)
29
- if value.is_a?(Hash) && !target[key].nil?
30
- nested_errors = ErrorProxy.new(errors, [key])
31
- nested_mapper = self.class.new(target[key], nested_errors)
32
- nested_mapper.set_attributes(value)
33
- else
34
- target[key] = value
35
- end
36
- rescue NoMethodError, ArgumentError => e
37
- errors[[key]] = e
38
- end
39
-
40
- end
41
-
42
- end
@@ -1,23 +0,0 @@
1
- module ConfigMapper
2
-
3
- # Wraps a Hash of errors, injecting prefixes
4
- #
5
- class ErrorProxy
6
-
7
- def initialize(errors, prefix)
8
- @errors = errors
9
- @prefix = prefix
10
- end
11
-
12
- def []=(key, value)
13
- errors[prefix + key] = value
14
- end
15
-
16
- private
17
-
18
- attr_reader :errors
19
- attr_reader :prefix
20
-
21
- end
22
-
23
- end
@@ -1,29 +0,0 @@
1
- module ConfigMapper
2
-
3
- # Wrap an object to make it look more like a Hash.
4
- #
5
- class ObjectAsHash
6
-
7
- def self.[](target)
8
- if target.is_a?(Hash)
9
- target
10
- else
11
- ObjectAsHash.new(target)
12
- end
13
- end
14
-
15
- def initialize(target)
16
- @target = target
17
- end
18
-
19
- def [](key)
20
- @target.public_send(key)
21
- end
22
-
23
- def []=(key, value)
24
- @target.public_send("#{key}=", value)
25
- end
26
-
27
- end
28
-
29
- end