configurations 2.0.0.pre → 2.0.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.
Files changed (60) hide show
  1. checksums.yaml +7 -7
  2. data/.travis.yml +4 -2
  3. data/License.txt +1 -1
  4. data/README.md +23 -7
  5. data/Rakefile +1 -1
  6. data/configurations.gemspec +7 -3
  7. data/lib/configurations.rb +8 -4
  8. data/lib/configurations/arbitrary.rb +124 -0
  9. data/lib/configurations/blank_object.rb +60 -0
  10. data/lib/configurations/configurable.rb +153 -42
  11. data/lib/configurations/configuration.rb +96 -213
  12. data/lib/configurations/strict.rb +170 -0
  13. data/test/configurations/arbitrary/test.rb +23 -0
  14. data/test/configurations/arbitrary/test_defaults.rb +5 -0
  15. data/test/configurations/arbitrary/test_hash_methods.rb +11 -0
  16. data/test/configurations/arbitrary/test_methods.rb +5 -0
  17. data/test/configurations/arbitrary/test_not_configured.rb +5 -0
  18. data/test/configurations/arbitrary/test_not_configured_default.rb +5 -0
  19. data/test/configurations/shared/defaults.rb +43 -0
  20. data/test/configurations/shared/hash_methods.rb +40 -0
  21. data/test/configurations/shared/kernel_methods.rb +9 -0
  22. data/test/configurations/shared/methods.rb +31 -0
  23. data/test/configurations/shared/not_configured_callbacks.rb +42 -0
  24. data/test/configurations/shared/not_configured_default_callback.rb +42 -0
  25. data/test/configurations/shared/properties.rb +46 -0
  26. data/test/configurations/shared/properties_outside_block.rb +40 -0
  27. data/test/configurations/shared/strict_hash_methods.rb +43 -0
  28. data/test/configurations/strict/test.rb +20 -0
  29. data/test/configurations/strict/test_defaults.rb +6 -0
  30. data/test/configurations/strict/test_hash_methods.rb +11 -0
  31. data/test/configurations/strict/test_methods.rb +6 -0
  32. data/test/configurations/strict/test_not_configured.rb +6 -0
  33. data/test/configurations/strict/test_not_configured_default.rb +6 -0
  34. data/test/configurations/strict_types/test.rb +18 -0
  35. data/test/configurations/strict_types/test_defaults.rb +6 -0
  36. data/test/configurations/strict_types/test_hash_methods.rb +11 -0
  37. data/test/configurations/strict_types/test_methods.rb +6 -0
  38. data/test/configurations/strict_types/test_not_configured.rb +6 -0
  39. data/test/configurations/strict_types/test_not_configured_default.rb +6 -0
  40. data/test/configurations/strict_types_with_blocks/test.rb +22 -0
  41. data/test/configurations/strict_types_with_blocks/test_defaults.rb +6 -0
  42. data/test/configurations/strict_types_with_blocks/test_hash_methods.rb +14 -0
  43. data/test/configurations/strict_types_with_blocks/test_methods.rb +6 -0
  44. data/test/configurations/strict_types_with_blocks/test_not_configured.rb +6 -0
  45. data/test/configurations/strict_types_with_blocks/test_not_configured_default.rb +6 -0
  46. data/test/configurations/strict_with_blocks/test.rb +16 -0
  47. data/test/configurations/strict_with_blocks/test_defaults.rb +6 -0
  48. data/test/configurations/strict_with_blocks/test_hash_methods.rb +14 -0
  49. data/test/configurations/strict_with_blocks/test_methods.rb +6 -0
  50. data/test/configurations/strict_with_blocks/test_not_configured.rb +6 -0
  51. data/test/configurations/strict_with_blocks/test_not_configured_default.rb +6 -0
  52. data/test/support/setup.rb +173 -0
  53. data/test/support/shared.rb +11 -0
  54. data/test/test_helper.rb +6 -5
  55. metadata +148 -65
  56. data/test/configurations/test_configurable_with_blocks_configuration.rb +0 -54
  57. data/test/configurations/test_configuration.rb +0 -182
  58. data/test/configurations/test_configuration_methods.rb +0 -85
  59. data/test/configurations/test_stricter_configuration.rb +0 -173
  60. data/test/support/testmodules.rb +0 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
- ---
2
- SHA1:
3
- metadata.gz: e4dac5ba9d58bb68f5ff84dbd03433a9015f976a
4
- data.tar.gz: 7ba636f43c643628e1281c2382dc73c4ee4b4a97
5
- SHA512:
6
- metadata.gz: e2813ce78bbd73297da31cf753832ae05e058bb5d75cdab01a73171302c58c7931979305f4d2dd267e7ab978573134d589ef57a72e6f75887221f791d961dc0f
7
- data.tar.gz: 4b102d91d228b20f77097858c6753bd59e880cf060540a4115a5b3452d3bc930ad12804bb77f6fac25c3741cb67ce373c153e29524235709c5229d5b571acd57
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f9907568167e40b9014fea6fd1999980874e83a8
4
+ data.tar.gz: 848cd43a6bb4e9c41cf9aa6ff21d1b3966fb7238
5
+ SHA512:
6
+ metadata.gz: a60a00a58aa9af53a443fa2781138c734248ac77b5c2f55724cb330eca8a173636445657e8b7f23a4e907fd319cd9eb651a26ad2216c9d80d9d98501f4e7e646
7
+ data.tar.gz: 8b0a6f40548e80e3ba7f21eb6674980b2877ff9f270ed02e0c8aadedea532c350270895ccf12108302c74a076967b14f84d5796669e87083e451b1e45ffaa9a8
@@ -6,8 +6,10 @@ rvm:
6
6
  - 1.9.2
7
7
  - 1.9.3
8
8
  - 2.0.0
9
- - 2.1.0
9
+ - 2.1.5
10
+ - 2.2.0
10
11
  - rbx
11
12
  - jruby-19mode
12
-
13
+ - jruby-20mode
14
+
13
15
  script: CODECLIMATE_REPO_TOKEN=36cf84c73264d3c361003f66903eec8aa5fb2b3494496f3a9676630518ecc9f9 rake
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014 Beat Richartz
1
+ Copyright (c) 2015 Beat Richartz
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -10,13 +10,13 @@ Configurations provides a unified approach to do configurations using the `MyGem
10
10
 
11
11
  or with Bundler
12
12
 
13
- `gem 'configurations', '~> 2.0.0.pre'`
13
+ `gem 'configurations', '~> 2.0.0'`
14
14
 
15
15
  Configurations uses [Semver 2.0](http://semver.org/)
16
16
 
17
17
  ## Compatibility
18
18
 
19
- Compatible with MRI 1.9.2 - 2.1, Rubinius, jRuby
19
+ Compatible with MRI 1.9.2 - 2.2, Rubinius 2.2, jRuby 1.7 and 9K
20
20
 
21
21
  ## Why?
22
22
 
@@ -61,7 +61,7 @@ Undefined properties on an arbitrary configuration will return `nil`
61
61
  MyGem.configuration.not_set #=> nil
62
62
  ```
63
63
 
64
- If you want to define the behaviour for not set properties yourself, use `not_configured`.
64
+ If you want to define the behaviour for not set properties yourself, use `not_configured`. You can either define a catch-all `not_configured` which will be executed whenever you call a value that has not been configured and has no default:
65
65
 
66
66
  ```
67
67
  module MyGem
@@ -71,6 +71,15 @@ module MyGem
71
71
  end
72
72
  ```
73
73
 
74
+ Or you can define finer-grained callbacks:
75
+
76
+ ```
77
+ module MyGem
78
+ not_configured my: { nested: :prop } do |prop|
79
+ raise NoMethodError, "#{prop} must be configured"
80
+ end
81
+ end
82
+ ```
74
83
 
75
84
  ### Second way: Restricted Configuration
76
85
 
@@ -114,7 +123,7 @@ If you want to define the behaviour for not set properties yourself, use `not_co
114
123
 
115
124
  ```
116
125
  module MyGem
117
- not_configured do |prop|
126
+ not_configured :awesome, :nice do |prop| # omit the arguments to get a catch-all not_configured
118
127
  warn :not_configured, "Please configure #{prop} or live in danger"
119
128
  end
120
129
  end
@@ -219,6 +228,13 @@ You get:
219
228
  MyGem.configuration.foobar('ARG') #=> 'FOOBARARG'
220
229
  ```
221
230
 
231
+ configuration methods can also be installed on nested properties using hashes:
232
+
233
+ ```
234
+ configuration_method foo: :bar do |arg|
235
+ foo + bar + arg
236
+ end
237
+ ```
222
238
 
223
239
  ### Defaults:
224
240
 
@@ -239,7 +255,7 @@ MyGem.configuration.to_h #=> a Hash
239
255
 
240
256
  ### Configure with a hash where needed
241
257
 
242
- Sometimes your users will have a hash of configuration values which are not handy to press into the block form. In that case, they can use `from_h` inside the `configure` block to either read in the full or a nested configuration.
258
+ Sometimes your users will have a hash of configuration values which are not handy to press into the block form. In that case, they can use `from_h` inside the `configure` block to either read in the full or a nested configuration. With a everything besides arbitrary configurations, `from_h` can also be used outside the block.
243
259
 
244
260
  ```
245
261
  yaml_hash = YAML.load_file('configuration.yml')
@@ -252,7 +268,7 @@ end
252
268
 
253
269
  ### Some caveats
254
270
 
255
- The `to_h` from above is along with `method_missing`, `object_id` and `initialize` the only purposely defined API method which you can not overwrite with a configuration value.
271
+ The `to_h` from above is along with `method_missing`, `object_id` and `initialize` and `singleton_class` the only purposely defined API method which you can not overwrite with a configuration value.
256
272
  Apart from these methods, you should be able to set pretty much any property name you like. `Configuration` inherits from `BasicObject`, so even `Kernel` and `Object` method names are available.
257
273
 
258
274
  ## Contributing
@@ -263,4 +279,4 @@ Let's make this awesome. Write tests for your added stuff, bonus points for feat
263
279
 
264
280
  ### Copyright
265
281
 
266
- Copyright © 2014 Beat Richartz. See LICENSE.txt for further details.
282
+ Copyright © 2015 Beat Richartz. See LICENSE.txt for further details.
data/Rakefile CHANGED
@@ -4,5 +4,5 @@ task default: :test
4
4
 
5
5
  Rake::TestTask.new do |t|
6
6
  t.libs << 'test'
7
- t.pattern = 'test/**/test_*.rb'
7
+ t.pattern = 'test/**/test*.rb'
8
8
  end
@@ -5,13 +5,17 @@ Gem::Specification.new do |s|
5
5
  s.name = 'configurations'
6
6
  s.version = Configurations::VERSION
7
7
  s.authors = ['Beat Richartz']
8
- s.description = 'Configurations provides a unified approach to do configurations with the flexibility to do everything from arbitrary configurations to type asserted configurations for your gem or any other ruby code.'
8
+ s.description = <<-DESCRIPTION
9
+ Configurations provides a unified approach to do configurations with the flexibility to do everything
10
+ from arbitrary configurations to type asserted configurations for your gem or any other ruby code.
11
+ DESCRIPTION
9
12
  s.email = 'attr_accessor@gmail.com'
10
13
  s.homepage = 'http://github.com/beatrichartz/configurations'
11
14
  s.licenses = %w(MIT)
12
15
  s.require_paths = %w(lib)
13
- s.summary = 'Configurations with a configure block from arbitrary to type-restricted for your gem or other ruby code.'
14
-
16
+ s.summary = <<-SUMMARY
17
+ Configurations with a configure block from arbitrary to type-restricted for your gem or other ruby code.
18
+ SUMMARY
15
19
  s.files = `git ls-files`.split("\n")
16
20
  s.test_files = `git ls-files -- test/*`.split("\n")
17
21
 
@@ -1,10 +1,14 @@
1
1
  require_relative 'configurations/error'
2
+ require_relative 'configurations/blank_object'
2
3
  require_relative 'configurations/configuration'
4
+ require_relative 'configurations/arbitrary'
5
+ require_relative 'configurations/strict'
3
6
  require_relative 'configurations/configurable'
4
7
 
5
- # Configurations provides a unified approach to do configurations with the flexibility to do everything
6
- # from arbitrary configurations to type asserted configurations for your gem or any other ruby code.
7
- # @version 1.0.0
8
+ # Configurations provides a unified approach to do configurations
9
+ # with the flexibility to do everything from arbitrary configurations
10
+ # to type asserted configurations for your gem or any other ruby code.
11
+ # @version 2.0.0
8
12
  # @author Beat Richartz
9
13
  #
10
14
  module Configurations
@@ -12,5 +16,5 @@ module Configurations
12
16
 
13
17
  # Version number of Configurations
14
18
  #
15
- VERSION = '2.0.0.pre'
19
+ VERSION = '2.0.0'
16
20
  end
@@ -0,0 +1,124 @@
1
+ # coding: utf-8
2
+ module Configurations
3
+ # Configuration is a blank object in order to allow configuration of
4
+ # various properties including keywords
5
+ #
6
+ class ArbitraryConfiguration < Configuration
7
+ # Initialize a new configuration
8
+ # @param [Hash] options The options to initialize a configuration with
9
+ # @option options [Hash] methods a hash of method names pointing to procs
10
+ # @option options [Proc] not_configured a proc to evaluate for
11
+ # not_configured properties
12
+ # @param [Proc] block a block to configure this configuration with
13
+ # @yield [HostModule::Configuration] a configuration
14
+ # @return [HostModule::Configuration] a configuration
15
+ # @note An arbitrary configuration has to control its writeable state,
16
+ # therefore configuration is only possible in the initialization block
17
+ #
18
+ def initialize(options = {}, &block)
19
+ self.__writeable__ = true
20
+ super
21
+ self.__writeable__ = false if block
22
+ end
23
+
24
+ # Method missing gives access for reading and writing to the underlying
25
+ # configuration hash via dot notation
26
+ #
27
+ def method_missing(method, *args, &block)
28
+ if __respond_to_writer?(method)
29
+ __assign!(method.to_s[0..-2].to_sym, args.first)
30
+ elsif __respond_to_method_for_write?(method)
31
+ @data[method]
32
+ elsif __respond_to_method_for_read?(method, *args, &block)
33
+ @data.fetch(method, &__not_configured_callback_for__(method))
34
+ else
35
+ super
36
+ end
37
+ end
38
+
39
+ # Respond to missing according to the method_missing implementation
40
+ #
41
+ def respond_to_missing?(method, include_private = false)
42
+ __respond_to_writer?(method) ||
43
+ __respond_to_method_for_read?(method, *args, &block) ||
44
+ __respond_to_method_for_write?(method) ||
45
+ super
46
+ end
47
+
48
+ # A convenience accessor to instantiate a configuration from a hash
49
+ # @param [Hash] h the hash to read into the configuration
50
+ # @return [Configuration] the configuration with values assigned
51
+ # @note can only be accessed during writeable state (in configure block).
52
+ # Unassignable values are ignored
53
+ # @raise [ArgumentError] unless used in writeable state (in configure block)
54
+ #
55
+ def from_h(h)
56
+ unless @__writeable__
57
+ fail ::ArgumentError, 'can not dynamically assign values from a hash'
58
+ end
59
+
60
+ super
61
+ end
62
+
63
+ # @param [Symbol] property The property to test for configurability
64
+ # @return [Boolean] whether the given property is configurable
65
+ #
66
+ def __configurable?(_property)
67
+ true
68
+ end
69
+
70
+ # Set the configuration to writeable or read only. Access to writer methods
71
+ # is only allowed within the configure block, this method is used to invoke
72
+ # writeability for subconfigurations.
73
+ # @param [Boolean] data true if the configuration should be writeable, false
74
+ # otherwise
75
+ #
76
+ def __writeable__=(data)
77
+ @__writeable__ = data
78
+ return if @data.nil?
79
+
80
+ @data.each do |_k, v|
81
+ v.__writeable__ = data if v.is_a?(__class__)
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ # @param [Symbol] property The property to test for
88
+ # @return [Boolean] whether the given property has been configured
89
+ #
90
+ def __configured?(_property)
91
+ true
92
+ end
93
+
94
+ # @param [Symbol] method the method to test for
95
+ # @return [Boolean] whether the given method is a writer
96
+ #
97
+ def __is_writer?(method)
98
+ method.to_s.end_with?('=')
99
+ end
100
+
101
+ # @param [Symbol] method the method to test for
102
+ # @return [Boolean] whether the configuration responds to the given property
103
+ # as a method during writeable state
104
+ #
105
+ def __respond_to_method_for_write?(method)
106
+ !__is_writer?(method) && @__writeable__ && @data[method].is_a?(__class__)
107
+ end
108
+
109
+ # @param [Symbol] method the method to test for
110
+ # @return [Boolean] whether the configuration responds to the given property
111
+ #
112
+ def __respond_to_method_for_read?(method, *args, &block)
113
+ !__is_writer?(method) && args.empty? && block.nil?
114
+ end
115
+
116
+ # @param [Symbol] method the method to test for
117
+ # @return [Boolean] whether the method is a writer and is used in writeable
118
+ # state
119
+ #
120
+ def __respond_to_writer?(method)
121
+ @__writeable__ && __is_writer?(method)
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,60 @@
1
+ module Configurations
2
+ # Create a blank object with some kernel methods
3
+ #
4
+ class BlankObject < ::BasicObject
5
+ # The instance methods to keep on the blank object.
6
+ #
7
+ KEEP_METHODS = [
8
+ :equal?,
9
+ :object_id,
10
+ :__id__,
11
+ :__send__,
12
+ :method_missing
13
+ ].freeze
14
+
15
+ # The kernel methods to alias to an internal name
16
+ #
17
+ ALIAS_KERNEL_METHODS = {
18
+ __class__: :class,
19
+ __instance_eval__: :instance_eval,
20
+ __define_singleton_method__: :define_singleton_method
21
+ }.freeze
22
+
23
+ # The kernel methods to keep on the blank object
24
+ #
25
+ KEEP_KERNEL_METHODS = [
26
+ :respond_to?,
27
+ :is_a?,
28
+ :inspect,
29
+ :object_id,
30
+ # rbx needs the singleton class to access singleton methods
31
+ :singleton_class,
32
+ *ALIAS_KERNEL_METHODS.keys
33
+ ].freeze
34
+
35
+ # Undefines every instance method except the kept methods
36
+ #
37
+ (instance_methods - KEEP_METHODS).each do |method|
38
+ undef_method method
39
+ end
40
+
41
+ # @return [Module] A Kernel module with only the methods
42
+ # defined in KEEP_KERNEL_METHODS
43
+ #
44
+ def self.blank_kernel
45
+ kernel = ::Kernel.dup
46
+
47
+ ALIAS_KERNEL_METHODS.each do |new_name, old_name|
48
+ kernel.module_eval { alias_method new_name, old_name }
49
+ end
50
+
51
+ (kernel.instance_methods - KEEP_KERNEL_METHODS).each do |method|
52
+ kernel.module_eval { undef_method method }
53
+ end
54
+
55
+ kernel
56
+ end
57
+
58
+ include blank_kernel
59
+ end
60
+ end
@@ -4,7 +4,8 @@ module Configurations
4
4
  module Configurable
5
5
  extend self
6
6
 
7
- # Once included, Configurations installs three methods in the host module: configure, configuration_defaults and configurable
7
+ # Once included, Configurations installs three methods in the host module:
8
+ # configure, configuration_defaults and configurable
8
9
  #
9
10
  def included(base)
10
11
  install_configure_in(base)
@@ -13,29 +14,25 @@ module Configurations
13
14
  end
14
15
  end
15
16
 
16
- # Installs #configure in base, and makes sure that it will instantiate configuration as a subclass of the host module
17
+ # Installs #configure in base, and makes sure that it will instantiate
18
+ # configuration as a subclass of the host module
17
19
  #
18
20
  def install_configure_in(base)
19
21
  base.class_eval <<-EOF
20
- class << self
21
- # The central configure method
22
- # @params [Proc] block the block to configure host module with
23
- # @raise [ArgumentError] error when not given a block
24
- # @example Configure a configuration
25
- # MyGem.configure do |c|
26
- # c.foo = :bar
27
- # end
28
- #
29
- def configure(&block)
30
- raise ArgumentError, 'can not configure without a block' unless block_given?
31
- @configuration = #{base.name}::Configuration.new(
32
- defaults: @configuration_defaults,
33
- methods: @configuration_methods,
34
- configurable: @configurable,
35
- not_configured: @not_configured_callback,
36
- &block
37
- )
38
- end
22
+ # The central configure method
23
+ # @params [Proc] block the block to configure host module with
24
+ # @raise [ArgumentError] error when not given a block
25
+ # @example Configure a configuration
26
+ # MyGem.configure do |c|
27
+ # c.foo = :bar
28
+ # end
29
+ #
30
+ def self.configure(&block)
31
+ fail ArgumentError, "configure needs a block" unless block_given?
32
+ @configuration = #{base.name}.const_get(configuration_type).new(
33
+ configuration_options,
34
+ &block
35
+ )
39
36
  end
40
37
  EOF
41
38
  end
@@ -46,70 +43,121 @@ module Configurations
46
43
  # A reader for Configuration
47
44
  #
48
45
  def configuration
49
- @configuration ||= @configuration_defaults && configure { }
46
+ @configuration ||= @configuration_defaults && configure {}
50
47
  end
51
48
 
52
- # Configuration defaults can be used to set the defaults of any Configuration
49
+ # Configuration defaults can be used to set the defaults of
50
+ # any Configuration
53
51
  # @param [Proc] block setting the default values of the configuration
54
52
  #
55
53
  def configuration_defaults(&block)
56
54
  @configuration_defaults = block
57
55
  end
58
56
 
59
- # configurable can be used to set the properties which should be configurable, as well as a type which
60
- # the given property should be asserted to
61
- # @param [Class, Symbol, Hash] properties a type as a first argument to type assert (if any) or nested properties to allow for setting
62
- # @param [Proc] block a block with arity 2 to evaluate when a property is set. It will be given: property name and value
57
+ # configurable can be used to set the properties which should be
58
+ # configurable, as well as a type which the given property should
59
+ # be asserted to
60
+ # @param [Class, Symbol, Hash] properties a type as a first argument to
61
+ # type assert (if any) or nested properties to allow for setting
62
+ # @param [Proc] block a block with arity 2 to evaluate when a property
63
+ # is set. It will be given: property name and value
63
64
  # @example Define a configurable property
64
65
  # configurable :foo
65
66
  # @example Define a type asserted, nested property for type String
66
67
  # configurable String, bar: :baz
67
68
  # @example Define a custom assertion for a property
68
69
  # configurable biz: %i(bi bu) do |value|
69
- # raise ArgumentError, 'must be one of a, b, c' unless %w(a b c).include?(value)
70
+ # unless %w(a b c).include?(value)
71
+ # fail ArgumentError, 'must be one of a, b, c'
72
+ # end
70
73
  # end
71
74
  #
72
75
  def configurable(*properties, &block)
73
76
  type = properties.shift if properties.first.is_a?(Class)
77
+
74
78
  @configurable ||= {}
75
79
  @configurable.merge! to_configurable_hash(properties, type, &block)
76
80
  end
77
81
 
78
82
  # returns whether a property is set to be configurable
79
83
  # @param [Symbol] property the property to ask status for
84
+ # @return [Boolean] whether the property is configurable
80
85
  #
81
86
  def configurable?(property)
82
- @configurable.is_a?(Hash) && @configurable.has_key?(property)
87
+ @configurable.is_a?(Hash) && @configurable.key?(property)
83
88
  end
84
89
 
85
- # configuration method can be used to retrieve properties from the configuration which use your gem's context
90
+ # configuration method can be used to retrieve properties
91
+ # from the configuration
92
+ # which use your gem's context
86
93
  # @param [Class, Symbol, Hash] method the method to define
87
94
  # @param [Proc] block the block to evaluate
88
- # @example Define a configuration method 'foobararg' returning configuration properties 'foo' and 'bar' plus an argument
95
+ # @example Define a configuration method 'foobararg'
89
96
  # configuration_method :foobararg do |arg|
90
97
  # foo + bar + arg
91
98
  # end
99
+ # @example Define a configuration method on a nested property
100
+ # configuration_method foo: { bar: :arg } do
101
+ # baz + biz
102
+ # end
92
103
  #
93
104
  def configuration_method(method, &block)
94
- raise ArgumentError, "can not be both a configurable property and a configuration method" if configurable?(method)
105
+ fail ArgumentError, "can't be configuration property and a method" if configurable?(method)
106
+
95
107
  @configuration_methods ||= {}
96
- @configuration_methods.merge! method => block
108
+ method_hash = if method.is_a?(Hash)
109
+ ingest_configuration_block!(method, &block)
110
+ else
111
+ { method => block }
112
+ end
113
+
114
+ @configuration_methods.merge! method_hash
97
115
  end
98
116
 
99
- # not_configured defines the behaviour when a property has not been configured
100
- # This can be useful for presence validations of certain properties
101
- # or behaviour for undefined properties deviating from the original behaviour
102
- # @param [Proc] block the block to evaluate
117
+ # not_configured defines the behaviour when a property has not been
118
+ # configured. This can be useful for presence validations of certain
119
+ # properties or behaviour for undefined properties deviating from the
120
+ # original behaviour.
121
+ # @param [Array, Symbol, Hash] properties the properties to install
122
+ # the callback on. If omitted, the callback will be installed on
123
+ # all properties that have no specific callbacks
124
+ # @param [Proc] block the block to evaluate when a property
125
+ # has not been configured
103
126
  # @yield [Symbol] the property that has not been configured
127
+ # @example Define a specific not_configured callback
128
+ # not_configured :property1, property2: :property3 do |property|
129
+ # raise ArgumentError, "#{property} should be configured"
130
+ # end
131
+ # @example Define a catch-all not_configured callback
132
+ # not_configured do |property|
133
+ # raise StandardError, "You did not configure #{property}"
134
+ # end
135
+ #
136
+ def not_configured(*properties, &block)
137
+ @not_configured ||= {}
138
+
139
+ if properties.empty?
140
+ @not_configured.default_proc = ->(h, k) { h[k] = block }
141
+ else
142
+ nested_merge_not_configured_hash(*properties, &block)
143
+ end
144
+ end
145
+
146
+ # @return the class name of the configuration class to use
104
147
  #
105
- def not_configured(&block)
106
- @not_configured_callback = block
148
+ def configuration_type
149
+ if @configurable.nil? || @configurable.empty?
150
+ :ArbitraryConfiguration
151
+ else
152
+ :StrictConfiguration
153
+ end
107
154
  end
108
155
 
109
156
  private
110
157
 
111
158
  # Instantiates a configurable hash from a property and a type
112
- # @param [Symbol, Hash, Array] properties configurable properties, either single or nested
159
+ # @param [Symbol, Hash, Array] properties configurable properties,
160
+ # either single or nested
113
161
  # @param [Class] type the type to assert, if any
114
162
  # @return a hash with configurable values pointing to their types
115
163
  #
@@ -118,8 +166,71 @@ module Configurations
118
166
  assertion_hash.merge! block: block if block_given?
119
167
  assertion_hash.merge! type: type if type
120
168
 
121
- assertions = ([assertion_hash] * properties.size)
122
- Hash[properties.zip(assertions)]
169
+ zip_to_hash(assertion_hash, *properties)
170
+ end
171
+
172
+ # Makes all values of hash point to block
173
+ # @param [Hash] hash the hash to modify
174
+ # @param [Proc] block the block to point to
175
+ # @return a hash with all previous values being keys pointing to block
176
+ #
177
+ def ingest_configuration_block!(hash, &block)
178
+ hash.each do |k, v|
179
+ value = if v.is_a?(Hash)
180
+ ingest_configuration_block!(v, &block)
181
+ else
182
+ zip_to_hash(block, *Array(v))
183
+ end
184
+
185
+ hash.merge! k => value
186
+ end
187
+ end
188
+
189
+ # @return a hash of configuration options with no nil values
190
+ #
191
+ def configuration_options
192
+ {
193
+ defaults: @configuration_defaults,
194
+ methods: @configuration_methods,
195
+ configurable: @configurable,
196
+ not_configured: @not_configured
197
+ }.delete_if { |_, value| value.nil? }
198
+ end
199
+
200
+ # merges the properties given into a not_configured hash
201
+ # @param [Symbol, Hash, Array] properties the properties to merge
202
+ # @param [Proc] block the block to point the properties to when
203
+ # not configured
204
+ #
205
+ def nested_merge_not_configured_hash(*properties, &block)
206
+ nested = properties.last.is_a?(Hash) ? properties.pop : {}
207
+ nested = ingest_configuration_block!(nested, &block)
208
+ props = zip_to_hash(block, *properties)
209
+
210
+ @not_configured.merge! nested, &method(:configuration_deep_merge)
211
+ @not_configured.merge! props, &method(:configuration_deep_merge)
212
+ end
213
+
214
+ # Solves merge conflicts when merging
215
+ # @param [Symbol] key the key that conflicts
216
+ # @param [Anything] oldval the value of the left side of the merge
217
+ # @param [Anything] newval the value of the right side of the merge
218
+ # @return a mergable value with conflicts solved
219
+ #
220
+ def configuration_deep_merge(_key, oldval, newval)
221
+ if oldval.is_a?(Hash) && newval.is_a?(Hash)
222
+ oldval.merge(newval, &method(:configuration_deep_merge))
223
+ else
224
+ Array(oldval) + Array(newval)
225
+ end
226
+ end
227
+
228
+ # Zip a value with keys to a hash so all keys point to the value
229
+ # @param [Anything] value the value to point to
230
+ # @param [Array] keys the keys to install
231
+ #
232
+ def zip_to_hash(value, *keys)
233
+ Hash[keys.zip([value] * keys.size)]
123
234
  end
124
235
  end
125
236
  end