config_mapper 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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