config_mapper 1.4.1 → 1.5.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: 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
  - - ">="