const_conf 0.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.
@@ -0,0 +1,382 @@
1
+ # A configuration setting definition that encapsulates the properties and
2
+ # behavior of a single environment variable-based setting.
3
+ #
4
+ # The Setting class provides a structured way to define, validate, and
5
+ # retrieve configuration values from environment variables, including support
6
+ # for default values, required validation, decoding logic, and descriptive
7
+ # metadata.
8
+ #
9
+ # @example Defining a configuration setting
10
+ # Setting.new(name: 'DATABASE_URL', prefix: 'APP') { |s|
11
+ # description = 'The database connection string'
12
+ # required true
13
+ # }
14
+ class ConstConf::Setting
15
+ extend ConstConf::SettingAccessor
16
+ include DSLKit::BlockSelf
17
+
18
+ # Initializes a new Setting instance with the given name and prefix.
19
+ #
20
+ # @param name [Array<String>, String] the name components for the setting
21
+ # @param prefix [String, nil] the optional upcassed prefix to use when
22
+ # constructing environment variable names
23
+ # @yield [] optional block to configure the setting
24
+ def initialize(name:, prefix: nil, &block)
25
+ @parent_namespace = block_self(&block)
26
+ @name = Array(name) * '::'
27
+ self.prefix(prefix)
28
+
29
+ block and self.class.enable_setter_mode { instance_eval(&block) }
30
+ end
31
+
32
+ # The name reader accessor returns the name of the setting.
33
+ #
34
+ # @return [String] the constructed environment variable name
35
+ attr_reader :name
36
+
37
+ # The parent_namespace reader accessor returns the parent namespace of the
38
+ # setting.
39
+ #
40
+ # @return [Module, nil] the parent namespace module, or nil if not set
41
+ attr_reader :parent_namespace
42
+
43
+ # The prefix reader accessor returns the configured prefix for the setting.
44
+ #
45
+ # @return [String, nil] the prefix value, or nil if not set
46
+ setting_accessor :prefix,
47
+ transform: -> value { value.ask_and_send_or_self(:upcase) }
48
+
49
+ # The activated reader and writer accessor for configuration settings.
50
+ #
51
+ # This method provides access to the activated state of a configuration
52
+ # setting, which determines whether the setting should be considered active
53
+ # based on its current value or other conditions. It defaults to `:present?`
54
+ # of not set.
55
+ #
56
+ # @return [Object] the updated activated state value
57
+ # @see #active?
58
+ setting_accessor :activated, :present?
59
+
60
+ # Checks if the configuration setting is active based on its activated state.
61
+ #
62
+ # This method evaluates whether the configuration setting is considered
63
+ # active by examining its activated property. If activated is set to true, it
64
+ # checks if the setting's value is present. If activated is a Symbol, it
65
+ # sends that symbol as a message to the value and returns the result. If
66
+ # activated is a Proc, it evaluates the proc with or without the value
67
+ # depending on its arity. For any other value, it returns false.
68
+ #
69
+ # @return [Boolean] true if the setting is active according to its activated
70
+ # state, false otherwise
71
+ def active?
72
+ case activated
73
+ when true
74
+ value.present?
75
+ when Symbol
76
+ !!value.send(activated)
77
+ when Proc
78
+ if activated.arity == 1
79
+ !!activated.(value)
80
+ else
81
+ !!activated.()
82
+ end
83
+ else
84
+ false
85
+ end
86
+ end
87
+
88
+ # Sets or retrieves the confirmation check logic for the configuration
89
+ # setting.
90
+ #
91
+ # This method provides access to the check configuration, which is used to
92
+ # validate that a setting meets certain criteria beyond basic required and
93
+ # default value checks. The check can be a Proc that evaluates to true,
94
+ # :unchecked_true (truthy), or false, allowing for custom validation logic. It
95
+ # deffaults to true, if not set otherwise.
96
+ #
97
+ # @return [Proc, Object] the current check configuration value
98
+ setting_accessor :check, -> setting { :unchecked_true }
99
+
100
+ # Checks if the configuration setting passes its validation check.
101
+ #
102
+ # @return [Boolean, Symbol] true if the setting's check logic evaluates to true,
103
+ # false or false if not. I no check was defined, returns :unchecked_true.
104
+ # @see check
105
+ def checked?
106
+ instance_eval(&check)
107
+ end
108
+
109
+ # Checks if a configuration setting has been provided with a valid value.
110
+ #
111
+ # @return [Boolean] true if the setting has either an environment variable
112
+ # value or a default value that is not nil, false otherwise
113
+ def value_provided?
114
+ !configured_value_or_default_value.nil?
115
+ end
116
+
117
+ # Sets whether the setting is required.
118
+ #
119
+ # @param value [Boolean, Proc] the value to set for the required flag
120
+ # - true/false: Simple boolean requirement check
121
+ # - Proc: Dynamic validation logic that can be evaluated in two ways:
122
+ # * With arity 1: Called with the setting's value (e.g., `->(value) { value.present? }`)
123
+ # * With arity 0: Called without arguments (e.g., `-> { some_value.present? }`)
124
+ # @return [Boolean, Proc] returns the value that was set
125
+ # @method required(value = nil, &block)
126
+ # @see #required?
127
+ setting_accessor :required, false
128
+
129
+ # Checks if the setting has a required value configured or as a default
130
+ # value.
131
+ #
132
+ # This method evaluates whether the configuration setting is marked as required
133
+ # and determines if a valid value is present. It handles different forms of
134
+ # required specification including boolean flags and Proc objects that can
135
+ # perform dynamic validation based on the current value or context.
136
+ #
137
+ # @return [Boolean] true if the setting is marked as required and has a valid
138
+ # value according to its validation logic, false otherwise
139
+ def required?
140
+ !!case required
141
+ when Proc
142
+ if required.arity == 1
143
+ required.(configured_value_or_default_value)
144
+ else
145
+ required.()
146
+ end
147
+ else
148
+ required
149
+ end
150
+ end
151
+
152
+ # Checks if the setting has a required value configured.
153
+ #
154
+ # @return [Boolean] true if the setting is marked as required and has a valid
155
+ # value, false otherwise
156
+ setting_accessor :sensitive, false
157
+
158
+ alias sensitive? sensitive
159
+
160
+ # Sets or retrieves the description for the configuration setting.
161
+ #
162
+ # @return [String] the current description value
163
+ setting_accessor :description
164
+
165
+ # Sets or retrieves the decoding configuration for the setting.
166
+ #
167
+ # @param value [Proc, nil] the decoding configuration value
168
+ # - Proc: A function that transforms the raw environment variable value
169
+ # - nil: No decoding applied
170
+ # @return [Proc, nil] the current decoding configuration value
171
+ # @method decode(value = nil)
172
+ # @see #decoding?
173
+ setting_accessor :decode
174
+
175
+ # Checks if the setting has decoding logic configured.
176
+
177
+ # @return [Boolean] true if the setting has a Proc-based decoding configuration,
178
+ # false otherwise
179
+ def decoding?
180
+ !!decode&.is_a?(Proc)
181
+ end
182
+
183
+ # Sets the default value for the configuration option.
184
+ #
185
+ # @param value [Object] the default value to use when no environment
186
+ # variable is set
187
+ setting_accessor :default
188
+
189
+ # Returns the default value for the configuration option, evaluating it if
190
+ # it's a Proc.
191
+ #
192
+ # @return [Object] the default value, or the result of evaluating the
193
+ # default if it's a Proc
194
+ def default_value
195
+ case default
196
+ when Proc
197
+ default.()
198
+ else
199
+ default
200
+ end
201
+ end
202
+
203
+ # Sets whether this configuration option should be ignored when reading
204
+ # values from environment variables with name env_var_name.
205
+ #
206
+ # @param value [Boolean] true if the configuration option should be
207
+ # ignored, false otherwise
208
+ setting_accessor :ignored
209
+
210
+ # Checks if the configuration setting is marked as ignored.
211
+ #
212
+ # This method returns true if the setting has been explicitly marked to be
213
+ # ignored when reading values from environment variables, indicating that
214
+ # it should be skipped during configuration processing.
215
+ #
216
+ # @return [Boolean] true if the setting is ignored, false otherwise
217
+ def ignored?
218
+ !!ignored
219
+ end
220
+
221
+ # Generates the environment variable name for this configuration option by
222
+ # constructing it from the configured prefix and name components, replacing
223
+ # namespace separators with underscores.
224
+ #
225
+ # @return [String] the constructed environment variable name
226
+ def env_var_name
227
+ prefix = @prefix.full? { "#{it}::" }.to_s
228
+ name.sub(/^#{parent_namespace}::/, prefix).gsub(/::/, ?_)
229
+ end
230
+
231
+ # Retrieves the value of the environment variable associated with this
232
+ # configuration option.
233
+ #
234
+ # @return [String, nil] the value of the environment variable if it exists,
235
+ # or nil if not set
236
+ def env_var
237
+ ENV[env_var_name]
238
+ end
239
+
240
+ # Returns the configured value for the setting, considering ignore status and
241
+ # environment variable presence.
242
+ #
243
+ # @return [String, nil] the environment variable value if the setting is not
244
+ # ignored and the environment variable is set, nil otherwise
245
+ def configured_value
246
+ if ignored
247
+ nil
248
+ elsif env_var.nil?
249
+ nil
250
+ else
251
+ env_var
252
+ end
253
+ end
254
+
255
+ # Checks if the configuration setting has been configured with a value.
256
+ #
257
+ # This method determines whether the configuration setting has been provided
258
+ # with a value, either through an environment variable or a default value.
259
+ # It returns true if a valid value is present, and false otherwise.
260
+ #
261
+ # @return [Boolean] true if the setting has been configured with a value,
262
+ # false otherwise
263
+ def configured?
264
+ !configured_value.nil?
265
+ end
266
+
267
+ # Returns the configured value for the setting, or falls back to the default
268
+ # value if not configured.
269
+ #
270
+ # @return [Object, nil] the configured value if present, otherwise the
271
+ # default value, or nil if neither is set
272
+ def configured_value_or_default_value
273
+ configured_value || default_value
274
+ end
275
+
276
+ # Retrieves the effective value for the configuration setting.
277
+ #
278
+ # This method determines the appropriate value for the configuration
279
+ # setting by first checking if the setting is not ignored and an
280
+ # environment variable value is present. If so, it uses the environment
281
+ # variable value. Otherwise, it falls back to the default value. The
282
+ # resulting value is then passed through any configured decoding logic.
283
+ #
284
+ # @return [Object] the effective configuration value after applying any
285
+ # decoding logic, or the default value if no environment variable is set
286
+ def value
287
+ decoded_value(configured_value_or_default_value)
288
+ end
289
+
290
+ # Confirms that the configuration setting and its parent module meet all
291
+ # required criteria.
292
+ #
293
+ # This method ensures that the setting has a description, that its parent
294
+ # module (if it's a ConstConf module) has a description, that any required
295
+ # values are provided, and that the setting passes its confirmation check. It
296
+ # raises appropriate exceptions if any of these validation rules are not
297
+ # satisfied.
298
+ #
299
+ # @return [ ConstConf::Setting ] returns self if all validations pass
300
+ #
301
+ # @raise [ ConstConf::RequiredDescriptionNotConfigured ] if the setting's description
302
+ # is blank or the parent module's description is missing
303
+ # @raise [ ConstConf::RequiredValueNotConfigured ] if the setting is required but no
304
+ # value is provided
305
+ # @raise [ ConstConf::SettingCheckFailed ] if the setting's check fails
306
+ def confirm!
307
+ if parent_namespace.is_a?(Module) && parent_namespace < ConstConf
308
+ parent_namespace.description.present? or
309
+ raise ConstConf::RequiredDescriptionNotConfigured,
310
+ "required description for ConstConf module #{parent_namespace} not configured"
311
+ end
312
+ if description.blank?
313
+ raise ConstConf::RequiredDescriptionNotConfigured,
314
+ "required description for setting #{env_var_name} not configured"
315
+ end
316
+ if required? && !value_provided?
317
+ raise ConstConf::RequiredValueNotConfigured,
318
+ "required value for #{env_var_name} not configured"
319
+ end
320
+ unless checked?
321
+ raise ConstConf::SettingCheckFailed, "check failed for #{name} setting"
322
+ end
323
+ self
324
+ end
325
+
326
+ # Displays a textual representation of this configuration setting.
327
+ #
328
+ # This method generates a formatted tree-like view of the current configuration
329
+ # setting, including its name, description, environment variable name, and
330
+ # associated metadata such as prefix, default value, and configuration status.
331
+ # The output can be directed to a specified IO object or displayed to the
332
+ # standard output.
333
+ #
334
+ # @param io [IO, nil] the IO object to write the output to; if nil, uses STDOUT
335
+ def view(io: nil)
336
+ parent_namespace.view(object: self, io:)
337
+ end
338
+
339
+ # Returns the string representation of the tree structure.
340
+ #
341
+ # This method generates a formatted string representation of the tree node
342
+ # and its children, including the node's name and any associated value, with
343
+ # proper indentation to show the hierarchical relationship between nodes.
344
+ #
345
+ # @return [String] a multi-line string representation of the tree structure
346
+ def to_s
347
+ io = StringIO.new
348
+ view(io:)
349
+ io.string
350
+ end
351
+
352
+ alias inspect_original inspect
353
+
354
+ # The inspect method returns a string representation of the object, with
355
+ # special handling for IRB colorization.
356
+ #
357
+ # @return [String] the string representation of the object,
358
+ # optionally stripped of ANSI color codes when used in IRB with
359
+ # colorization enabled
360
+ def inspect
361
+ if defined?(IRB) && IRB.conf[:USE_COLORIZE]
362
+ Term::ANSIColor.uncolor(to_s)
363
+ else
364
+ to_s
365
+ end
366
+ end
367
+
368
+ private
369
+
370
+ # Decodes a value using the configured decoder if present, otherwise returns the value as-is.
371
+ #
372
+ # @param value_arg [Object] the value to be decoded
373
+ # @return [Object] the decoded value or the original value if no decoding is
374
+ # configured
375
+ def decoded_value(value_arg)
376
+ if decode.is_a?(Proc)
377
+ decode.(value_arg)
378
+ else
379
+ value_arg
380
+ end
381
+ end
382
+ end
@@ -0,0 +1,103 @@
1
+ # A module that provides functionality for defining setting accessors within
2
+ # configuration modules.
3
+ #
4
+ # The SettingAccessor module enables the creation of dynamic getter and setter
5
+ # methods for configuration settings, supporting features like default values,
6
+ # block evaluation, and lazy initialization. It is designed to be included in
7
+ # classes or modules that need to manage configuration properties with ease and
8
+ # consistency.
9
+ #
10
+ # @example Defining a setting accessor
11
+ # class MyClass
12
+ # extend ConstConf::SettingAccessor
13
+ # setting_accessor :my_setting, 'default_value'
14
+ # end
15
+ module ConstConf::SettingAccessor
16
+ # @!attribute [rw] setter_mode
17
+ # Thread-local flag that controls behavior during configuration block
18
+ # evaluation.
19
+ #
20
+ # When {setter_mode} is true (during {ConstConf::Setting} initialization),
21
+ # the accessor enforces that required settings must be explicitly provided
22
+ # rather than falling back to defaults. This prevents accidental
23
+ # configuration of settings with nil values when they should be explicitly
24
+ # set.
25
+ #
26
+ # @see ConstConf::SettingAccessor#setting_accessor
27
+ # @see ConstConf::Setting#initialize
28
+ # @return [Boolean] true when in setter mode, false otherwise
29
+ thread_local :setter_mode, false
30
+
31
+ # Enables setter mode for configuration block evaluation.
32
+ #
33
+ # This method temporarily sets the setter_mode flag to true, which affects
34
+ # how setting accessors behave during configuration block evaluation. When
35
+ # setter mode is active, certain validation rules are applied to ensure that
36
+ # required settings are explicitly provided rather than falling back to
37
+ # defaults.
38
+ #
39
+ # @return [Object] the return value of the block passed to this method
40
+ def enable_setter_mode
41
+ old, self.setter_mode = self.setter_mode, true
42
+ yield
43
+ ensure
44
+ self.setter_mode = old
45
+ end
46
+
47
+ # Defines a setting accessor method that creates a dynamic getter/setter for
48
+ # configuration values.
49
+ #
50
+ # This method generates a singleton method that provides access to a
51
+ # configuration setting, supporting default values, block evaluation for
52
+ # defaults, and lazy initialization of the setting's value. The generated
53
+ # method can be used to retrieve or set the value of the configuration
54
+ # setting, with support for different types of default values including Proc
55
+ # objects which are evaluated when needed.
56
+ #
57
+ # @param name [Symbol] the name of the setting accessor to define
58
+ # @param default [Object] the default value for the setting, can be a Proc that gets evaluated
59
+ # @yield [] optional block to evaluate for default value when no explicit default is provided
60
+ # @return [Symbol] always returns the name as Symbol as it defines a method
61
+ def setting_accessor(name, default = nil, transform: nil, &block)
62
+ variable = "@#{name}"
63
+ define_method(name) do |arg = :not_set, &arg_block|
64
+ was_not_set = if arg.equal?(:not_set)
65
+ arg = nil
66
+ true
67
+ else
68
+ false
69
+ end
70
+ if arg_block
71
+ arg.nil? or raise ArgumentError,
72
+ 'only either block or positional argument allowed'
73
+ arg = arg_block
74
+ end
75
+ if arg.nil?
76
+ if self.class.respond_to?(:setter_mode)
77
+ if self.class.setter_mode && was_not_set
78
+ raise ArgumentError,
79
+ "need an argument for the setting #{name.inspect} of #{self}, was nil"
80
+ end
81
+ end
82
+ result =
83
+ if instance_variable_defined?(variable)
84
+ instance_variable_get(variable)
85
+ end
86
+ if result.nil?
87
+ result = if default.nil?
88
+ block && instance_eval(&block)
89
+ elsif default
90
+ default
91
+ end
92
+ instance_variable_set(variable, result)
93
+ result
94
+ else
95
+ result
96
+ end
97
+ else
98
+ arg = transform.(arg) if transform
99
+ instance_variable_set(variable, arg)
100
+ end
101
+ end
102
+ end
103
+ end