ace-support-config 0.9.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,277 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config/version"
4
+ require_relative "config/errors"
5
+
6
+ # Use ace-support-fs for path/project utilities
7
+ require "ace/support/fs"
8
+
9
+ # Load atoms first (no dependencies)
10
+ require_relative "config/atoms/deep_merger"
11
+ require_relative "config/atoms/path_validator"
12
+ require_relative "config/atoms/path_rule_matcher"
13
+ require_relative "config/atoms/yaml_parser"
14
+
15
+ # Load models (depend on atoms)
16
+ require_relative "config/models/cascade_path"
17
+ require_relative "config/models/config"
18
+ require_relative "config/models/config_group"
19
+
20
+ # Load molecules (depend on atoms, models)
21
+ require_relative "config/molecules/yaml_loader"
22
+ require_relative "config/molecules/config_finder"
23
+ require_relative "config/molecules/file_config_resolver"
24
+ require_relative "config/molecules/project_config_scanner"
25
+
26
+ # Load organisms (depend on molecules)
27
+ require_relative "config/organisms/config_resolver"
28
+ require_relative "config/organisms/virtual_config_resolver"
29
+
30
+ module Ace
31
+ # Generic configuration cascade management
32
+ #
33
+ # Provides a reusable configuration cascade system with customizable
34
+ # folder names, supporting project-level, user-level, and gem-level
35
+ # configuration with deep merging and priority-based resolution.
36
+ #
37
+ # @example Basic usage with defaults
38
+ # config = Ace::Support::Config.create
39
+ # config.get("key", "nested")
40
+ #
41
+ # @example Custom folder names
42
+ # config = Ace::Support::Config.create(
43
+ # config_dir: ".my-app",
44
+ # defaults_dir: ".my-app-defaults"
45
+ # )
46
+ #
47
+ # @example With gem defaults
48
+ # config = Ace::Support::Config.create(
49
+ # gem_path: __dir__,
50
+ # defaults_dir: ".ace-defaults"
51
+ # )
52
+ #
53
+ # @example Test mode (skip filesystem searches)
54
+ # Ace::Support::Config.test_mode = true
55
+ # config = Ace::Support::Config.create # Returns empty config immediately
56
+ #
57
+ # # Or with mock data
58
+ # Ace::Support::Config.default_mock = { "key" => "value" }
59
+ # config = Ace::Support::Config.create # Returns mock config
60
+ #
61
+ module Support
62
+ module Config
63
+ # Default folder name for user configuration
64
+ DEFAULT_CONFIG_DIR = ".ace"
65
+
66
+ # Default folder name for gem defaults
67
+ DEFAULT_DEFAULTS_DIR = ".ace-defaults"
68
+
69
+ # Default project root markers
70
+ DEFAULT_PROJECT_MARKERS = %w[
71
+ .git
72
+ Gemfile
73
+ package.json
74
+ Cargo.toml
75
+ pyproject.toml
76
+ go.mod
77
+ .hg
78
+ .svn
79
+ Rakefile
80
+ Makefile
81
+ ].freeze
82
+
83
+ class << self
84
+ # Thread-local test mode state
85
+ # Uses Thread.current for true thread isolation in parallel test environments
86
+ # @return [Boolean, nil] Whether test mode is enabled
87
+ def test_mode
88
+ Thread.current[:ace_config_test_mode]
89
+ end
90
+
91
+ # Set thread-local test mode state
92
+ # @param value [Boolean, nil] Whether test mode is enabled
93
+ def test_mode=(value)
94
+ Thread.current[:ace_config_test_mode] = value
95
+ end
96
+
97
+ # Thread-local mock configuration data to return in test mode
98
+ # @return [Hash, nil] Mock configuration data
99
+ def default_mock
100
+ Thread.current[:ace_config_default_mock]
101
+ end
102
+
103
+ # Set thread-local mock configuration data
104
+ # @param value [Hash, nil] Mock configuration data
105
+ def default_mock=(value)
106
+ Thread.current[:ace_config_default_mock] = value
107
+ end
108
+
109
+ # Check if test mode is active
110
+ #
111
+ # Test mode is active when:
112
+ # 1. Ace::Support::Config.test_mode is explicitly set to true
113
+ # 2. ACE_CONFIG_TEST_MODE environment variable is set to "1" or "true" (case-insensitive)
114
+ #
115
+ # Note: We intentionally do NOT auto-detect based on Minitest being loaded,
116
+ # as that would break tests that need to test real filesystem access
117
+ # (like ace-config's own tests). Use explicit opt-in instead.
118
+ #
119
+ # Note: ENV lookup is intentionally NOT memoized to allow dynamic control
120
+ # of test_mode via environment variable changes at runtime.
121
+ #
122
+ # @return [Boolean] True if test mode is active
123
+ def test_mode?
124
+ # Note: test_mode (without ?) is the thread-local getter defined above,
125
+ # not a recursive call - it reads from Thread.current[:ace_config_test_mode]
126
+ return true if test_mode == true
127
+
128
+ env_value = ENV["ACE_CONFIG_TEST_MODE"]
129
+ return false if env_value.nil?
130
+ return true if env_value == "1"
131
+ return true if env_value.casecmp("true").zero?
132
+
133
+ false
134
+ end
135
+
136
+ # Create a new configuration resolver with customizable options
137
+ #
138
+ # @param config_dir [String] User config folder name (default: ".ace")
139
+ # @param defaults_dir [String] Gem defaults folder name (default: ".ace-defaults")
140
+ # @param gem_path [String, nil] Optional gem root for defaults
141
+ # @param merge_strategy [Symbol] Array merge strategy (:replace, :concat, :union)
142
+ # @param cache_namespaces [Boolean] Whether to cache resolve_namespace results (default: false)
143
+ # @param test_mode [Boolean, nil] Force test mode on/off (default: nil = auto-detect)
144
+ # @param mock_config [Hash, nil] Mock config data for test mode (default: nil = use default_mock)
145
+ # @return [Organisms::ConfigResolver] Configuration resolver instance
146
+ #
147
+ # @example Create with defaults
148
+ # config = Ace::Support::Config.create
149
+ # value = config.get("some", "key")
150
+ #
151
+ # @example Create with custom folders
152
+ # config = Ace::Support::Config.create(
153
+ # config_dir: ".my-app",
154
+ # defaults_dir: ".my-app-defaults",
155
+ # gem_path: File.expand_path("..", __dir__)
156
+ # )
157
+ #
158
+ # @example Create with namespace caching for performance
159
+ # config = Ace::Support::Config.create(cache_namespaces: true)
160
+ # config.resolve_namespace("my_gem") # reads from disk
161
+ # config.resolve_namespace("my_gem") # returns cached result
162
+ #
163
+ # @example Test mode with mock config
164
+ # config = Ace::Support::Config.create(test_mode: true, mock_config: { "key" => "value" })
165
+ # config.resolve.get("key") # => "value"
166
+ #
167
+ def create(
168
+ config_dir: DEFAULT_CONFIG_DIR,
169
+ defaults_dir: DEFAULT_DEFAULTS_DIR,
170
+ gem_path: nil,
171
+ merge_strategy: :replace,
172
+ cache_namespaces: false,
173
+ test_mode: nil,
174
+ mock_config: nil
175
+ )
176
+ # Determine effective test mode
177
+ effective_test_mode = test_mode.nil? ? test_mode? : test_mode
178
+
179
+ Organisms::ConfigResolver.new(
180
+ config_dir: config_dir,
181
+ defaults_dir: defaults_dir,
182
+ gem_path: gem_path,
183
+ merge_strategy: merge_strategy,
184
+ cache_namespaces: cache_namespaces,
185
+ test_mode: effective_test_mode,
186
+ mock_config: mock_config || default_mock
187
+ )
188
+ end
189
+
190
+ # Create a configuration finder for lower-level access
191
+ #
192
+ # @param config_dir [String] Config folder name
193
+ # @param defaults_dir [String] Defaults folder name
194
+ # @param gem_path [String, nil] Gem root path
195
+ # @return [Molecules::ConfigFinder] Configuration finder instance
196
+ def finder(config_dir: DEFAULT_CONFIG_DIR, defaults_dir: DEFAULT_DEFAULTS_DIR, gem_path: nil)
197
+ Molecules::ConfigFinder.new(
198
+ config_dir: config_dir,
199
+ defaults_dir: defaults_dir,
200
+ gem_path: gem_path
201
+ )
202
+ end
203
+
204
+ # Create a path expander with explicit context
205
+ #
206
+ # @param source_dir [String] Source document directory
207
+ # @param project_root [String] Project root directory
208
+ # @return [Ace::Support::Fs::Atoms::PathExpander] Path expander instance
209
+ def path_expander(source_dir:, project_root:)
210
+ Ace::Support::Fs::Atoms::PathExpander.new(source_dir: source_dir, project_root: project_root)
211
+ end
212
+
213
+ # Find project root from a starting path
214
+ #
215
+ # @param start_path [String, nil] Path to start searching from
216
+ # @param markers [Array<String>] Project root markers
217
+ # @return [String, nil] Project root path or nil
218
+ def find_project_root(start_path: nil, markers: DEFAULT_PROJECT_MARKERS)
219
+ Ace::Support::Fs::Molecules::ProjectRootFinder.find(start_path: start_path, markers: markers)
220
+ end
221
+
222
+ # Create a virtual config resolver for path-based lookups
223
+ #
224
+ # VirtualConfigResolver provides a "virtual filesystem" view where nearest
225
+ # config file wins. Useful for finding presets and resources across the
226
+ # configuration cascade without loading/merging YAML content.
227
+ #
228
+ # @param config_dir [String] Config folder name (default: ".ace")
229
+ # @param defaults_dir [String] Defaults folder name (default: ".ace-defaults")
230
+ # @param start_path [String, nil] Starting path for traversal (default: Dir.pwd)
231
+ # @param gem_path [String, nil] Gem root path for defaults (lowest priority)
232
+ # @return [Organisms::VirtualConfigResolver] Virtual resolver instance
233
+ #
234
+ # @example Find preset files across cascade
235
+ # resolver = Ace::Support::Config.virtual_resolver
236
+ # resolver.glob("presets/*.yml").each do |relative, absolute|
237
+ # puts "Found: #{relative} at #{absolute}"
238
+ # end
239
+ #
240
+ # @example Check if resource exists with gem defaults
241
+ # resolver = Ace::Support::Config.virtual_resolver(
242
+ # config_dir: ".my-app",
243
+ # gem_path: File.expand_path("..", __dir__)
244
+ # )
245
+ # if resolver.exists?("templates/default.md")
246
+ # path = resolver.resolve_path("templates/default.md")
247
+ # end
248
+ #
249
+ def virtual_resolver(
250
+ config_dir: DEFAULT_CONFIG_DIR,
251
+ defaults_dir: DEFAULT_DEFAULTS_DIR,
252
+ start_path: nil,
253
+ gem_path: nil
254
+ )
255
+ Organisms::VirtualConfigResolver.new(
256
+ config_dir: config_dir,
257
+ defaults_dir: defaults_dir,
258
+ start_path: start_path,
259
+ gem_path: gem_path
260
+ )
261
+ end
262
+
263
+ # Reset all cached configuration state
264
+ #
265
+ # Per ADR-022, this method allows test isolation by clearing all cached
266
+ # state in the configuration system.
267
+ #
268
+ # @return [void]
269
+ def reset_config!
270
+ Ace::Support::Fs::Molecules::ProjectRootFinder.clear_cache!
271
+ Thread.current[:ace_config_test_mode] = nil
272
+ Thread.current[:ace_config_default_mock] = nil
273
+ end
274
+ end
275
+ end
276
+ end
277
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Entry point for ace-support-config gem
4
+ require_relative "support/config"
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ace-support-config
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ platform: ruby
6
+ authors:
7
+ - Michal Czyz
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ace-support-fs
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.2'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.2'
26
+ - !ruby/object:Gem::Dependency
27
+ name: minitest
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '5.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '13.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '13.0'
54
+ description: Reusable configuration cascade with customizable folder names. Supports
55
+ project-level, user-level, and gem-level configuration with deep merging and priority-based
56
+ resolution.
57
+ email:
58
+ - mc@cs3b.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".ace-defaults/config/config.yml"
64
+ - CHANGELOG.md
65
+ - LICENSE
66
+ - README.md
67
+ - Rakefile
68
+ - lib/ace/support.rb
69
+ - lib/ace/support/config.rb
70
+ - lib/ace/support/config/atoms/deep_merger.rb
71
+ - lib/ace/support/config/atoms/path_rule_matcher.rb
72
+ - lib/ace/support/config/atoms/path_validator.rb
73
+ - lib/ace/support/config/atoms/yaml_parser.rb
74
+ - lib/ace/support/config/errors.rb
75
+ - lib/ace/support/config/models/cascade_path.rb
76
+ - lib/ace/support/config/models/config.rb
77
+ - lib/ace/support/config/models/config_group.rb
78
+ - lib/ace/support/config/molecules/config_finder.rb
79
+ - lib/ace/support/config/molecules/file_config_resolver.rb
80
+ - lib/ace/support/config/molecules/project_config_scanner.rb
81
+ - lib/ace/support/config/molecules/yaml_loader.rb
82
+ - lib/ace/support/config/organisms/config_resolver.rb
83
+ - lib/ace/support/config/organisms/virtual_config_resolver.rb
84
+ - lib/ace/support/config/version.rb
85
+ homepage: https://github.com/cs3b/ace/tree/main/ace-support-config
86
+ licenses:
87
+ - MIT
88
+ metadata:
89
+ homepage_uri: https://github.com/cs3b/ace/tree/main/ace-support-config
90
+ source_code_uri: https://github.com/cs3b/ace/tree/main/ace-support-config/tree/main/ace-support-config/
91
+ changelog_uri: https://github.com/cs3b/ace/blob/main/ace-support-config/CHANGELOG.md
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: 3.2.0
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubygems_version: 3.6.9
107
+ specification_version: 4
108
+ summary: Generic configuration cascade management
109
+ test_files: []