const_conf 0.7.0 → 0.8.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b76ad430877943dee52a91ac10544d95e84ce65894b6b261fd00804c1a98bb8f
4
- data.tar.gz: 93a117220a737ac347e37aa7c527aaa631119c68536ce773e69a6caea15c6402
3
+ metadata.gz: 733e481acb2879393f627bb74c310b0fd796471fb5c80c09bfb5c7e78fc961c8
4
+ data.tar.gz: 5870ede1708a4b3dd2e098f9086fa73d6b98783126368227e55c3ee583e94cb4
5
5
  SHA512:
6
- metadata.gz: 16bad82d96bf596c527a06fb9b5e957586d2deed54f0ce6a4ed08c65cd9f2ab17364a47e98dad8f335cd43e07cd7899b75781338cd7e789d0174ad2fcbb93df9
7
- data.tar.gz: cd453d85cb6c326264d038954ffd4f8d4b6baabb98e5658d36df533b12b956b2911c2e179dd5aef0bf8a2bb274ec5277d9c54770c232e768bac493baa54a37f6
6
+ metadata.gz: 240af1e8ed2bcc9ca5bee46abfd1f2124c4157ed7b613873dfe14b9d79f72392670bfa9176d6260a4616594fb78c34e40107a72d190b560eb2797e177047110a
7
+ data.tar.gz: 2ffdc43bba110607f565689e2c996431801102df9ea03cdba534288048b59545afc7f653ff94cd960a6b7972f9f76ce31b1674799b398facb705e60b6886aef1
data/CHANGES.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-07-20 v0.8.0
4
+
5
+ ### Changed
6
+
7
+ - Enhanced `ConstConf::JSONPlugin` API:
8
+ - Overloaded `ConstConf::JSONPlugin#json` to function as both a file loader
9
+ and a decoder factory returning a lambda.
10
+ - Introduced `ConstConf::JSONPlugin::JSONConfig` to prevent arbitrary class
11
+ materialization by stripping `'json_class'` during serialization.
12
+ - Updated documentation in `README.md` regarding the dual-mode behavior of
13
+ `JSONPlugin`.
14
+ - Fixed documentation assertions in `spec/const_conf_spec.rb` for the header
15
+ `"## ConstConf settings via environment variables"`.
16
+ - Reorganized tests in `spec/const_conf/json_plugin_spec.rb` into file and
17
+ string contexts to verify the new `#json` API.
18
+
19
+ ### Dependencies
20
+
21
+ - Updated `simplecov` to **1.0**.
22
+ - Updated `rubygems_version` to **4.0.16**.
23
+ - Updated `gem_hadar` to **2.17.1**.
24
+
3
25
  ## 2026-01-28 v0.7.0
4
26
 
5
27
  - Updated documentation method to improve ConstConf settings output
data/README.md CHANGED
@@ -290,17 +290,23 @@ end
290
290
 
291
291
  #### JSONPlugin
292
292
 
293
- Enables JSON-based configuration through the `json()` method:
293
+ Enables JSON-based configuration through the `json()` method, which acts as
294
+ both a file loader and a decoding factory.
294
295
 
295
296
  ```ruby
296
297
  CONFIG = set do
298
+ description 'JSON Configuration'
299
+ # Load from a JSON file as default
297
300
  default json('config.json')
298
- # or with custom object class:
299
- # default json('config.json', object_class: MyCustomClass)
300
-
301
+
302
+ # Decode an environment variable JSON string into a stable object
303
+ decode json
301
304
  end
302
305
  ```
303
306
 
307
+ The `json` method uses a custom `JSONConfig` class to ensure stability and
308
+ prevent arbitrary Ruby class instantiation during deserialization.
309
+
304
310
  #### YAMLPlugin
305
311
 
306
312
  Enables YAML-based configuration through the `yaml()` method:
@@ -877,6 +883,58 @@ AppConfig.view
877
883
  AppConfig::DATABASE_URL!.view
878
884
  ```
879
885
 
886
+ ## Automating Configuration Documentation
887
+
888
+ Because `ConstConf` provides a comprehensive visual representation of your
889
+ settings via `.view`, it is highly recommended to export this view to a
890
+ markdown file and commit it to your repository. This ensures that all team
891
+ members have an up-to-date reference of required environment variables without
892
+ needing to run the application.
893
+
894
+ ### 1. Create a Rake Task
895
+
896
+ First, define a rake task to export the configuration documentation. Replace
897
+ `AppConfig` with the name of your configuration module and choose a destination
898
+ path for the markdown file:
899
+
900
+ ```ruby
901
+ # lib/tasks/doc.rake
902
+ namespace :doc do
903
+ desc 'Generate ConstConf configuration documentation'
904
+ task :const_conf do
905
+ # This assumes AppConfig is your module including ConstConf
906
+ File.write('config/CONFIG_DOCS.md') do
907
+ AppConfig.view # or AppConfig.documentation if you have a helper method
908
+ end
909
+ end
910
+ end
911
+ ```
912
+
913
+ ### 2. Automate with a Git Hook
914
+
915
+ To ensure the documentation never goes stale, you can use a Git `pre-commit`
916
+ hook to regenerate the file whenever your configuration definition changes.
917
+
918
+ Add the following to `.git/hooks/pre-commit`:
919
+
920
+ ```bash
921
+ #!/usr/bin/env bash
922
+
923
+ # Regenerate config docs only if the configuration file changed
924
+ if ! git diff --quiet HEAD -- config/app_config.rb
925
+ then
926
+ echo "Configuration changed – regenerating documentation..."
927
+ bundle exec rake doc:const_conf
928
+ git add config/CONFIG_DOCS.md
929
+ fi
930
+
931
+ exit 0
932
+ ```
933
+
934
+ This workflow transforms your configuration DSL into a living document,
935
+ significantly reducing onboarding friction for new developers joining the
936
+ project. ✨
937
+
880
938
  ## Download
881
939
 
882
940
  The homepage of this library is located at
data/Rakefile CHANGED
@@ -38,7 +38,7 @@ GemHadar do
38
38
  development_dependency 'debug'
39
39
  development_dependency 'rspec', '~> 3.13'
40
40
  development_dependency 'context_spook', '~> 0.4'
41
- development_dependency 'simplecov', '~> 0.22'
41
+ development_dependency 'simplecov', '~> 1.0'
42
42
  development_dependency 'all_images', '>= 0.12'
43
43
 
44
44
  licenses << 'MIT'
data/const_conf.gemspec CHANGED
@@ -1,9 +1,9 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: const_conf 0.7.0 ruby lib
2
+ # stub: const_conf 0.8.0 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "const_conf".freeze
6
- s.version = "0.7.0".freeze
6
+ s.version = "0.8.0".freeze
7
7
 
8
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
9
9
  s.require_paths = ["lib".freeze]
@@ -17,17 +17,17 @@ Gem::Specification.new do |s|
17
17
  s.licenses = ["MIT".freeze]
18
18
  s.rdoc_options = ["--title".freeze, "ConstConf - Clean DSL for config settings with validation and Rails integration".freeze, "--main".freeze, "README.md".freeze]
19
19
  s.required_ruby_version = Gem::Requirement.new(">= 3.2".freeze)
20
- s.rubygems_version = "4.0.3".freeze
20
+ s.rubygems_version = "4.0.16".freeze
21
21
  s.summary = "Clean DSL for config settings with validation and Rails integration".freeze
22
22
  s.test_files = ["spec/const_conf/dir_plugin_spec.rb".freeze, "spec/const_conf/env_dir_extension_spec.rb".freeze, "spec/const_conf/file_plugin_spec.rb".freeze, "spec/const_conf/json_plugin_spec.rb".freeze, "spec/const_conf/setting_accessor_spec.rb".freeze, "spec/const_conf/setting_spec.rb".freeze, "spec/const_conf/tree_spec.rb".freeze, "spec/const_conf/yaml_plugin_spec.rb".freeze, "spec/const_conf_spec.rb".freeze, "spec/spec_helper.rb".freeze]
23
23
 
24
24
  s.specification_version = 4
25
25
 
26
- s.add_development_dependency(%q<gem_hadar>.freeze, [">= 2.17.0".freeze])
26
+ s.add_development_dependency(%q<gem_hadar>.freeze, [">= 2.17.1".freeze])
27
27
  s.add_development_dependency(%q<debug>.freeze, [">= 0".freeze])
28
28
  s.add_development_dependency(%q<rspec>.freeze, ["~> 3.13".freeze])
29
29
  s.add_development_dependency(%q<context_spook>.freeze, ["~> 0.4".freeze])
30
- s.add_development_dependency(%q<simplecov>.freeze, ["~> 0.22".freeze])
30
+ s.add_development_dependency(%q<simplecov>.freeze, ["~> 1.0".freeze])
31
31
  s.add_development_dependency(%q<all_images>.freeze, [">= 0.12".freeze])
32
32
  s.add_runtime_dependency(%q<tins>.freeze, ["~> 1.43".freeze])
33
33
  s.add_runtime_dependency(%q<json>.freeze, ["~> 2.0".freeze])
@@ -1,37 +1,60 @@
1
1
  require 'json'
2
2
 
3
- # A module that provides functionality for reading and parsing JSON file
4
- # contents as configuration values.
3
+ # A module that provides functionality for loading JSON files or parsing
4
+ # JSON strings as configuration values.
5
5
  #
6
6
  # The JSONPlugin module extends the ConstConf::Setting class to enable
7
- # configuration settings that are sourced from JSON files. This allows for
8
- # structured configuration data to be loaded from files and used within the
9
- # application's configuration system, supporting both standard JSON parsing and
10
- # environment-specific configuration loading.
7
+ # configuration settings that are either sourced from JSON files on disk
8
+ # or decoded from JSON-formatted environment variables. It ensures a stable,
9
+ # polymorphic representation by using the {ConstConf::JSONPlugin::JSONConfig}
10
+ # class for all parsed objects.
11
11
  module ConstConf::JSONPlugin
12
- # Reads and parses a JSON file, optionally with environment-specific loading
12
+ # JSONConfig - Custom JSON object class for config values
13
13
  #
14
- # This method attempts to read and parse a JSON file at the specified path.
15
- # It supports environment-specific configuration loading when the env
16
- # parameter is provided. The method uses thread synchronization to ensure
17
- # safe access to the ComplexConfig provider.
14
+ # Rejects 'json_class' during serialization to prevent forcing concrete
15
+ # class re-materialization on deserialization. Useful for polymorphic
16
+ # configuration handling.
17
+ class JSONConfig < JSON::GenericObject
18
+ # as_json - Serialize hash without metadata keys
19
+ # @return [Hash] Hash representation excluding json_class keys
20
+ def as_json(*a)
21
+ super.reject { _1 == 'json_class' }
22
+ end
23
+ end
24
+
25
+ # Provides JSON loading or decoding logic based on the presence of a path.
26
+ #
27
+ # === As a Loader (Path provided)
28
+ # When a +path+ is given, it reads the file from the filesystem and parses
29
+ # its content into a {JSONConfig} object.
30
+ #
31
+ # === As a Factory (No path provided)
32
+ # When called without arguments, it returns a Proc that can be used in
33
+ # a +decode+ block to parse JSON strings (e.g., from environment variables).
18
34
  #
19
- # @param path [String] the filesystem path to the JSON file to be read
20
- # @param required [Boolean] whether the file is required to exist, defaults to false
35
+ # @param path [String, nil] the filesystem path to the JSON file; if omitted,
36
+ # a decoder Proc is returned instead.
37
+ # @param required [Boolean] whether the file must exist (only applicable
38
+ # when +path+ is provided), defaults to false.
39
+ # @param object_class [Class] the class used for parsing JSON objects,
40
+ # defaults to {ConstConf::JSONPlugin::JSONConfig}.
21
41
  #
22
- # @return [Object, nil] the parsed JSON content as a Ruby object, or nil if
23
- # the file doesn't exist and required is false
42
+ # @return [JSONConfig, Proc, nil] returns a parsed {JSONConfig} (or nil if
43
+ # missing and not required) when +path+ is provided; otherwise, returns a
44
+ # Proc for decoding.
24
45
  #
25
46
  # @raise [ConstConf::RequiredValueNotConfigured] if the file does not exist
26
- # and required is true
27
- # @raise [ConstConf::RequiredValueNotConfigured] if env is true and RAILS_ENV
28
- # is not set and no explicit environment is provided
29
- def json(path, required: false, object_class: JSON::GenericObject)
30
- if File.exist?(path)
31
- JSON.load_file(path, object_class:)
32
- elsif required
33
- raise ConstConf::RequiredValueNotConfigured,
34
- "JSON file required at path #{path.to_s.inspect}"
47
+ # and +required+ is true.
48
+ def json(path = nil, required: false, object_class: JSONConfig)
49
+ if path
50
+ if File.exist?(path)
51
+ JSON.load_file(path, object_class:)
52
+ elsif required
53
+ raise ConstConf::RequiredValueNotConfigured,
54
+ "JSON file required at path #{path.to_s.inspect}"
55
+ end
56
+ else
57
+ -> value { JSON.parse(value, object_class:) unless value.nil? }
35
58
  end
36
59
  end
37
60
  end
@@ -1,6 +1,6 @@
1
1
  module ConstConf
2
2
  # ConstConf version
3
- VERSION = '0.7.0'
3
+ VERSION = '0.8.0'
4
4
  VERSION_ARRAY = VERSION.split('.').map(&:to_i) # :nodoc:
5
5
  VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
6
6
  VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
@@ -1,63 +1,120 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe ConstConf::JSONPlugin do
4
- let(:json_file) { asset('config.json') }
4
+ context 'with file' do
5
+ let(:json_file) { asset('config.json') }
5
6
 
6
- describe '#json' do
7
- context 'when file exists' do
8
- it 'reads and parses JSON content as a hash' do
9
- instance = double('Config').extend(described_class)
10
- result = instance.json(json_file)
11
- expect(result.hello).to eq 'world'
7
+ describe '#json' do
8
+ context 'when file exists' do
9
+ it 'reads and parses JSON content as a hash' do
10
+ instance = double('Config').extend(described_class)
11
+ result = instance.json(json_file)
12
+ expect(result.hello).to eq 'world'
13
+ end
14
+
15
+ it 'handles JSON with custom object_class' do
16
+ instance = double('Config').extend(described_class)
17
+ foobar_class = Class.new(JSON::GenericObject)
18
+ result = instance.json(json_file, object_class: foobar_class)
19
+ expect(result).to be_a(foobar_class)
20
+ end
12
21
  end
13
22
 
14
- it 'handles JSON with custom object_class' do
15
- instance = double('Config').extend(described_class)
16
- foobar_class = Class.new(JSON::GenericObject)
17
- result = instance.json(json_file, object_class: foobar_class)
18
- expect(result).to be_a(foobar_class)
23
+ context 'when file does not exist' do
24
+ let(:nonexistent_file) { asset('nonexistent.json') }
25
+
26
+ it 'returns nil when file does not exist and required is false' do
27
+ instance = double('Config').extend(described_class)
28
+ result = instance.json(nonexistent_file, required: false)
29
+ expect(result).to be_nil
30
+ end
31
+
32
+ it 'raises RequiredValueNotConfigured when file is required and does not exist' do
33
+ instance = double('Config').extend(described_class)
34
+ expect {
35
+ instance.json(nonexistent_file, required: true)
36
+ }.to raise_error(ConstConf::RequiredValueNotConfigured)
37
+ end
38
+ end
39
+
40
+ context 'with custom configuration module' do
41
+ before do
42
+ eval %{
43
+ if Object.const_defined?(:ConstConfTestModuleWithJSON)
44
+ Object.send(:remove_const, :ConstConfTestModuleWithJSON)
45
+ end
46
+ module ::ConstConfTestModuleWithJSON
47
+ include ConstConf
48
+ plugin ConstConf::JSONPlugin
49
+
50
+ description 'Module with JSONPlugin'
51
+
52
+ CONFIG_VALUE = set do
53
+ description 'Configuration from JSON file'
54
+ default json("#{json_file}")
55
+ end
56
+ end
57
+ }
58
+ end
59
+
60
+ it 'can be used in a ConstConf module' do
61
+ expect(ConstConfTestModuleWithJSON::CONFIG_VALUE.hello).to eq 'world'
62
+ end
19
63
  end
20
64
  end
65
+ end
66
+
67
+ context 'with string frome env var' do
68
+ describe '#json' do
69
+ let(:instance) { double('Config').extend(described_class) }
21
70
 
22
- context 'when file does not exist' do
23
- let(:nonexistent_file) { asset('nonexistent.json') }
71
+ it 'returns a lambda that parses JSON strings into JSONConfig objects' do
72
+ decoder = instance.json
73
+ expect(decoder).to be_a Proc
24
74
 
25
- it 'returns nil when file does not exist and required is false' do
26
- instance = double('Config').extend(described_class)
27
- result = instance.json(nonexistent_file, required: false)
28
- expect(result).to be_nil
75
+ json_string = '{"hello": "world"}'
76
+ result = decoder.(json_string)
77
+
78
+ expect(result).to be_a ConstConf::JSONPlugin::JSONConfig
79
+ expect(result.hello).to eq 'world'
29
80
  end
30
81
 
31
- it 'raises RequiredValueNotConfigured when file is required and does not exist' do
32
- instance = double('Config').extend(described_class)
33
- expect {
34
- instance.json(nonexistent_file, required: true)
35
- }.to raise_error(ConstConf::RequiredValueNotConfigured)
82
+ it 'returns nil when the input value is nil' do
83
+ decoder = instance.json
84
+ expect(decoder.(nil)).to be_nil
36
85
  end
37
- end
38
86
 
39
- context 'with custom configuration module' do
40
- before do
41
- eval %{
42
- if Object.const_defined?(:ConstConfTestModuleWithJSON)
43
- Object.send(:remove_const, :ConstConfTestModuleWithJSON)
87
+ context 'integration with ConstConf settings' do
88
+ let :build_config do
89
+ eval %{
90
+ if Object.const_defined?(:ConstConfTestModuleWithJSONDecode)
91
+ Object.send(:remove_const, :ConstConfTestModuleWithJSONDecode)
44
92
  end
45
- module ::ConstConfTestModuleWithJSON
93
+ module ::ConstConfTestModuleWithJSONDecode
46
94
  include ConstConf
47
95
  plugin ConstConf::JSONPlugin
96
+ prefix 'TEST'
97
+ description 'Test module for JSON decode'
48
98
 
49
- description 'Module with JSONPlugin'
50
-
51
- CONFIG_VALUE = set do
52
- description 'Configuration from JSON file'
53
- default json("#{json_file}")
99
+ JSON_SETTING = set do
100
+ description 'Setting decoded from JSON string'
101
+ default '{"foo": "bar"}'
102
+ decode json
54
103
  end
55
104
  end
56
- }
57
- end
105
+ }
106
+ end
107
+
108
+ it 'handles default JSON strings' do
109
+ build_config
110
+ expect(ConstConfTestModuleWithJSONDecode::JSON_SETTING.foo).to eq 'bar'
111
+ end
58
112
 
59
- it 'can be used in a ConstConf module' do
60
- expect(ConstConfTestModuleWithJSON::CONFIG_VALUE.hello).to eq 'world'
113
+ it 'handles environment variable overrides with JSON strings' do
114
+ ENV['TEST_JSON_SETTING'] = '{"foo": "env_value"}'
115
+ build_config
116
+ expect(ConstConfTestModuleWithJSONDecode::JSON_SETTING.foo).to eq 'env_value'
117
+ end
61
118
  end
62
119
  end
63
120
  end
@@ -271,7 +271,7 @@ describe ConstConf do
271
271
  it 'generates markdown documentation' do
272
272
  doc = TestConstConf.documentation
273
273
  expect(doc).to be_a String
274
- expect(doc).to include('## Environment variables')
274
+ expect(doc).to include('## ConstConf settings via environment variables')
275
275
  expect(doc).to include('`TEST_CONST_CONF_TEST`')
276
276
  expect(doc).to match(/description.*?test/)
277
277
  expect(doc).to match(/default.*?bar/)
@@ -283,7 +283,7 @@ describe ConstConf do
283
283
 
284
284
  expect(result).to be_a StringIO
285
285
  doc = io.string
286
- expect(doc).to include('## Environment variables')
286
+ expect(doc).to include('## ConstConf settings via environment variables')
287
287
  expect(doc).to include('`TEST_CONST_CONF_TEST`')
288
288
  expect(doc).to match(/description.*?test/)
289
289
  expect(doc).to match(/default.*?bar/)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: const_conf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Florian Frank
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 2.17.0
18
+ version: 2.17.1
19
19
  type: :development
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: 2.17.0
25
+ version: 2.17.1
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: debug
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -71,14 +71,14 @@ dependencies:
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: '0.22'
74
+ version: '1.0'
75
75
  type: :development
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
- version: '0.22'
81
+ version: '1.0'
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: all_images
84
84
  requirement: !ruby/object:Gem::Requirement
@@ -229,7 +229,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
229
229
  - !ruby/object:Gem::Version
230
230
  version: '0'
231
231
  requirements: []
232
- rubygems_version: 4.0.3
232
+ rubygems_version: 4.0.16
233
233
  specification_version: 4
234
234
  summary: Clean DSL for config settings with validation and Rails integration
235
235
  test_files: