configsl 1.0.2 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 996520d6a141a33f05a05cca3318e284646e1513b25ee4c0a4f40cfacc794922
4
- data.tar.gz: 4c3df5cff5db3806c8e8423c0d6813bd2e180ccf05827b65a80e47f0aa99b29b
3
+ metadata.gz: bcb5d7709de6f2d5a3b019662fc01771cfb754ac1a4f234a7e760e20c40b159f
4
+ data.tar.gz: f94d74b4fd5976c935eb6fac512746ff529f55a7a2611e6c1760d70ba3695951
5
5
  SHA512:
6
- metadata.gz: 0d2d5b158111e87a0de35946016c8bd29ed2fdd9b47f85ec72e4224ade52ae292ddbe84bbad7ac48cf0221a57f6fed14d05ff17e54a56a04dfed471078cfbff1
7
- data.tar.gz: ae533574e3eb01cb827e71eb7319e7028fdd723eb546f85fd28d57bc27d21c76a007724ea075a6366420062fafe03acdc09059ba763168ce19ba4350216760c7
6
+ metadata.gz: 658ed24bbaf0efb17956713c48df37cd02b46b954386547a8258b4f20088e1306c9056918ac036e4e1c951f62c8b40dc85e77e22ae6b394411399ea7f5c86a72
7
+ data.tar.gz: 0411a560f507de254e92ca98268913ec9ffead15e31d715b4e4ad15ff62c9622f84d7f1fd346454f26e4d31b598439e7fb8632c8bddac685b3c1076be5b1ddaf
data/CHANGELOG.md CHANGED
@@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog][changelog], and this project adheres
6
6
  to [Semantic Versioning][versioning].
7
7
 
8
+ ## [1.1.1]
9
+
10
+ ### Fixed
11
+
12
+ - Reported gem version
13
+
14
+ ## [1.1.0]
15
+
16
+ ### Added
17
+
18
+ - Support collections of configuration objects with the new `Collection` module
19
+ - Add `ToHash` module for converting configuration objects to hashes using
20
+ `to_h`
21
+ - Create setter methods for options
22
+ - Added `Merge` module to merge parameters from multiple sources
23
+
24
+ ### Fixed
25
+
26
+ - Renamed `@params` to `@configsl_params` to avoid collisions
27
+ - Add `configsl_` prefix to more methods to avoid conflicts
28
+
29
+ ### Deprecated
30
+
31
+ - `FileSupport.find_file` is deprecated; use `configsl_find_file` instead
32
+ - `FileSupport.find_file_format` is deprecated; use `configsl_find_file_format`
33
+ instead
34
+
8
35
  ## [1.0.2]
9
36
 
10
37
  ### Added
data/Gemfile CHANGED
@@ -6,20 +6,20 @@ gemspec
6
6
 
7
7
  group :development do
8
8
  gem 'rake', '~> 13.2'
9
- gem 'rubocop', '~> 1.65'
10
- gem 'rubocop-factory_bot', '~> 2.26'
11
- gem 'rubocop-rake', '~> 0.6'
12
- gem 'rubocop-rspec', '~> 3.0'
9
+ gem 'rubocop', '~> 1.75'
10
+ gem 'rubocop-factory_bot', '~> 2.27'
11
+ gem 'rubocop-md', '~> 2.0'
12
+ gem 'rubocop-performance', '~> 1.25'
13
+ gem 'rubocop-rake', '~> 0.7'
14
+ gem 'rubocop-rspec', '~> 3.5'
13
15
  end
14
16
 
15
17
  group :test do
16
- # activesupport 7.2 introduces a breaking change that causes the specs to
17
- # fail.
18
- gem 'activesupport', '~> 7.1.0'
18
+ gem 'activesupport', '~> 8.1'
19
19
 
20
20
  gem 'coveralls_reborn', '~> 0.28'
21
- gem 'factory_bot', '~> 6.4'
21
+ gem 'factory_bot', '~> 6.5'
22
22
  gem 'rspec', '~> 3.13'
23
- gem 'rspec-github', '~> 2.4'
23
+ gem 'rspec-github', '~> 3.0'
24
24
  gem 'simplecov', '~> 0.22'
25
25
  end
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # ConfigSL [![Gem Version](https://badge.fury.io/rb/configsl.svg)](https://badge.fury.io/rb/configsl) [![Coverage Status][badge-coverage]][coverage] [![Code Checks](https://github.com/jamesiarmes/configsl/actions/workflows/checks.yaml/badge.svg?branch=main)](https://github.com/jamesiarmes/configsl/actions/workflows/checks.yaml)
1
+ # ConfigSL [![Gem Version][badge-version]][rubygems] [![Coverage Status][badge-coverage]][coverage] [![Code Checks][badge-checks]][checks]
2
2
 
3
3
  ConfigSL is a simple Domain-Specific Language (DSL) module for configuration.
4
4
  It is designed to provide a declarative way to define configuration, with as few
@@ -10,7 +10,7 @@ extensible, so you can use as little or as much as you need.
10
10
  Add this line to your application's Gemfile:
11
11
 
12
12
  ```ruby
13
- gem 'configsl'
13
+ gem 'configsl', '~> 1.1'
14
14
  ```
15
15
 
16
16
  And then execute:
@@ -37,26 +37,47 @@ You can start defining your configurations using two methods:
37
37
  The `ConfigSL::Config` base class includes common functionality for working with
38
38
  configurations. Currently, the class provides the following features:
39
39
 
40
+ - **Collections**: Supports arrays and hashes of other configurations
40
41
  - **DSL**: The primary DSL for defining configuration options
41
42
  - **Format**: A simple way to enforce option value formatting
42
43
  - **FromEnvironment**: Load configuration from environment variables
43
44
  - **FromFile**: Load configuration from a file
45
+ - **Merge**: Merge configuration from multiple sources
46
+ - **ToHash**: Recursively convert the configuration to a hash
44
47
  - **Validation**: Built-in validation for configuration options
45
48
 
46
49
  ```ruby
47
50
  require 'configsl'
48
51
 
49
52
  class AppConfig < ConfigSL::Config
50
- register_file_format :json
51
- register_file_format :yaml
53
+ register_file_format :json
54
+ register_file_format :yaml
52
55
 
53
- option :name, type: String, default: 'My App'
54
- option :environment, type: Symbol, enum: %i[dev test prod], default: :dev,
55
- env_variable: 'RACK_ENV'
56
- option :database, type: DatabaseConfig, required: true
56
+ option :name, type: String, default: 'My App'
57
+ option :environment, type: Symbol, enum: %i[dev test prod], default: :dev,
58
+ env_variable: 'RACK_ENV'
59
+ option :database, type: DatabaseConfig, required: true
60
+
61
+ # Collect arrays or hashes into configuration objects. Automatically set a key
62
+ # on the collected configurations based on their index (arrays) or key
63
+ # (hashes).
64
+ option :hosts, type: Hash, collection: { type: HostConfig, key: :hostname }
65
+
66
+ # Use shorthand syntax if you don't need to set a key.
67
+ option :plugins, type: Array, collection: PluginConfig
57
68
  end
58
69
  ```
59
70
 
71
+ You can load your configuration from different sources using the following
72
+ methods:
73
+
74
+ ```ruby
75
+ AppConfig.new(params) # Load from parameters
76
+ AppConfig.from_file # Load from a file
77
+ AppConfig.from_environment # Load from environment variables
78
+ AppConfig.load # Merge from multiple sources
79
+ ```
80
+
60
81
  ### Including modules
61
82
 
62
83
  If you'd like to pick and choose the features you want to use, you can include
@@ -70,19 +91,19 @@ that sets the configuration values by calling `set_value` for each option.
70
91
  require 'configsl'
71
92
 
72
93
  class ApplicationConfig
73
- include ConfigSL::DSL
74
- include ConfigSL::Format
75
- include ConfigSL::FromEnvironment
76
-
77
- option :name, type: String, default: 'My App'
78
- option :environment, type: Symbol, env_variable: 'RACK_ENV'
79
- option :database, type: DatabaseConfig
80
-
81
- def initialize(params = {})
82
- params.each do |name, value|
83
- set_value(name, value)
84
- end
94
+ include ConfigSL::DSL
95
+ include ConfigSL::Format
96
+ include ConfigSL::FromEnvironment
97
+
98
+ option :name, type: String, default: 'My App'
99
+ option :environment, type: Symbol, env_variable: 'RACK_ENV'
100
+ option :database, type: DatabaseConfig
101
+
102
+ def initialize(params = {})
103
+ params.each do |name, value|
104
+ set_value(name, value)
85
105
  end
106
+ end
86
107
  end
87
108
  ```
88
109
 
@@ -101,5 +122,9 @@ It's important to note that this is not limited to your defined configuration
101
122
  options, but also methods such as `register_file_format` and
102
123
  `config_file_path`.
103
124
 
125
+ [badge-checks]: https://github.com/jamesiarmes/configsl/actions/workflows/checks.yaml/badge.svg?branch=main
104
126
  [badge-coverage]: https://coveralls.io/repos/github/jamesiarmes/configsl/badge.svg
127
+ [badge-version]: https://badge.fury.io/rb/configsl.svg
128
+ [checks]: https://github.com/jamesiarmes/configsl/actions/workflows/checks.yaml
105
129
  [coverage]: https://coveralls.io/github/jamesiarmes/configsl
130
+ [rubygems]: https://rubygems.org/gems/configsl
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'format'
4
+
5
+ module ConfigSL
6
+ # Support options as collections.
7
+ #
8
+ # This will format the values of the defined options when they are set on the
9
+ # config object. It will also format the values when they are retrieved, if
10
+ # they don't match their defined type.
11
+ #
12
+ # The default behavior is to cast the value as the defined type. If the value
13
+ # is nil, it will be returned as is.
14
+ #
15
+ # @example Use the shorthand syntax to define a collection with no additional
16
+ # options
17
+ #
18
+ # class TestConfig < ConfigSL::Config
19
+ # include ConfigSL::Collection
20
+ #
21
+ # option :subarray, type: Array, collection: SubConfig
22
+ # option :subhash, type: Hash, collection: SubConfig
23
+ # end
24
+ #
25
+ # @example Automatically set a key on the collection members based on their
26
+ # index (arrays) or keys (hashes), unless the key is already set
27
+ #
28
+ # class TestConfig < ConfigSL::Config
29
+ # include ConfigSL::Collection
30
+ #
31
+ # option :subarray, type: Array, collection: { type: SubConfig, key: :index }
32
+ # option :subhash, type: Hash, collection: { type: SubConfig, key: :name }
33
+ # end
34
+ module Collection
35
+ def self.included(base)
36
+ base.include(Format) unless base.include?(Format)
37
+ base.extend ClassMethods
38
+ end
39
+
40
+ # Class methods necessary for defining collection options.
41
+ module ClassMethods
42
+ # @see DSL#option
43
+ def option(name, opts = {})
44
+ # If the option was set using the shorthand syntax, convert it to an
45
+ # options hash for consistent handling.
46
+ opts[:collection] = { type: opts[:collection], key: nil } \
47
+ if opts[:collection].is_a?(Class)
48
+
49
+ super
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ # @see DSL#set_value
56
+ #
57
+ # @raise [InvalidValueError] If the value is not of the expected type.
58
+ def set_value(name, value)
59
+ configsl_option_exists!(name)
60
+ if configsl_collection?(name) &&
61
+ !value.nil? &&
62
+ !value.is_a?(options[name][:type])
63
+ # If the option is a collection, we want to make sure the value is of the
64
+ # expected type.
65
+ raise InvalidValueError,
66
+ "Invalid value type (#{value.class}) for collection " \
67
+ "option #{name}; expected #{options[name][:type]}"
68
+ end
69
+
70
+ super
71
+ end
72
+
73
+ # Determines if an option is a collection.
74
+ #
75
+ # @param option [Symbol] Option to check.
76
+ # @return [Boolean]
77
+ def configsl_collection?(option)
78
+ options[option].fetch(:collection, false) &&
79
+ [Array, Hash].include?(options[option][:type])
80
+ end
81
+
82
+ # Determines if the values in a collection have been collected.
83
+ #
84
+ # @param option [Symbol] Option being checked.
85
+ # @param values [Array, Hash] Values for the option.
86
+ # @return [Boolean]
87
+ def configsl_collected?(option, values)
88
+ return true if values.empty?
89
+
90
+ value = options[option][:type] == Array ? values.first : values.values.first
91
+ value.is_a?(options[option][:collection][:type])
92
+ end
93
+
94
+ # Collects values into an appropriate collection.
95
+ #
96
+ # @param option [Symbol] Option whose values are being collected.
97
+ # @param values [Array, Hash] Values to collect.
98
+ # @return [Array, Hash] Collected values as the defined type.
99
+ #
100
+ # @raise [InvalidValueError] If the values are not of the correct type.
101
+ def configsl_collect_values(option, values)
102
+ return values if configsl_collected?(option, values)
103
+
104
+ if values.is_a?(Hash)
105
+ configsl_collect_hash(option, values)
106
+ else
107
+ configsl_collect_array(option, values)
108
+ end
109
+ end
110
+
111
+ # Collect values for an array collection.
112
+ #
113
+ # @param option [Symbol] Option being processed.
114
+ # @param values [Array] Values to collect.
115
+ # @return [Array] Collected values.
116
+ def configsl_collect_array(option, values)
117
+ values.map.with_index do |value, index|
118
+ configsl_collect_value(option, value, index)
119
+ end
120
+ end
121
+
122
+ # Collect values for a hash collection.
123
+ #
124
+ # @param option [Symbol] Option being processed.
125
+ # @param values [Hash] Values to collect.
126
+ # @return [Hash] Collected values.
127
+ def configsl_collect_hash(option, values)
128
+ values.to_h do |key, value|
129
+ configsl_collect_value(option, [key, value], key)
130
+ end
131
+ end
132
+
133
+ # Collect a single value to be added to the collection.
134
+ #
135
+ # @param option [Symbol] Option whose value is being collected.
136
+ # @param value [Object] The value to be added to the collection.
137
+ # @return [Array, Hash]
138
+ def configsl_collect_value(option, value, index = nil)
139
+ is_array = options[option][:type] == Array
140
+ params = is_array ? value : value[1]
141
+ configsl_collection_key(params, options[option][:collection][:key], index)
142
+
143
+ config = options[option][:collection][:type].new(params)
144
+ is_array ? config : [value[0], config]
145
+ end
146
+
147
+ def configsl_collection_key(value, key, index)
148
+ return if key.nil? || index.nil?
149
+ return value[key] unless value[key].nil?
150
+
151
+ value[key] = index
152
+ end
153
+
154
+ # @see Format#format_value
155
+ def format_value(option, value)
156
+ return super unless configsl_collection?(option)
157
+
158
+ return configsl_collect_values(option, value) unless value.nil? || value.empty?
159
+
160
+ formatter = Format::FORMATTERS[options[option][:type]]
161
+ value.send(formatter)
162
+ end
163
+ end
164
+ end
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'collection'
3
4
  require_relative 'dsl'
4
5
  require_relative 'file_format/json'
5
6
  require_relative 'file_format/yaml'
6
7
  require_relative 'format'
7
8
  require_relative 'from_environment'
8
9
  require_relative 'from_file'
10
+ require_relative 'merge'
11
+ require_relative 'to_hash'
9
12
  require_relative 'validation'
10
13
 
11
14
  module ConfigSL
@@ -15,7 +18,10 @@ module ConfigSL
15
18
  include Format
16
19
  include FromEnvironment
17
20
  include FromFile
21
+ include ToHash
18
22
  include Validation
23
+ include Collection
24
+ include Merge
19
25
 
20
26
  def initialize(params = {})
21
27
  params.each do |name, value|
data/lib/configsl/dsl.rb CHANGED
@@ -46,13 +46,13 @@ module ConfigSL
46
46
  # If the option is not set, it will return the default value.
47
47
  #
48
48
  # @param name [Symbol] The name of the option.
49
- # @return [Object] The value of the option.
49
+ # @return [Object, nil] The value of the option.
50
50
  #
51
51
  # @raise [InvalidOptionError] If the option is not defined.
52
52
  def get_value(name)
53
- raise InvalidOptionError, "Option #{name} is not defined" unless options.key?(name)
53
+ configsl_option_exists!(name)
54
54
 
55
- @params.fetch(name, options[name]&.[](:default))
55
+ configsl_params.fetch(name, options[name]&.[](:default))
56
56
  end
57
57
 
58
58
  # Sets the value of an option.
@@ -63,10 +63,27 @@ module ConfigSL
63
63
  #
64
64
  # @raise [InvalidOptionError] If the option is not defined.
65
65
  def set_value(name, value)
66
- raise InvalidOptionError, "Option #{name} is not defined" unless options.key?(name)
66
+ configsl_option_exists!(name)
67
67
 
68
- @params ||= {}
69
- @params[name] = value
68
+ configsl_params[name] = value
69
+ end
70
+
71
+ # Validates that an option exists for the current configuration class.
72
+ #
73
+ # @param name [Symbol] Name of the option to validate.
74
+ #
75
+ # @raise [InvalidOptionError] If the option is not defined.
76
+ def configsl_option_exists!(name)
77
+ return if options.key?(name)
78
+
79
+ raise InvalidOptionError, "Option #{name} is not defined"
80
+ end
81
+
82
+ # Returns the params hash for the class of the current instance.
83
+ #
84
+ # @return [Hash] The params hash.
85
+ def configsl_params
86
+ @configsl_params ||= {}
70
87
  end
71
88
 
72
89
  # Required class methods for the config DSL.
@@ -80,8 +97,10 @@ module ConfigSL
80
97
  # @param opts [Hash] The options for the option.
81
98
  # @return [void]
82
99
  def option(name, opts = {})
83
- options.merge!({ name => opts })
100
+ options[name] = opts
101
+
84
102
  define_method(name) { get_value(name) }
103
+ define_method("#{name}=") { |val| set_value(name, val) }
85
104
  end
86
105
 
87
106
  # Returns the options hash for the class.
@@ -2,4 +2,5 @@
2
2
 
3
3
  module ConfigSL
4
4
  class InvalidOptionError < ArgumentError; end
5
+ class InvalidValueError < ArgumentError; end
5
6
  end
@@ -72,8 +72,6 @@ module ConfigSL
72
72
  @config_file_formats[format] = opts
73
73
  end
74
74
 
75
- private
76
-
77
75
  # Find the configuration file based on the default path, name, and defined
78
76
  # formats.
79
77
  #
@@ -91,7 +89,7 @@ module ConfigSL
91
89
  # provided, will only match files of the given format. Ignored if `path`
92
90
  # is provided.
93
91
  # @return [Array<String>] Array of matching files.
94
- def find_file(path: nil, format: nil)
92
+ def configsl_find_file(path: nil, format: nil)
95
93
  paths = Dir.glob(
96
94
  path.nil? ? "#{config_file_name}.{#{file_extensions(format:).join(',')}}" : path,
97
95
  base: path.nil? ? config_file_path : nil
@@ -99,7 +97,7 @@ module ConfigSL
99
97
 
100
98
  raise FileNotFoundError, 'No configuration file found!' if paths.empty?
101
99
 
102
- paths.map { |p| File.join(config_file_path, p) }
100
+ path.nil? ? paths.map { |p| File.join(config_file_path, p) } : paths
103
101
  end
104
102
 
105
103
  # Find the format for a file given its extension.
@@ -109,7 +107,7 @@ module ConfigSL
109
107
  #
110
108
  # @raise [FileFormatError] If no file formats have been defined.
111
109
  # @raise [FileFormatError] If no file format is found for the extension.
112
- def find_file_format(extension)
110
+ def configsl_find_file_format(extension)
113
111
  raise FileFormatError, 'No file formats have been defined' if @config_file_formats.nil?
114
112
 
115
113
  extension = extension.sub(/^\./, '').to_sym
@@ -119,6 +117,28 @@ module ConfigSL
119
117
 
120
118
  raise FileFormatError, "No file format found for extension: #{extension}"
121
119
  end
120
+
121
+ private
122
+
123
+ # Deprecated: Use configsl_find_file instead.
124
+ def find_file(path: nil, format: nil)
125
+ warn '[ConfigSL] DEPRECATION WARNING: ' \
126
+ '`find_file` is deprecated. ' \
127
+ 'Use `configsl_find_file` instead. ' \
128
+ "Called from: #{caller(1..1).first}",
129
+ category: :deprecated
130
+ configsl_find_file(path:, format:)
131
+ end
132
+
133
+ # Deprecated: Use configsl_find_file_format instead.
134
+ def find_file_format(extension)
135
+ warn '[ConfigSL] DEPRECATION WARNING: ' \
136
+ '`find_file_format` is deprecated. ' \
137
+ 'Use `configsl_find_file_format` instead. ' \
138
+ "Called from: #{caller(1..1).first}",
139
+ category: :deprecated
140
+ configsl_find_file_format(extension)
141
+ end
122
142
  end
123
143
  end
124
144
  end
@@ -39,6 +39,10 @@ module ConfigSL
39
39
  super(name, format_value(name, value))
40
40
  end
41
41
 
42
+ # Formats a value based on its option definition.
43
+ #
44
+ # @param option [Symbol] Option whose value is being formatted.
45
+ # @param value [Object] Value for the option.
42
46
  def format_value(option, value)
43
47
  return value if value.nil? || value.is_a?(options[option][:type])
44
48
 
@@ -21,6 +21,28 @@ module ConfigSL
21
21
  base.from_environment_prefix ''
22
22
  end
23
23
 
24
+ def self.configsl_source_name
25
+ :environment
26
+ end
27
+
28
+ # Loads the configuration parameters from environment variables.
29
+ #
30
+ # @param klass [Class] The class loading the configuration.
31
+ # @param opts [Hash] Options for loading the configuration.
32
+ # @option opts [Boolean] :defaults Whether to populate default values,
33
+ # defaults to `true`
34
+ # @return [Hash] Loaded configuration parameters.
35
+ def self.configsl_load_params(klass, opts = {})
36
+ klass.options.each_with_object({}) do |(name, option_opts), hash|
37
+ env_var = option_opts[:env_variable] || name.to_s.upcase
38
+ if ENV.key?(env_var)
39
+ hash[name] = ENV[env_var]
40
+ elsif opts[:defaults]
41
+ hash[name] = option_opts[:default]
42
+ end
43
+ end
44
+ end
45
+
24
46
  # Class methods necessary for loading configuration from the environment.
25
47
  module ClassMethods
26
48
  # Set the prefix for environment variables.
@@ -34,10 +56,7 @@ module ConfigSL
34
56
  #
35
57
  # @return [self] The new config object
36
58
  def from_environment
37
- params = options.transform_values do |opts|
38
- ENV.fetch(opts[:env_variable], opts[:default])
39
- end
40
-
59
+ params = FromEnvironment.configsl_load_params(self, defaults: true)
41
60
  new(params)
42
61
  end
43
62
 
@@ -32,6 +32,46 @@ module ConfigSL
32
32
  base.extend(ClassMethods)
33
33
  end
34
34
 
35
+ def self.configsl_source_name
36
+ :file
37
+ end
38
+
39
+ # Loads the configuration parameters from a file.
40
+ #
41
+ # @param klass [Class] The class loading the configuration.
42
+ # @param opts [Hash] Options for loading the configuration.
43
+ # @option opts [String] :path Optional path to the file to load; uses the
44
+ # default path and filename if not specified.
45
+ # @option opts [Symbol] :format Optional format to use for the file; uses
46
+ # the file extension if not specified.
47
+ # @option opts [Boolean] :defaults Whether to populate default values,
48
+ # defaults to `true`.
49
+ # @return [Hash] Loaded configuration parameters.
50
+ def self.configsl_load_params(klass, opts = {})
51
+ formats = klass.instance_variable_get(:@config_file_formats) || {}
52
+ return {} if formats.empty?
53
+
54
+ path = opts[:path] || klass.configsl_find_file.first
55
+ format = opts[:format] || klass.configsl_find_file_format(File.extname(path))
56
+ data = formats[format][:class].new(path).read
57
+
58
+ populate_defaults(klass, data) if opts[:defaults]
59
+ data
60
+ end
61
+
62
+ # Populates default values for unset options.
63
+ #
64
+ # @param klass [Class] The class loading the configuration.
65
+ # @param data [Hash] Configuration parameters.
66
+ def self.populate_defaults(klass, data)
67
+ klass.options.each do |name, option_opts|
68
+ name_sym = name.to_sym
69
+ next if data.key?(name_sym) || data.key?(name.to_s)
70
+
71
+ data[name_sym] = option_opts[:default]
72
+ end
73
+ end
74
+
35
75
  # Required class methods for loading config files.
36
76
  module ClassMethods
37
77
  # Loads configuration from a file.
@@ -46,10 +86,8 @@ module ConfigSL
46
86
  # file extension if not specified.
47
87
  # @return [self]
48
88
  def from_file(path = nil, format: nil)
49
- path ||= find_file.first
50
- format ||= find_file_format(File.extname(path))
51
- file = @config_file_formats[format][:class].new(path)
52
- new(file.read)
89
+ params = FromFile.configsl_load_params(self, path:, format:, defaults: true)
90
+ new(params)
53
91
  end
54
92
  end
55
93
  end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConfigSL
4
+ # Support merging configurations from multiple sources based on precedence.
5
+ module Merge
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+ end
9
+
10
+ # Class methods to manage precedence and loading of sources.
11
+ module ClassMethods
12
+ # Load config from all available sources and merge them in order.
13
+ #
14
+ # @param params [Hash] Configuration parameters to merge in at the highest
15
+ # precedence.
16
+ # @param source_options [Hash] Optional per-source configuration options.
17
+ # @return [self] Configuration object with merged parameters.
18
+ def load(params = {}, source_options = {})
19
+ merged_params = configsl_load_merged_params(params, source_options)
20
+ new(merged_params)
21
+ end
22
+
23
+ # Set or get precedence order
24
+ #
25
+ # @param sources [Array<Symbol>] List of sources in order of priority,
26
+ # highest to lowest precedence.
27
+ # @return [Array<Symbol>] Full list of sources in precedence order.
28
+ def precedence(*sources)
29
+ sources = sources.flatten
30
+ if sources.any?
31
+ configsl_validate_precedence_sources(sources)
32
+ @precedence = sources
33
+ else
34
+ @precedence || configsl_default_precedence
35
+ end
36
+ end
37
+
38
+ # Return the available sources dynamically discovered from ancestors
39
+ #
40
+ # @return [Array<Symbol>]
41
+ def configsl_available_sources
42
+ [:params] + ancestors.select { |ancestor| ancestor.respond_to?(:configsl_source_name) }
43
+ .map(&:configsl_source_name)
44
+ end
45
+
46
+ private
47
+
48
+ # Returns the default precedence order.
49
+ #
50
+ # +:params+ has the highest precedence, followed by included sources in
51
+ # the order they were included.
52
+ #
53
+ # @return [Array<Symbol>] Default source precedence order.
54
+ def configsl_default_precedence
55
+ # Ancestors are listed in reverse order of inclusion, so we filter
56
+ # modules that are config sources and reverse the array to order them by
57
+ # inclusion.
58
+ modules = ancestors.select { |ancestor| ancestor.respond_to?(:configsl_source_name) }
59
+ [:params] + modules.reverse.map(&:configsl_source_name)
60
+ end
61
+
62
+ # Loads configs from all available sources and merges them in order.
63
+ #
64
+ # @param params [Hash] Configuration parameters to merge in at the highest
65
+ # precedence.
66
+ # @param opts [Hash] Optional per-source configuration options.
67
+ # @return [Hash] Merged parameters.
68
+ def configsl_load_merged_params(params = {}, opts = {})
69
+ params = params.is_a?(Hash) ? params.transform_keys(&:to_sym) : {}
70
+ sources = { params: params }
71
+
72
+ configsl_load_sources(sources, opts)
73
+ configsl_merge_sources_by_precedence(sources)
74
+ end
75
+
76
+ # Loads configuration data from all registered config sources.
77
+ #
78
+ # If an explicit precedence has been defined, only sources configured in
79
+ # the chain will be loaded.
80
+ #
81
+ # @param sources [Hash{Symbol => Hash}] Hash for loaded sources.
82
+ # @param opts [Hash] Optional per-source configuration options.
83
+ def configsl_load_sources(sources, opts = {})
84
+ ancestors.each do |ancestor|
85
+ next unless ancestor.respond_to?(:configsl_source_name)
86
+
87
+ source_name = ancestor.configsl_source_name.to_sym
88
+ next unless precedence.include?(source_name)
89
+
90
+ source_opts = opts[source_name] || {}
91
+ sources[source_name] ||=
92
+ ancestor.configsl_load_params(self, source_opts)
93
+ end
94
+ end
95
+
96
+ # Merges all loaded configuration sources in precedence order.
97
+ #
98
+ # @param sources [Hash{Symbol => Hash}] Loaded configuration hashes.
99
+ # @return [Hash] Merged configuration parameters.
100
+ def configsl_merge_sources_by_precedence(sources)
101
+ merged = {}
102
+ precedence.reverse_each do |source|
103
+ merged.merge!(sources[source] || {})
104
+ end
105
+
106
+ merged
107
+ end
108
+
109
+ # Validates that all specified precedence sources are available.
110
+ #
111
+ # @param sources [Array<Symbol>] The sources to validate.
112
+ #
113
+ # @raise [ArgumentError] If any source is invalid.
114
+ def configsl_validate_precedence_sources(sources)
115
+ invalid = sources - configsl_available_sources
116
+ return if invalid.empty?
117
+
118
+ msg = "Invalid precedence sources: #{invalid.join(', ')}. " \
119
+ "Available: #{configsl_available_sources.join(', ')}"
120
+ raise ArgumentError, msg
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConfigSL
4
+ # Converts a configuration value to a hash structure.
5
+ module ToHash
6
+ def to_h
7
+ configsl_params.to_h { |k, v| [k, configsl_serialize_value(v)] }
8
+ end
9
+
10
+ private
11
+
12
+ # Recursively serializes configuration values.
13
+ #
14
+ # @param val [Object] Value to serialize.
15
+ # @return [Object] Serialized value.
16
+ def configsl_serialize_value(val)
17
+ case val
18
+ when nil then nil
19
+ when Array then configsl_serialize_array(val)
20
+ when Hash then configsl_serialize_hash(val)
21
+ else val.respond_to?(:to_h) ? val.to_h : val
22
+ end
23
+ end
24
+
25
+ # Serializes array values.
26
+ #
27
+ # @param array [Array] Array to serialize.
28
+ # @return [Array] Serialized array.
29
+ def configsl_serialize_array(array)
30
+ array.map { |item| configsl_serialize_value(item) }
31
+ end
32
+
33
+ # Serializes hash values.
34
+ #
35
+ # @param hash [Hash] Hash to serialize.
36
+ # @return [Hash] Serialized hash.
37
+ def configsl_serialize_hash(hash)
38
+ hash.transform_values { |item| configsl_serialize_value(item) }
39
+ end
40
+ end
41
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  # :nocov:
4
4
  module ConfigSL
5
- VERSION = '1.0.2'
5
+ VERSION = '1.1.1'
6
6
  end
7
7
  # :nocov:
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: configsl
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - James I. Armes
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-09-15 00:00:00.000000000 Z
11
+ date: 2026-07-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: facets
@@ -36,6 +36,7 @@ files:
36
36
  - Gemfile
37
37
  - README.md
38
38
  - lib/configsl.rb
39
+ - lib/configsl/collection.rb
39
40
  - lib/configsl/config.rb
40
41
  - lib/configsl/dsl.rb
41
42
  - lib/configsl/exception.rb
@@ -47,6 +48,8 @@ files:
47
48
  - lib/configsl/format.rb
48
49
  - lib/configsl/from_environment.rb
49
50
  - lib/configsl/from_file.rb
51
+ - lib/configsl/merge.rb
52
+ - lib/configsl/to_hash.rb
50
53
  - lib/configsl/validation.rb
51
54
  - lib/configsl/version.rb
52
55
  homepage: https://github.com/jamesiarmes/configsl
@@ -58,7 +61,7 @@ metadata:
58
61
  homepage_uri: https://github.com/jamesiarmes/configsl
59
62
  rubygems_mfa_required: 'true'
60
63
  source_code_uri: https://github.com/jamesiarmes/configsl
61
- post_install_message:
64
+ post_install_message:
62
65
  rdoc_options: []
63
66
  require_paths:
64
67
  - lib
@@ -66,7 +69,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
66
69
  requirements:
67
70
  - - ">="
68
71
  - !ruby/object:Gem::Version
69
- version: '3.2'
72
+ version: '3.3'
70
73
  required_rubygems_version: !ruby/object:Gem::Requirement
71
74
  requirements:
72
75
  - - ">="
@@ -74,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
77
  version: '0'
75
78
  requirements: []
76
79
  rubygems_version: 3.5.9
77
- signing_key:
80
+ signing_key:
78
81
  specification_version: 4
79
82
  summary: A simple DSL for declarative configuration in ruby.
80
83
  test_files: []