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.
- checksums.yaml +7 -0
- data/.ace-defaults/config/config.yml +23 -0
- data/CHANGELOG.md +224 -0
- data/LICENSE +21 -0
- data/README.md +35 -0
- data/Rakefile +15 -0
- data/lib/ace/support/config/atoms/deep_merger.rb +126 -0
- data/lib/ace/support/config/atoms/path_rule_matcher.rb +118 -0
- data/lib/ace/support/config/atoms/path_validator.rb +76 -0
- data/lib/ace/support/config/atoms/yaml_parser.rb +50 -0
- data/lib/ace/support/config/errors.rb +24 -0
- data/lib/ace/support/config/models/cascade_path.rb +94 -0
- data/lib/ace/support/config/models/config.rb +134 -0
- data/lib/ace/support/config/models/config_group.rb +57 -0
- data/lib/ace/support/config/molecules/config_finder.rb +230 -0
- data/lib/ace/support/config/molecules/file_config_resolver.rb +419 -0
- data/lib/ace/support/config/molecules/project_config_scanner.rb +164 -0
- data/lib/ace/support/config/molecules/yaml_loader.rb +81 -0
- data/lib/ace/support/config/organisms/config_resolver.rb +349 -0
- data/lib/ace/support/config/organisms/virtual_config_resolver.rb +141 -0
- data/lib/ace/support/config/version.rb +9 -0
- data/lib/ace/support/config.rb +277 -0
- data/lib/ace/support.rb +4 -0
- metadata +109 -0
|
@@ -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
|
data/lib/ace/support.rb
ADDED
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: []
|