dry-configurable 0.12.1 → 1.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.
@@ -1,11 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/configurable/constants'
4
- require 'dry/configurable/setting'
5
- require 'dry/configurable/settings'
6
- require 'dry/configurable/compiler'
7
- require 'dry/configurable/dsl/args'
8
-
9
3
  module Dry
10
4
  module Configurable
11
5
  # Setting DSL used by the class API
@@ -14,49 +8,67 @@ module Dry
14
8
  class DSL
15
9
  VALID_NAME = /\A[a-z_]\w*\z/i.freeze
16
10
 
17
- # @api private
18
11
  attr_reader :compiler
19
12
 
20
- # @api private
21
13
  attr_reader :ast
22
14
 
23
- # @api private
24
- def initialize(&block)
15
+ attr_reader :options
16
+
17
+ def initialize(**options, &block)
25
18
  @compiler = Compiler.new
26
19
  @ast = []
20
+ @options = options
27
21
  instance_exec(&block) if block
28
22
  end
29
23
 
30
- # Register a new setting node and compile it into a setting object
24
+ # Registers a new setting node and compile it into a setting object
31
25
  #
32
26
  # @see ClassMethods.setting
33
- # @api public
27
+ # @api private
34
28
  # @return Setting
35
- def setting(name, *args, &block)
29
+ def setting(name, **options, &block) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
36
30
  unless VALID_NAME.match?(name.to_s)
37
31
  raise ArgumentError, "#{name} is not a valid setting name"
38
32
  end
39
33
 
40
- args = Args.new(args)
41
-
42
- args.ensure_valid_options
34
+ ensure_valid_options(options)
43
35
 
44
- default, opts = args
36
+ options = {default: default, config_class: config_class, **options}
45
37
 
46
- node = [:setting, [name.to_sym, default, opts == default ? EMPTY_HASH : opts]]
38
+ node = [:setting, [name.to_sym, options]]
47
39
 
48
40
  if block
49
- if block.arity.zero?
50
- ast << [:nested, [node, DSL.new(&block).ast]]
51
- else
52
- ast << [:constructor, [node, block]]
53
- end
41
+ ast << [:nested, [node, DSL.new(&block).ast]]
54
42
  else
55
43
  ast << node
56
44
  end
57
45
 
58
46
  compiler.visit(ast.last)
59
47
  end
48
+
49
+ def config_class
50
+ options[:config_class]
51
+ end
52
+
53
+ def default
54
+ options[:default_undefined] ? Undefined : nil
55
+ end
56
+
57
+ private
58
+
59
+ def ensure_valid_options(options)
60
+ return if options.none?
61
+
62
+ invalid_keys = options.keys - Setting::OPTIONS
63
+
64
+ raise ArgumentError, "Invalid options: #{invalid_keys.inspect}" unless invalid_keys.empty?
65
+ end
66
+
67
+ # Returns a tuple of valid and invalid options hashes derived from the options hash
68
+ # given to the setting
69
+ def valid_and_invalid_options(options)
70
+ options.partition { |k, _| Setting::OPTIONS.include?(k) }.map(&:to_h)
71
+ end
60
72
  end
61
73
  end
62
74
  end
@@ -6,7 +6,8 @@ module Dry
6
6
  # @api public
7
7
  module Configurable
8
8
  Error = Class.new(::StandardError)
9
- AlreadyIncluded = ::Class.new(Error)
10
- FrozenConfig = ::Class.new(Error)
9
+
10
+ AlreadyIncludedError = Class.new(Error)
11
+ FrozenConfigError = Class.new(Error)
11
12
  end
12
13
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Configurable
5
+ class Extension < Module
6
+ # @api private
7
+ attr_reader :config_class
8
+
9
+ # @api private
10
+ attr_reader :default_undefined
11
+
12
+ # @api private
13
+ def initialize(config_class: Configurable::Config, default_undefined: false)
14
+ super()
15
+ @config_class = config_class
16
+ @default_undefined = default_undefined
17
+ freeze
18
+ end
19
+
20
+ # @api private
21
+ def extended(klass)
22
+ super
23
+ klass.extend(ClassMethods)
24
+ klass.instance_variable_set(:@__config_extension__, self)
25
+ end
26
+
27
+ # @api private
28
+ def included(klass)
29
+ raise AlreadyIncludedError if klass.include?(InstanceMethods)
30
+
31
+ super
32
+
33
+ klass.class_eval do
34
+ extend(ClassMethods)
35
+ include(InstanceMethods)
36
+ prepend(Initializer)
37
+
38
+ class << self
39
+ undef :config
40
+ undef :configure
41
+ end
42
+ end
43
+
44
+ klass.instance_variable_set(:@__config_extension__, self)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/configurable/config'
4
- require 'dry/configurable/methods'
3
+ require "dry/configurable/config"
4
+ require "dry/configurable/methods"
5
5
 
6
6
  module Dry
7
7
  module Configurable
@@ -12,9 +12,11 @@ module Dry
12
12
  module Initializer
13
13
  # @api private
14
14
  def initialize(*)
15
- @config = Config.new(self.class._settings.dup)
15
+ @config = self.class.__config_build__(self.class.settings)
16
+
16
17
  super
17
18
  end
19
+ ruby2_keywords(:initialize) if respond_to?(:ruby2_keywords, true)
18
20
  end
19
21
 
20
22
  # Instance-level API when `Dry::Configurable` is included in a class
@@ -33,9 +35,7 @@ module Dry
33
35
  # Finalize the config and freeze the object
34
36
  #
35
37
  # @api public
36
- def finalize!
37
- return self if frozen?
38
-
38
+ def finalize!(freeze_values: false)
39
39
  super
40
40
  freeze
41
41
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/configurable/errors'
4
-
5
3
  module Dry
6
4
  module Configurable
7
5
  # Common API for both classes and instances
@@ -10,7 +8,7 @@ module Dry
10
8
  module Methods
11
9
  # @api public
12
10
  def configure(&block)
13
- raise FrozenConfig, 'Cannot modify frozen config' if frozen?
11
+ raise FrozenConfigError, "Cannot modify frozen config" if config.frozen?
14
12
 
15
13
  yield(config) if block
16
14
  self
@@ -21,10 +19,8 @@ module Dry
21
19
  # @return [Dry::Configurable::Config]
22
20
  #
23
21
  # @api public
24
- def finalize!
25
- return self if config.frozen?
26
-
27
- config.finalize!
22
+ def finalize!(freeze_values: false)
23
+ config.finalize!(freeze_values: freeze_values)
28
24
  self
29
25
  end
30
26
  end
@@ -1,107 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
4
-
5
- require 'dry/core/equalizer'
6
-
7
- require 'dry/configurable/constants'
8
- require 'dry/configurable/config'
3
+ require "set"
9
4
 
10
5
  module Dry
11
6
  module Configurable
12
- # This class represents a setting and is used internally.
7
+ # A defined setting.
13
8
  #
14
- # @api private
9
+ # @api public
15
10
  class Setting
16
- include Dry::Equalizer(:name, :value, :options, inspect: false)
11
+ include Dry::Equalizer(:name, :default, :constructor, :children, :options, inspect: false)
17
12
 
18
- OPTIONS = %i[input default reader constructor cloneable settings].freeze
13
+ OPTIONS = %i[default reader constructor mutable cloneable settings config_class].freeze
19
14
 
20
15
  DEFAULT_CONSTRUCTOR = -> v { v }.freeze
21
16
 
22
- CLONEABLE_VALUE_TYPES = [Array, Hash, Set, Config].freeze
17
+ MUTABLE_VALUE_TYPES = [Array, Hash, Set, Config].freeze
23
18
 
24
- # @api private
19
+ # @api public
25
20
  attr_reader :name
26
21
 
27
- # @api private
28
- attr_reader :writer_name
29
-
30
- # @api private
31
- attr_reader :input
32
-
33
- # @api private
22
+ # @api public
34
23
  attr_reader :default
35
24
 
36
- # @api private
37
- attr_reader :options
25
+ # @api public
26
+ attr_reader :mutable
38
27
 
39
- # Specialized Setting which includes nested settings
40
- #
41
- # @api private
42
- class Nested < Setting
43
- CONSTRUCTOR = Config.method(:new)
28
+ # @api public
29
+ attr_reader :constructor
44
30
 
45
- # @api private
46
- def pristine
47
- with(input: input.pristine)
48
- end
31
+ # @api public
32
+ attr_reader :children
49
33
 
50
- # @api private
51
- def constructor
52
- CONSTRUCTOR
53
- end
54
- end
34
+ # @api public
35
+ attr_reader :options
55
36
 
56
37
  # @api private
57
- def self.cloneable_value?(value)
58
- CLONEABLE_VALUE_TYPES.any? { |type| value.is_a?(type) }
38
+ def self.mutable_value?(value)
39
+ MUTABLE_VALUE_TYPES.any? { |type| value.is_a?(type) }
59
40
  end
60
41
 
61
42
  # @api private
62
- def initialize(name, input: Undefined, default: Undefined, **options)
43
+ def initialize(
44
+ name,
45
+ default:,
46
+ constructor: DEFAULT_CONSTRUCTOR,
47
+ children: EMPTY_ARRAY,
48
+ **options
49
+ )
63
50
  @name = name
64
- @writer_name = :"#{name}="
65
- @input = input
66
51
  @default = default
52
+ @mutable = children.any? || options.fetch(:mutable) {
53
+ # Allow `cloneable` as an option alias for `mutable`
54
+ options.fetch(:cloneable) { Setting.mutable_value?(default) }
55
+ }
56
+ @constructor = constructor
57
+ @children = children
67
58
  @options = options
68
-
69
- evaluate if input_defined?
70
- end
71
-
72
- # @api private
73
- def input_defined?
74
- !input.equal?(Undefined)
75
- end
76
-
77
- # @api private
78
- def value
79
- @value ||= evaluate
80
- end
81
-
82
- # @api private
83
- def evaluated?
84
- instance_variable_defined?(:@value)
85
- end
86
-
87
- # @api private
88
- def nested(settings)
89
- Nested.new(name, input: settings, **options)
90
- end
91
-
92
- # @api private
93
- def pristine
94
- with(input: Undefined)
95
- end
96
-
97
- # @api private
98
- def with(new_opts)
99
- self.class.new(name, input: input, default: default, **options, **new_opts)
100
- end
101
-
102
- # @api private
103
- def constructor
104
- options[:constructor] || DEFAULT_CONSTRUCTOR
105
59
  end
106
60
 
107
61
  # @api private
@@ -109,43 +63,23 @@ module Dry
109
63
  options[:reader].equal?(true)
110
64
  end
111
65
 
112
- # @api private
113
- def writer?(meth)
114
- writer_name.equal?(meth)
66
+ # @api public
67
+ def mutable?
68
+ mutable
115
69
  end
70
+ alias_method :cloneable?, :mutable?
116
71
 
117
72
  # @api private
118
- def cloneable?
119
- if options.key?(:cloneable)
120
- # Return cloneable option if explicitly set
121
- options[:cloneable]
73
+ def to_value
74
+ if children.any?
75
+ (options[:config_class] || Config).new(children)
122
76
  else
123
- # Otherwise, infer cloneable from any of the input, default, or value
124
- Setting.cloneable_value?(input) || Setting.cloneable_value?(default) || (
125
- evaluated? && Setting.cloneable_value?(value)
126
- )
127
- end
128
- end
129
-
130
- private
77
+ value = default
78
+ value = constructor.(value) unless value.eql?(Undefined)
131
79
 
132
- # @api private
133
- def initialize_copy(source)
134
- super
135
-
136
- @options = source.options.dup
137
-
138
- if source.cloneable?
139
- @input = source.input.dup
140
- @default = source.default.dup
141
- @value = source.value.dup if source.evaluated?
80
+ mutable? ? value.dup : value
142
81
  end
143
82
  end
144
-
145
- # @api private
146
- def evaluate
147
- @value = constructor[Undefined.coalesce(input, default, nil)]
148
- end
149
83
  end
150
84
  end
151
85
  end
@@ -1,71 +1,64 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'concurrent/map'
4
-
5
- require 'dry/core/equalizer'
6
- require 'dry/configurable/constants'
7
-
8
3
  module Dry
9
4
  module Configurable
10
- # A settings map
5
+ # A collection of defined settings on a given class.
11
6
  #
12
7
  # @api private
13
8
  class Settings
14
- include Dry::Equalizer(:elements)
9
+ include Dry::Equalizer(:settings)
15
10
 
16
11
  include Enumerable
17
12
 
18
13
  # @api private
19
- attr_reader :elements
14
+ attr_reader :settings
15
+
16
+ # @api private
17
+ def initialize(settings = EMPTY_ARRAY)
18
+ @settings = settings.each_with_object({}) { |s, m| m[s.name] = s }
19
+ end
20
20
 
21
21
  # @api private
22
- def initialize(elements = EMPTY_ARRAY)
23
- initialize_elements(elements)
22
+ private def initialize_copy(source)
23
+ @settings = source.settings.dup
24
24
  end
25
25
 
26
26
  # @api private
27
27
  def <<(setting)
28
- elements[setting.name] = setting
28
+ settings[setting.name] = setting
29
29
  self
30
30
  end
31
31
 
32
- # @api private
32
+ # Returns the setting for the given name, if found.
33
+ #
34
+ # @return [Setting, nil] the setting, or nil if not found
35
+ #
36
+ # @api public
33
37
  def [](name)
34
- elements[name]
38
+ settings[name]
35
39
  end
36
40
 
37
- # @api private
41
+ # Returns true if a setting for the given name is defined.
42
+ #
43
+ # @return [Boolean]
44
+ #
45
+ # @api public
38
46
  def key?(name)
39
47
  keys.include?(name)
40
48
  end
41
49
 
42
- # @api private
50
+ # Returns the list of defined setting names.
51
+ #
52
+ # @return [Array<Symbol>]
53
+ #
54
+ # @api public
43
55
  def keys
44
- elements.keys
56
+ settings.keys
45
57
  end
46
58
 
47
- # @api private
59
+ # @api public
48
60
  def each(&block)
49
- elements.values.each(&block)
50
- end
51
-
52
- # @api private
53
- def pristine
54
- self.class.new(map(&:pristine))
55
- end
56
-
57
- private
58
-
59
- # @api private
60
- def initialize_copy(source)
61
- initialize_elements(source.map(&:dup))
62
- end
63
-
64
- # @api private
65
- def initialize_elements(elements)
66
- @elements = elements.each_with_object(Concurrent::Map.new) { |s, m|
67
- m[s.name] = s
68
- }
61
+ settings.each_value(&block)
69
62
  end
70
63
  end
71
64
  end
@@ -3,6 +3,6 @@
3
3
  module Dry
4
4
  module Configurable
5
5
  # @api public
6
- VERSION = '0.12.1'
6
+ VERSION = "1.0.0"
7
7
  end
8
8
  end
@@ -1,15 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'concurrent/array'
3
+ require "zeitwerk"
4
4
 
5
- require 'dry/configurable/constants'
6
- require 'dry/configurable/class_methods'
7
- require 'dry/configurable/instance_methods'
8
- require 'dry/configurable/config'
9
- require 'dry/configurable/setting'
10
- require 'dry/configurable/errors'
5
+ require "dry/core"
6
+ require "dry/configurable/constants"
7
+ require "dry/configurable/errors"
11
8
 
12
9
  module Dry
10
+ # @api public
11
+ def self.Configurable(**options)
12
+ Configurable::Extension.new(**options)
13
+ end
14
+
13
15
  # A simple configuration mixin
14
16
  #
15
17
  # @example class-level configuration
@@ -44,27 +46,32 @@ module Dry
44
46
  #
45
47
  # @api public
46
48
  module Configurable
49
+ def self.loader
50
+ @loader ||= Zeitwerk::Loader.new.tap do |loader|
51
+ root = File.expand_path("..", __dir__)
52
+ loader.tag = "dry-configurable"
53
+ loader.inflector = Zeitwerk::GemInflector.new("#{root}/dry-configurable.rb")
54
+ loader.push_dir(root)
55
+ loader.ignore(
56
+ "#{root}/dry-configurable.rb",
57
+ "#{root}/dry/configurable/{constants,errors,version}.rb"
58
+ )
59
+ loader.inflector.inflect("dsl" => "DSL")
60
+ end
61
+ end
62
+
47
63
  # @api private
48
64
  def self.extended(klass)
49
65
  super
50
- klass.extend(ClassMethods)
66
+ klass.extend(Extension.new)
51
67
  end
52
68
 
53
69
  # @api private
54
70
  def self.included(klass)
55
- raise AlreadyIncluded if klass.include?(InstanceMethods)
56
-
57
71
  super
58
- klass.class_eval do
59
- extend(ClassMethods)
60
- include(InstanceMethods)
61
- prepend(Initializer)
62
-
63
- class << self
64
- undef :config
65
- undef :configure
66
- end
67
- end
72
+ klass.include(Extension.new)
68
73
  end
74
+
75
+ loader.setup
69
76
  end
70
77
  end
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/configurable'
3
+ require "dry/configurable"
metadata CHANGED
@@ -1,22 +1,25 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-configurable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Holland
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-02-15 00:00:00.000000000 Z
11
+ date: 2022-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: concurrent-ruby
14
+ name: dry-core
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '2'
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -24,26 +27,23 @@ dependencies:
24
27
  - - "~>"
25
28
  - !ruby/object:Gem::Version
26
29
  version: '1.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '2'
27
33
  - !ruby/object:Gem::Dependency
28
- name: dry-core
34
+ name: zeitwerk
29
35
  requirement: !ruby/object:Gem::Requirement
30
36
  requirements:
31
37
  - - "~>"
32
38
  - !ruby/object:Gem::Version
33
- version: '0.5'
34
- - - ">="
35
- - !ruby/object:Gem::Version
36
- version: 0.5.0
39
+ version: '2.6'
37
40
  type: :runtime
38
41
  prerelease: false
39
42
  version_requirements: !ruby/object:Gem::Requirement
40
43
  requirements:
41
44
  - - "~>"
42
45
  - !ruby/object:Gem::Version
43
- version: '0.5'
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: 0.5.0
46
+ version: '2.6'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: bundler
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -104,8 +104,8 @@ files:
104
104
  - lib/dry/configurable/config.rb
105
105
  - lib/dry/configurable/constants.rb
106
106
  - lib/dry/configurable/dsl.rb
107
- - lib/dry/configurable/dsl/args.rb
108
107
  - lib/dry/configurable/errors.rb
108
+ - lib/dry/configurable/extension.rb
109
109
  - lib/dry/configurable/instance_methods.rb
110
110
  - lib/dry/configurable/methods.rb
111
111
  - lib/dry/configurable/setting.rb
@@ -117,7 +117,7 @@ licenses:
117
117
  - MIT
118
118
  metadata:
119
119
  allowed_push_host: https://rubygems.org
120
- changelog_uri: https://github.com/dry-rb/dry-configurable/blob/master/CHANGELOG.md
120
+ changelog_uri: https://github.com/dry-rb/dry-configurable/blob/main/CHANGELOG.md
121
121
  source_code_uri: https://github.com/dry-rb/dry-configurable
122
122
  bug_tracker_uri: https://github.com/dry-rb/dry-configurable/issues
123
123
  post_install_message:
@@ -128,14 +128,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
128
128
  requirements:
129
129
  - - ">="
130
130
  - !ruby/object:Gem::Version
131
- version: 2.5.0
131
+ version: 2.7.0
132
132
  required_rubygems_version: !ruby/object:Gem::Requirement
133
133
  requirements:
134
134
  - - ">="
135
135
  - !ruby/object:Gem::Version
136
136
  version: '0'
137
137
  requirements: []
138
- rubygems_version: 3.1.4
138
+ rubygems_version: 3.1.6
139
139
  signing_key:
140
140
  specification_version: 4
141
141
  summary: A mixin to add configuration functionality to your classes