configurations 2.0.0.pre → 2.0.0

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