config_mapper 1.4.1 → 1.5.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: 7de0e012b37758b972449905dd89e25fff9c5f78
4
- data.tar.gz: ea363b40759c5fa82e7531c088486eb535dc30f3
3
+ metadata.gz: 071bbe40a32e09004ef7e5a12ddc9eb909b7e06a
4
+ data.tar.gz: c49eff8eda1e383a3e02f72aa6c7baca0f8810c6
5
5
  SHA512:
6
- metadata.gz: c4fa80171dd00dfec8c0dfc7a6f561bfe400045e01713bf928136d21ab6fe3df3fe06c212620ab16c6d8b9b5e452e3217b5fa7be52fe90fbec8031724e0f1009
7
- data.tar.gz: 2fcacd97d600bb870f84ef1c8c7c9b3c991ef383db9ee6965bffb63be81d6c15de6ca461e8b8acc90fb4fababcdd9d40a67bac2647bd4127c60bc4e97b8cc282
6
+ metadata.gz: 8f8f2fdbf001b744b04c2ceb39c64c5df448568ea4b4e915d12060f0571c6bc2a8ad0581b27a545403066881f79a98f3f1633de2239959d90558967e2392089e
7
+ data.tar.gz: 51f1cffdbbccc557e8a13b7c0882b5cdaefef07d167971a79c22ffe20fc5153c37ae77c8381045c04681715514dabb8ef24e0c9b2334fef454b55a5fc3a67c89
data/README.md CHANGED
@@ -1,10 +1,26 @@
1
1
  # ConfigMapper
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/config_mapper.png)](http://badge.fury.io/rb/config_mapper)
4
- [![Build Status](https://secure.travis-ci.org/mdub/config_mapper.png?branch=master)](http://travis-ci.org/mdub/config_mapper)
3
+ [![Gem Version](https://badge.fury.io/rb/config_mapper.svg)](https://badge.fury.io/rb/config_mapper)
4
+ [![Build Status](https://travis-ci.org/mdub/config_mapper.svg?branch=master)](https://travis-ci.org/mdub/config_mapper)
5
5
 
6
6
  ConfigMapper maps configuration data onto Ruby objects.
7
7
 
8
+ <!-- TOC depthFrom:2 depthTo:6 withLinks:1 updateOnSave:1 orderedList:0 -->
9
+
10
+ - [Usage](#usage)
11
+ - [Target object](#target-object)
12
+ - [Errors](#errors)
13
+ - [ConfigStruct](#configstruct)
14
+ - [Attributes](#attributes)
15
+ - [Type validation/coercion](#type-validationcoercion)
16
+ - [Defaults](#defaults)
17
+ - [Semantic errors](#semantic-errors)
18
+ - [License](#license)
19
+ - [Contributing](#contributing)
20
+ - [See also](#see-also)
21
+
22
+ <!-- /TOC -->
23
+
8
24
  ## Usage
9
25
 
10
26
  Imagine you have some Ruby objects:
@@ -116,34 +132,65 @@ ConfigMapper works pretty well with plain old Ruby objects, but we
116
132
  provide a base-class, `ConfigMapper::ConfigStruct`, with a DSL that
117
133
  makes it even easier to declare configuration data-structures.
118
134
 
135
+ ### Attributes
136
+
137
+ The `attribute` method is similar to `attr_accessor`, defining both reader and writer methods for the named attribute.
138
+
119
139
  ```ruby
120
140
  require "config_mapper/config_struct"
121
141
 
122
142
  class State < ConfigMapper::ConfigStruct
123
143
 
124
- component :position do
125
- attribute :x
126
- attribute :y
144
+ attribute :orientation
145
+
146
+ end
147
+ ```
148
+
149
+ ### Type validation/coercion
150
+
151
+ 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.
152
+
153
+ ```ruby
154
+ class Server < ConfigMapper::ConfigStruct
155
+
156
+ attribute :host do |arg|
157
+ unless arg =~ /^\w+(\.\w+)+$/
158
+ raise ArgumentError, "invalid hostname: #{arg}"
159
+ end
160
+ arg
127
161
  end
128
162
 
129
- attribute :orientation
163
+ attribute :port do |arg|
164
+ Integer(arg)
165
+ end
130
166
 
131
167
  end
132
168
  ```
133
169
 
134
- `ConfigStruct#config_errors` returns errors for each unset mandatory attribute.
170
+ 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](http://dry-rb.org/gems/dry-types/) project.
135
171
 
136
172
  ```ruby
137
- state = State.new
138
- state.position.x = 3
139
- state.position.y = 4
140
- state.config_errors
141
- #=> { ".orientation" => #<ConfigMapper::ConfigStruct::NoValueProvided: no value provided> }
173
+ class Server < ConfigMapper::ConfigStruct
174
+
175
+ attribute :host, Types::Strict::String.constrained(format: /^\w+(\.\w+)+$/)
176
+ attribute :port, method(:Integer)
177
+
178
+ end
142
179
  ```
143
180
 
144
- `#config_errors` can be overridden to provide custom semantic validation.
181
+ For convenience, primitive Ruby types such as `Integer` and `Float` can be used as shorthand for their namesake type-coercion methods on `Kernel`:
145
182
 
146
- Attributes can be given default values. Specify a default value of `nil` to mark an attribute as optional, e.g.
183
+ ```ruby
184
+ class Server < ConfigMapper::ConfigStruct
185
+
186
+ attribute :port, Integer
187
+
188
+ end
189
+ ```
190
+
191
+ ### Defaults
192
+
193
+ Attributes can be given default values, e.g.
147
194
 
148
195
  ```ruby
149
196
  class Address < ConfigMapper::ConfigStruct
@@ -153,25 +200,37 @@ class Address < ConfigMapper::ConfigStruct
153
200
  end
154
201
  ```
155
202
 
156
- If a block is provided when an `attribute` is declared, it is used to validate values when they are set, and/or coerce them to a canonical type. The block should raise `ArgumentError` to indicate an invalid value.
203
+ Specify a default value of `nil` to mark an attribute as optional. Attributes without a default are treated as "required".
157
204
 
158
- ```ruby
159
- class Server < ConfigMapper::ConfigStruct
205
+ ### Sub-components
160
206
 
161
- attribute :host do |arg|
162
- unless arg =~ /^\w+(\.\w+)+$/
163
- raise ArgumentError, "invalid hostname: #{arg}"
164
- end
165
- arg
166
- end
207
+ The `component` method defines a nested component object, itself a `ConfigStruct`.
167
208
 
168
- attribute :port do |arg|
169
- Integer(arg)
209
+ ```ruby
210
+ class State < ConfigMapper::ConfigStruct
211
+
212
+ component :position do
213
+ attribute :x
214
+ attribute :y
170
215
  end
171
216
 
172
217
  end
173
218
  ```
174
219
 
220
+ ### Semantic errors
221
+
222
+ `ConfigStruct#config_errors` returns errors for each unset mandatory attribute.
223
+
224
+ ```ruby
225
+ state = State.new
226
+ state.position.x = 3
227
+ state.position.y = 4
228
+ state.config_errors
229
+ #=> { ".orientation" => #<ConfigMapper::ConfigStruct::NoValueProvided: no value provided> }
230
+ ```
231
+
232
+ `#config_errors` can be overridden to provide custom semantic validation.
233
+
175
234
  `ConfigStruct#configure_with` maps data into the object, and combines mapping errors and semantic errors (returned by `#config_errors`) into a single Hash:
176
235
 
177
236
  ```ruby
@@ -1,18 +1,45 @@
1
+ require "config_mapper/factory"
2
+ require "config_mapper/validator"
1
3
  require "forwardable"
2
4
 
3
5
  module ConfigMapper
4
6
 
5
7
  class ConfigDict
6
8
 
7
- def initialize(entry_type, key_type = nil)
8
- @entry_type = entry_type
9
- @key_type = key_type
9
+ class Factory
10
+
11
+ def initialize(entry_factory, key_validator)
12
+ @entry_factory = ConfigMapper::Factory.resolve(entry_factory)
13
+ @key_validator = ConfigMapper::Validator.resolve(key_validator)
14
+ end
15
+
16
+ attr_reader :entry_factory
17
+ attr_reader :key_validator
18
+
19
+ def new
20
+ ConfigDict.new(@entry_factory, @key_validator)
21
+ end
22
+
23
+ def config_doc
24
+ return {} unless entry_factory.respond_to?(:config_doc)
25
+ {}.tap do |result|
26
+ entry_factory.config_doc.each do |path, doc|
27
+ result["[X]#{path}"] = doc
28
+ end
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+ def initialize(entry_factory, key_validator = nil)
35
+ @entry_factory = entry_factory
36
+ @key_validator = key_validator
10
37
  @entries = {}
11
38
  end
12
39
 
13
40
  def [](key)
14
- key = @key_type.call(key) if @key_type
15
- @entries[key] ||= @entry_type.call
41
+ key = @key_validator.call(key) if @key_validator
42
+ @entries[key] ||= @entry_factory.new
16
43
  end
17
44
 
18
45
  def to_h
@@ -23,6 +50,18 @@ module ConfigMapper
23
50
  end
24
51
  end
25
52
 
53
+ def config_errors
54
+ {}.tap do |errors|
55
+ each do |key, value|
56
+ prefix = "[#{key.inspect}]"
57
+ next unless value.respond_to?(:config_errors)
58
+ value.config_errors.each do |path, path_errors|
59
+ errors["#{prefix}#{path}"] = path_errors
60
+ end
61
+ end
62
+ end
63
+ end
64
+
26
65
  extend Forwardable
27
66
 
28
67
  def_delegators :@entries, :each, :empty?, :key?, :keys, :map, :size
@@ -1,5 +1,7 @@
1
1
  require "config_mapper"
2
2
  require "config_mapper/config_dict"
3
+ require "config_mapper/factory"
4
+ require "config_mapper/validator"
3
5
 
4
6
  module ConfigMapper
5
7
 
@@ -18,28 +20,31 @@ module ConfigMapper
18
20
  # validate the argument.
19
21
  #
20
22
  # @param name [Symbol] attribute name
21
- # @options options [String] :default (nil) default value
23
+ # @param default default value
22
24
  # @yield type-coercion block
23
25
  #
24
- def attribute(name, options = {})
25
- name = name.to_sym
26
- required = true
27
- default_value = nil
28
- if options.key?(:default)
29
- default_value = options.fetch(:default).freeze
30
- required = false if default_value.nil?
26
+ def attribute(name, type = nil, default: :no_default, description: nil, &type_block)
27
+
28
+ attribute = attribute!(name)
29
+ attribute.description = description
30
+
31
+ if default == :no_default
32
+ attribute.required = true
33
+ else
34
+ attribute.default = default.freeze
31
35
  end
32
- attribute_initializers[name] = proc { default_value }
33
- required_attributes << name if required
34
- attr_reader(name)
35
- define_method("#{name}=") do |value|
36
+
37
+ attribute.validator = Validator.resolve(type || type_block)
38
+
39
+ define_method("#{attribute.name}=") do |value|
36
40
  if value.nil?
37
- raise NoValueProvided if required
41
+ raise NoValueProvided if attribute.required
38
42
  else
39
- value = yield(value) if block_given?
43
+ value = attribute.validator.call(value) if attribute.validator
40
44
  end
41
- instance_variable_set("@#{name}", value)
45
+ instance_variable_set("@#{attribute.name}", value)
42
46
  end
47
+
43
48
  end
44
49
 
45
50
  # Defines a sub-component.
@@ -48,17 +53,13 @@ module ConfigMapper
48
53
  # sub-components class.
49
54
  #
50
55
  # @param name [Symbol] component name
51
- # @options options [String] :type (ConfigMapper::ConfigStruct)
52
- # component base-class
56
+ # @param type [Class] component base-class
53
57
  #
54
- def component(name, options = {}, &block)
55
- name = name.to_sym
56
- declared_components << name
57
- type = options.fetch(:type, ConfigStruct)
58
+ def component(name, type: ConfigStruct, description: nil, &block)
58
59
  type = Class.new(type, &block) if block
59
- type = type.method(:new) if type.respond_to?(:new)
60
- attribute_initializers[name] = type
61
- attr_reader name
60
+ attribute = attribute!(name)
61
+ attribute.description = description
62
+ attribute.factory = type
62
63
  end
63
64
 
64
65
  # Defines an associative array of sub-components.
@@ -67,53 +68,53 @@ module ConfigMapper
67
68
  # sub-components class.
68
69
  #
69
70
  # @param name [Symbol] dictionary attribute name
70
- # @options options [Proc] :key_type
71
- # function used to validate keys
72
- # @options options [String] :type (ConfigMapper::ConfigStruct)
73
- # base-class for sub-component values
71
+ # @param type [Class] base-class for component values
72
+ # @param key_type [Proc] function used to validate keys
74
73
  #
75
- def component_dict(name, options = {}, &block)
76
- name = name.to_sym
77
- declared_component_dicts << name
78
- type = options.fetch(:type, ConfigStruct)
74
+ def component_dict(name, type: ConfigStruct, key_type: nil, description: nil, &block)
79
75
  type = Class.new(type, &block) if block
80
- type = type.method(:new) if type.respond_to?(:new)
81
- key_type = options[:key_type]
82
- key_type = key_type.method(:new) if key_type.respond_to?(:new)
83
- attribute_initializers[name] = lambda do
84
- ConfigDict.new(type, key_type)
85
- end
86
- attr_reader name
76
+ component(name, type: ConfigDict::Factory.new(type, key_type), description: description)
87
77
  end
88
78
 
89
- def required_attributes
90
- @required_attributes ||= []
79
+ # Generate documentation, as Ruby data.
80
+ #
81
+ # Returns an entry for each configurable path, detailing
82
+ # `description`, `type`, and `default`.
83
+ #
84
+ # @return [Hash] documentation, keyed by path
85
+ #
86
+ def config_doc
87
+ each_attribute.sort_by(&:name).map(&:config_doc).inject({}, :merge)
91
88
  end
92
89
 
93
- def attribute_initializers
94
- @attribute_initializers ||= {}
90
+ def attributes
91
+ attributes_by_name.values
95
92
  end
96
93
 
97
- def declared_components
98
- @declared_components ||= []
94
+ def each_attribute(&action)
95
+ return enum_for(:each_attribute) unless action
96
+ ancestors.each do |klass|
97
+ next unless klass.respond_to?(:attributes)
98
+ klass.attributes.each(&action)
99
+ end
99
100
  end
100
101
 
101
- def declared_component_dicts
102
- @declared_component_dicts ||= []
102
+ private
103
+
104
+ def attributes_by_name
105
+ @attributes_by_name ||= {}
103
106
  end
104
107
 
105
- def for_all(attribute, &action)
106
- ancestors.each do |klass|
107
- next unless klass.respond_to?(attribute)
108
- klass.public_send(attribute).each(&action)
109
- end
108
+ def attribute!(name)
109
+ attr_reader(name)
110
+ attributes_by_name[name] ||= Attribute.new(name)
110
111
  end
111
112
 
112
113
  end
113
114
 
114
115
  def initialize
115
- self.class.for_all(:attribute_initializers) do |name, initializer|
116
- instance_variable_set("@#{name}", initializer.call)
116
+ self.class.each_attribute do |attribute|
117
+ instance_variable_set("@#{attribute.name}", attribute.initial_value)
117
118
  end
118
119
  end
119
120
 
@@ -141,12 +142,12 @@ module ConfigMapper
141
142
  #
142
143
  def to_h
143
144
  {}.tap do |result|
144
- self.class.for_all(:attribute_initializers) do |attr_name, _|
145
- value = send(attr_name)
145
+ self.class.each_attribute do |attribute|
146
+ value = send(attribute.name)
146
147
  if value && value.respond_to?(:to_h) && !value.is_a?(Array)
147
148
  value = value.to_h
148
149
  end
149
- result[attr_name.to_s] = value
150
+ result[attribute.name.to_s] = value
150
151
  end
151
152
  end
152
153
  end
@@ -155,13 +156,9 @@ module ConfigMapper
155
156
 
156
157
  def components
157
158
  {}.tap do |result|
158
- self.class.for_all(:declared_components) do |name|
159
- result[".#{name}"] = instance_variable_get("@#{name}")
160
- end
161
- self.class.for_all(:declared_component_dicts) do |name|
162
- instance_variable_get("@#{name}").each do |key, value|
163
- result[".#{name}[#{key.inspect}]"] = value
164
- end
159
+ self.class.each_attribute do |a|
160
+ next unless a.factory
161
+ result[".#{a.name}"] = instance_variable_get("@#{a.name}")
165
162
  end
166
163
  end
167
164
  end
@@ -176,9 +173,9 @@ module ConfigMapper
176
173
 
177
174
  def missing_required_attribute_errors
178
175
  {}.tap do |errors|
179
- self.class.for_all(:required_attributes) do |name|
180
- if instance_variable_get("@#{name}").nil?
181
- errors[".#{name}"] = NoValueProvided.new
176
+ self.class.each_attribute do |a|
177
+ if a.required && instance_variable_get("@#{a.name}").nil?
178
+ errors[".#{a.name}"] = NoValueProvided.new
182
179
  end
183
180
  end
184
181
  end
@@ -186,13 +183,61 @@ module ConfigMapper
186
183
 
187
184
  def component_config_errors
188
185
  {}.tap do |errors|
189
- components.each do |component_name, component_value|
186
+ components.each do |component_path, component_value|
190
187
  next unless component_value.respond_to?(:config_errors)
191
- component_value.config_errors.each do |key, value|
192
- errors["#{component_name}#{key}"] = value
188
+ component_value.config_errors.each do |path, value|
189
+ errors["#{component_path}#{path}"] = value
190
+ end
191
+ end
192
+ end
193
+ end
194
+
195
+ class Attribute
196
+
197
+ def initialize(name)
198
+ @name = name.to_sym
199
+ end
200
+
201
+ attr_reader :name
202
+
203
+ attr_accessor :description
204
+ attr_accessor :factory
205
+ attr_accessor :validator
206
+ attr_accessor :default
207
+ attr_accessor :required
208
+
209
+ def initial_value
210
+ return factory.new if factory
211
+ default
212
+ end
213
+
214
+ def factory=(arg)
215
+ @factory = Factory.resolve(arg)
216
+ end
217
+
218
+ def config_doc
219
+ self_doc.merge(type_doc)
220
+ end
221
+
222
+ private
223
+
224
+ def self_doc
225
+ {
226
+ ".#{name}" => {}.tap do |doc|
227
+ doc["description"] = description if description
228
+ doc["default"] = default if default
229
+ doc["type"] = String(validator.name) if validator.respond_to?(:name)
193
230
  end
231
+ }
232
+ end
233
+
234
+ def type_doc
235
+ return {} unless factory.respond_to?(:config_doc)
236
+ factory.config_doc.each_with_object({}) do |(path, doc), result|
237
+ result[".#{name}#{path}"] = doc
194
238
  end
195
239
  end
240
+
196
241
  end
197
242
 
198
243
  end
@@ -0,0 +1,25 @@
1
+ module ConfigMapper
2
+
3
+ module Factory
4
+
5
+ def self.resolve(arg)
6
+ return arg if arg.respond_to?(:new)
7
+ return ProcFactory.new(arg) if arg.respond_to?(:call)
8
+ raise ArgumentError, "invalid factory"
9
+ end
10
+
11
+ end
12
+
13
+ class ProcFactory
14
+
15
+ def initialize(f)
16
+ @f = f
17
+ end
18
+
19
+ def new
20
+ @f.call
21
+ end
22
+
23
+ end
24
+
25
+ end
@@ -0,0 +1,16 @@
1
+ module ConfigMapper
2
+
3
+ module Validator
4
+
5
+ def self.resolve(arg)
6
+ return arg if arg.respond_to?(:call)
7
+ if arg.respond_to?(:name)
8
+ # looks like a primitive class -- find the corresponding coercion method
9
+ return Kernel.method(arg.name)
10
+ end
11
+ arg
12
+ end
13
+
14
+ end
15
+
16
+ end
@@ -1,5 +1,5 @@
1
1
  module ConfigMapper
2
2
 
3
- VERSION = "1.4.1".freeze
3
+ VERSION = "1.5.0".freeze
4
4
 
5
5
  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.4.1
4
+ version: 1.5.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: 2017-07-04 00:00:00.000000000 Z
11
+ date: 2017-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -65,8 +65,10 @@ files:
65
65
  - lib/config_mapper/collection_mapper.rb
66
66
  - lib/config_mapper/config_dict.rb
67
67
  - lib/config_mapper/config_struct.rb
68
+ - lib/config_mapper/factory.rb
68
69
  - lib/config_mapper/mapper.rb
69
70
  - lib/config_mapper/object_mapper.rb
71
+ - lib/config_mapper/validator.rb
70
72
  - lib/config_mapper/version.rb
71
73
  homepage: https://github.com/mdub/config_mapper
72
74
  licenses:
@@ -80,7 +82,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
80
82
  requirements:
81
83
  - - ">="
82
84
  - !ruby/object:Gem::Version
83
- version: '0'
85
+ version: '2.0'
84
86
  required_rubygems_version: !ruby/object:Gem::Requirement
85
87
  requirements:
86
88
  - - ">="