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,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "date"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Support
|
|
8
|
+
module Config
|
|
9
|
+
module Atoms
|
|
10
|
+
# Pure YAML parsing functions
|
|
11
|
+
module YamlParser
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Parse YAML string into Ruby hash
|
|
15
|
+
# @param yaml_string [String] YAML content to parse
|
|
16
|
+
# @return [Hash] Parsed YAML content
|
|
17
|
+
# @raise [YamlParseError] if parsing fails
|
|
18
|
+
def parse(yaml_string)
|
|
19
|
+
return {} if yaml_string.nil? || yaml_string.strip.empty?
|
|
20
|
+
|
|
21
|
+
YAML.safe_load(yaml_string, permitted_classes: [Symbol, Date], aliases: true)
|
|
22
|
+
rescue Psych::SyntaxError => e
|
|
23
|
+
raise YamlParseError, "Failed to parse YAML: #{e.message}"
|
|
24
|
+
rescue Encoding::CompatibilityError, Encoding::InvalidByteSequenceError => e
|
|
25
|
+
raise YamlParseError, "Failed to parse YAML (encoding error): #{e.message}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Convert Ruby hash to YAML string
|
|
29
|
+
# @param data [Hash] Data to convert
|
|
30
|
+
# @return [String] YAML representation
|
|
31
|
+
def dump(data)
|
|
32
|
+
return "" if data.nil? || data.empty?
|
|
33
|
+
|
|
34
|
+
YAML.dump(data)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Check if string is valid YAML
|
|
38
|
+
# @param yaml_string [String] YAML content to validate
|
|
39
|
+
# @return [Boolean] true if valid YAML
|
|
40
|
+
def valid?(yaml_string)
|
|
41
|
+
parse(yaml_string)
|
|
42
|
+
true
|
|
43
|
+
rescue YamlParseError
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/fs"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Support
|
|
7
|
+
module Config
|
|
8
|
+
# Base error class for all ace-support-config errors
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
# Raised when configuration file is not found
|
|
12
|
+
class ConfigNotFoundError < Error; end
|
|
13
|
+
|
|
14
|
+
# Raised when YAML parsing fails
|
|
15
|
+
class YamlParseError < Error; end
|
|
16
|
+
|
|
17
|
+
# Raised when a path cannot be resolved
|
|
18
|
+
class PathError < Error; end
|
|
19
|
+
|
|
20
|
+
# Raised when merge strategy is invalid
|
|
21
|
+
class MergeStrategyError < Error; end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Support
|
|
5
|
+
module Config
|
|
6
|
+
module Models
|
|
7
|
+
# Represents a configuration cascade path
|
|
8
|
+
class CascadePath
|
|
9
|
+
attr_reader :path, :priority, :exists, :type
|
|
10
|
+
|
|
11
|
+
# Initialize cascade path
|
|
12
|
+
# @param path [String] File system path
|
|
13
|
+
# @param priority [Integer] Priority (lower = higher priority)
|
|
14
|
+
# @param exists [Boolean] Whether path exists
|
|
15
|
+
# @param type [Symbol] Type of path (:local, :home, :gem)
|
|
16
|
+
def initialize(path:, priority: 100, exists: false, type: :local)
|
|
17
|
+
@path = path.to_s.freeze
|
|
18
|
+
@priority = priority
|
|
19
|
+
@exists = exists
|
|
20
|
+
@type = type
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Compare paths by priority
|
|
25
|
+
# @param other [CascadePath] Other path to compare
|
|
26
|
+
# @return [Integer] Comparison result
|
|
27
|
+
def <=>(other)
|
|
28
|
+
return nil unless other.is_a?(CascadePath)
|
|
29
|
+
|
|
30
|
+
priority <=> other.priority
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Check if this path should override another
|
|
34
|
+
# @param other [CascadePath] Other path
|
|
35
|
+
# @return [Boolean] true if this path has higher priority
|
|
36
|
+
def overrides?(other)
|
|
37
|
+
return false unless other.is_a?(CascadePath)
|
|
38
|
+
|
|
39
|
+
priority < other.priority
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Convert to string
|
|
43
|
+
# @return [String] The file path
|
|
44
|
+
def to_s
|
|
45
|
+
path
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Get path as Pathname
|
|
49
|
+
# @return [Pathname] Path object
|
|
50
|
+
def pathname
|
|
51
|
+
require "pathname"
|
|
52
|
+
Pathname.new(path)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Check if path is absolute
|
|
56
|
+
# @return [Boolean] true if absolute path
|
|
57
|
+
def absolute?
|
|
58
|
+
pathname.absolute?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Check if path is relative
|
|
62
|
+
# @return [Boolean] true if relative path
|
|
63
|
+
def relative?
|
|
64
|
+
!absolute?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def ==(other)
|
|
68
|
+
other.is_a?(self.class) &&
|
|
69
|
+
other.path == path &&
|
|
70
|
+
other.priority == priority &&
|
|
71
|
+
other.type == type
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def hash
|
|
75
|
+
[path, priority, type].hash
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
alias_method :eql?, :==
|
|
79
|
+
|
|
80
|
+
def inspect
|
|
81
|
+
attrs = [
|
|
82
|
+
"path=#{path.inspect}",
|
|
83
|
+
"type=#{type}",
|
|
84
|
+
"priority=#{priority}",
|
|
85
|
+
exists ? "exists" : "missing"
|
|
86
|
+
].join(" ")
|
|
87
|
+
|
|
88
|
+
"#<#{self.class.name} #{attrs}>"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Support
|
|
5
|
+
module Config
|
|
6
|
+
module Models
|
|
7
|
+
# Configuration data structure
|
|
8
|
+
class Config
|
|
9
|
+
attr_reader :data, :source, :merge_strategy
|
|
10
|
+
|
|
11
|
+
# Initialize configuration
|
|
12
|
+
# @param data [Hash] Configuration data
|
|
13
|
+
# @param source [String] Source file or identifier
|
|
14
|
+
# @param merge_strategy [Symbol] How to merge with other configs
|
|
15
|
+
def initialize(data = {}, source: nil, merge_strategy: :replace)
|
|
16
|
+
@data = data || {}
|
|
17
|
+
@source = source
|
|
18
|
+
@merge_strategy = merge_strategy
|
|
19
|
+
freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Get configuration value by key path
|
|
23
|
+
# @param keys [Array<String,Symbol>] Path to value
|
|
24
|
+
# @return [Object] Value at path or nil
|
|
25
|
+
def get(*keys)
|
|
26
|
+
keys = keys.flatten.map(&:to_s)
|
|
27
|
+
keys.reduce(data) do |current, key|
|
|
28
|
+
return nil unless current.is_a?(Hash)
|
|
29
|
+
|
|
30
|
+
current[key]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Check if configuration has key path
|
|
35
|
+
# @param keys [Array<String,Symbol>] Path to check
|
|
36
|
+
# @return [Boolean] true if path exists
|
|
37
|
+
def key?(*keys)
|
|
38
|
+
!get(*keys).nil?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Convert to hash
|
|
42
|
+
# @return [Hash] Configuration data
|
|
43
|
+
def to_h
|
|
44
|
+
data.dup
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Get all keys at root level
|
|
48
|
+
# @return [Array<String>] Root level keys
|
|
49
|
+
def keys
|
|
50
|
+
data.keys
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Check if configuration is empty
|
|
54
|
+
# @return [Boolean] true if no configuration data
|
|
55
|
+
def empty?
|
|
56
|
+
data.empty?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Iterate over root level key-value pairs
|
|
60
|
+
def each(&block)
|
|
61
|
+
data.each(&block)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Create new config with additional data merged in
|
|
65
|
+
# @param other_data [Hash] Data to merge
|
|
66
|
+
# @return [Config] New configuration instance
|
|
67
|
+
def merge(other_data)
|
|
68
|
+
merged_data = Atoms::DeepMerger.merge(
|
|
69
|
+
data,
|
|
70
|
+
other_data,
|
|
71
|
+
array_strategy: merge_strategy
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
self.class.new(
|
|
75
|
+
merged_data,
|
|
76
|
+
source: "#{source}+merged",
|
|
77
|
+
merge_strategy: merge_strategy
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Alias for backward compatibility
|
|
82
|
+
alias_method :with, :merge
|
|
83
|
+
|
|
84
|
+
# Factory method to wrap a hash and merge additional data, returning a hash
|
|
85
|
+
# Provides a convenient one-liner for the common pattern:
|
|
86
|
+
# Config.new(base, source: "...").merge(overrides).to_h
|
|
87
|
+
#
|
|
88
|
+
# @param base [Hash, nil] Base configuration data (nil coerced to empty hash)
|
|
89
|
+
# @param overrides [Hash, nil] Data to merge on top of base (default: {}, nil coerced to empty hash)
|
|
90
|
+
# @param source [String] Source identifier for debugging (default: "wrap")
|
|
91
|
+
# @param merge_strategy [Symbol] How to merge arrays (default: :replace)
|
|
92
|
+
# @return [Hash] Merged configuration as a plain hash
|
|
93
|
+
#
|
|
94
|
+
# @example Single hash wrapping
|
|
95
|
+
# Config.wrap(defaults)
|
|
96
|
+
# # => { "key" => "default_value" }
|
|
97
|
+
#
|
|
98
|
+
# @example Merge two hashes
|
|
99
|
+
# Config.wrap(defaults, overrides)
|
|
100
|
+
# # => { "key" => "override_value", "other" => "default" }
|
|
101
|
+
#
|
|
102
|
+
# @example With options
|
|
103
|
+
# Config.wrap(defaults, overrides, source: "git_config", merge_strategy: :union)
|
|
104
|
+
#
|
|
105
|
+
# @example Handling nil inputs (type coercion)
|
|
106
|
+
# Config.wrap(nil) # => {}
|
|
107
|
+
# Config.wrap({}, nil) # => {}
|
|
108
|
+
# Config.wrap(nil, nil) # => {}
|
|
109
|
+
#
|
|
110
|
+
def self.wrap(base, overrides = {}, source: "wrap", merge_strategy: :replace)
|
|
111
|
+
# Type coercion: ensure base and overrides are hashes to prevent unexpected behavior
|
|
112
|
+
base_hash = base.is_a?(Hash) ? base : {}
|
|
113
|
+
overrides_hash = overrides.is_a?(Hash) ? overrides : {}
|
|
114
|
+
|
|
115
|
+
new(base_hash, source: source, merge_strategy: merge_strategy)
|
|
116
|
+
.merge(overrides_hash)
|
|
117
|
+
.to_h
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def ==(other)
|
|
121
|
+
other.is_a?(self.class) &&
|
|
122
|
+
other.data == data &&
|
|
123
|
+
other.source == source &&
|
|
124
|
+
other.merge_strategy == merge_strategy
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def inspect
|
|
128
|
+
"#<#{self.class.name} source=#{source.inspect} keys=#{keys.inspect}>"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Support
|
|
5
|
+
module Config
|
|
6
|
+
module Models
|
|
7
|
+
# ConfigGroup represents a set of files sharing the same effective config
|
|
8
|
+
class ConfigGroup
|
|
9
|
+
# Default scope name when no path rule matches and no distributed config is found
|
|
10
|
+
DEFAULT_SCOPE_NAME = "project default"
|
|
11
|
+
|
|
12
|
+
# rule_config: Original path rule config (before cascade merge), used for grouping
|
|
13
|
+
# - When a path rule matches, this contains only the rule's own overrides
|
|
14
|
+
# - nil for distributed config matches or project default
|
|
15
|
+
# - Separates grouping (use rule_config) from message generation (use config)
|
|
16
|
+
attr_reader :name, :source, :config, :rule_config, :files
|
|
17
|
+
|
|
18
|
+
def initialize(name:, source:, config:, files: [], rule_config: nil)
|
|
19
|
+
@name = name
|
|
20
|
+
@source = source
|
|
21
|
+
@config = config || {}
|
|
22
|
+
@rule_config = rule_config
|
|
23
|
+
@files = Array(files)
|
|
24
|
+
freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def add_file(file)
|
|
28
|
+
self.class.new(
|
|
29
|
+
name: name,
|
|
30
|
+
source: source,
|
|
31
|
+
config: config,
|
|
32
|
+
rule_config: rule_config,
|
|
33
|
+
files: files + [file]
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def file_count
|
|
38
|
+
files.length
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def ==(other)
|
|
42
|
+
other.is_a?(self.class) &&
|
|
43
|
+
other.name == name &&
|
|
44
|
+
other.source == source &&
|
|
45
|
+
other.config == config &&
|
|
46
|
+
other.rule_config == rule_config &&
|
|
47
|
+
other.files == files
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def inspect
|
|
51
|
+
"#<#{self.class.name} name=#{name.inspect} files=#{files.length}>"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Support
|
|
5
|
+
module Config
|
|
6
|
+
module Molecules
|
|
7
|
+
# Find configuration files in cascade paths
|
|
8
|
+
class ConfigFinder
|
|
9
|
+
# Common config file patterns
|
|
10
|
+
DEFAULT_FILE_PATTERNS = %w[
|
|
11
|
+
settings.yml
|
|
12
|
+
settings.yaml
|
|
13
|
+
config.yml
|
|
14
|
+
config.yaml
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
attr_reader :config_dir, :defaults_dir, :gem_path, :file_patterns, :start_path
|
|
18
|
+
|
|
19
|
+
# Initialize finder with configurable paths
|
|
20
|
+
# @param config_dir [String] User config folder name (default: ".ace")
|
|
21
|
+
# @param defaults_dir [String] Gem defaults folder name (default: ".ace-defaults")
|
|
22
|
+
# @param gem_path [String, nil] Gem root path for defaults
|
|
23
|
+
# @param file_patterns [Array<String>] File patterns to look for
|
|
24
|
+
# @param use_traversal [Boolean] Whether to use directory traversal (default: true)
|
|
25
|
+
# @param start_path [String, nil] Starting path for traversal (default: Dir.pwd)
|
|
26
|
+
def initialize(
|
|
27
|
+
config_dir: ".ace",
|
|
28
|
+
defaults_dir: ".ace-defaults",
|
|
29
|
+
gem_path: nil,
|
|
30
|
+
file_patterns: DEFAULT_FILE_PATTERNS,
|
|
31
|
+
use_traversal: true,
|
|
32
|
+
start_path: nil
|
|
33
|
+
)
|
|
34
|
+
@config_dir = config_dir
|
|
35
|
+
@defaults_dir = defaults_dir
|
|
36
|
+
@gem_path = gem_path
|
|
37
|
+
@file_patterns = file_patterns
|
|
38
|
+
@use_traversal = use_traversal
|
|
39
|
+
@start_path = start_path ? Ace::Support::Fs::Atoms::PathExpander.expand(start_path) : Dir.pwd
|
|
40
|
+
@search_paths = build_search_paths
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Find all config files in cascade order
|
|
44
|
+
# @return [Array<Models::CascadePath>] Found config paths
|
|
45
|
+
def find_all
|
|
46
|
+
paths = []
|
|
47
|
+
|
|
48
|
+
@search_paths.each_with_index do |base_path, index|
|
|
49
|
+
priority = index * 10 # Lower index = higher priority
|
|
50
|
+
|
|
51
|
+
@file_patterns.each do |pattern|
|
|
52
|
+
found = find_in_path(base_path, pattern, priority)
|
|
53
|
+
paths.concat(found)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Add gem defaults with lowest priority
|
|
58
|
+
gem_config = find_gem_defaults
|
|
59
|
+
paths.concat(gem_config) if gem_config
|
|
60
|
+
|
|
61
|
+
paths.sort
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Find first existing config file
|
|
65
|
+
# @return [Models::CascadePath, nil] First found config path
|
|
66
|
+
def find_first
|
|
67
|
+
find_all.find(&:exists)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Find configs by type
|
|
71
|
+
# @param type [Symbol] Type to filter (:local, :home, :gem)
|
|
72
|
+
# @return [Array<Models::CascadePath>] Configs of given type
|
|
73
|
+
def find_by_type(type)
|
|
74
|
+
find_all.select { |path| path.type == type }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Find a specific config file using the cascade
|
|
78
|
+
# @param filename [String] Specific filename to find
|
|
79
|
+
# @return [String, nil] Path to the first found config file
|
|
80
|
+
def find_file(filename)
|
|
81
|
+
# Use pre-built search_paths (respects @use_traversal setting)
|
|
82
|
+
@search_paths.each do |dir|
|
|
83
|
+
file_path = File.join(dir, filename)
|
|
84
|
+
return file_path if File.exist?(file_path)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Check gem defaults if available
|
|
88
|
+
if @gem_path
|
|
89
|
+
gem_default_path = File.join(@gem_path, @defaults_dir, filename)
|
|
90
|
+
return gem_default_path if File.exist?(gem_default_path)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Find all instances of a config file in the cascade
|
|
97
|
+
# @param filename [String] Specific filename to find
|
|
98
|
+
# @return [Array<String>] All found file paths in cascade order
|
|
99
|
+
def find_all_files(filename)
|
|
100
|
+
files = []
|
|
101
|
+
|
|
102
|
+
# Use pre-built search_paths (respects @use_traversal setting)
|
|
103
|
+
@search_paths.each do |dir|
|
|
104
|
+
file_path = File.join(dir, filename)
|
|
105
|
+
files << file_path if File.exist?(file_path)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Check gem defaults if available
|
|
109
|
+
if @gem_path
|
|
110
|
+
gem_default_path = File.join(@gem_path, @defaults_dir, filename)
|
|
111
|
+
files << gem_default_path if File.exist?(gem_default_path)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
files
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Get the search paths being used
|
|
118
|
+
# @return [Array<String>] Ordered list of search paths
|
|
119
|
+
attr_reader :search_paths
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
# Build search paths using directory traversal
|
|
124
|
+
# @return [Array<String>] Expanded search paths
|
|
125
|
+
def build_search_paths
|
|
126
|
+
if @use_traversal
|
|
127
|
+
traverser = Ace::Support::Fs::Molecules::DirectoryTraverser.new(
|
|
128
|
+
config_dir: @config_dir,
|
|
129
|
+
start_path: @start_path
|
|
130
|
+
)
|
|
131
|
+
paths = traverser.find_config_directories
|
|
132
|
+
|
|
133
|
+
# Add home directory if not already included
|
|
134
|
+
home_config = File.expand_path("~/#{@config_dir}")
|
|
135
|
+
paths << home_config unless paths.include?(home_config)
|
|
136
|
+
|
|
137
|
+
paths
|
|
138
|
+
else
|
|
139
|
+
# Simple default paths
|
|
140
|
+
[
|
|
141
|
+
File.join(@start_path, @config_dir),
|
|
142
|
+
File.expand_path("~/#{@config_dir}")
|
|
143
|
+
]
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Find config files in a specific path with pattern
|
|
148
|
+
# @param base_path [String] Base path to search
|
|
149
|
+
# @param pattern [String] File pattern
|
|
150
|
+
# @param base_priority [Integer] Base priority for found files
|
|
151
|
+
# @return [Array<Models::CascadePath>] Found paths
|
|
152
|
+
def find_in_path(base_path, pattern, base_priority)
|
|
153
|
+
paths = []
|
|
154
|
+
full_pattern = File.join(base_path, pattern)
|
|
155
|
+
|
|
156
|
+
# Determine type based on path
|
|
157
|
+
type = determine_type(base_path)
|
|
158
|
+
|
|
159
|
+
Dir.glob(full_pattern).each_with_index do |file, index|
|
|
160
|
+
next unless File.file?(file)
|
|
161
|
+
|
|
162
|
+
paths << Models::CascadePath.new(
|
|
163
|
+
path: file,
|
|
164
|
+
priority: base_priority + index,
|
|
165
|
+
exists: true,
|
|
166
|
+
type: type
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# If no files found but we're looking for a specific file, add as missing
|
|
171
|
+
if paths.empty? && !pattern.include?("*")
|
|
172
|
+
file_path = File.join(base_path, pattern)
|
|
173
|
+
paths << Models::CascadePath.new(
|
|
174
|
+
path: file_path,
|
|
175
|
+
priority: base_priority,
|
|
176
|
+
exists: false,
|
|
177
|
+
type: type
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
paths
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Find gem's default configs
|
|
185
|
+
# @return [Array<Models::CascadePath>] Gem default paths
|
|
186
|
+
def find_gem_defaults
|
|
187
|
+
return nil unless @gem_path
|
|
188
|
+
|
|
189
|
+
defaults_path = File.join(@gem_path, @defaults_dir)
|
|
190
|
+
return nil unless Dir.exist?(defaults_path)
|
|
191
|
+
|
|
192
|
+
paths = []
|
|
193
|
+
|
|
194
|
+
@file_patterns.each_with_index do |pattern, index|
|
|
195
|
+
Dir.glob(File.join(defaults_path, pattern)).each do |file|
|
|
196
|
+
next unless File.file?(file)
|
|
197
|
+
|
|
198
|
+
paths << Models::CascadePath.new(
|
|
199
|
+
path: file,
|
|
200
|
+
priority: 2000 + index, # Very low priority for gem defaults
|
|
201
|
+
exists: true,
|
|
202
|
+
type: :gem
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
paths.empty? ? nil : paths
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Determine config type from path
|
|
211
|
+
# @param path [String] Path to check
|
|
212
|
+
# @return [Symbol] Config type
|
|
213
|
+
def determine_type(path)
|
|
214
|
+
expanded = Ace::Support::Fs::Atoms::PathExpander.expand(path)
|
|
215
|
+
home = Ace::Support::Fs::Atoms::PathExpander.expand("~")
|
|
216
|
+
|
|
217
|
+
# Use @start_path (stable) instead of Dir.pwd (mutable)
|
|
218
|
+
if expanded.start_with?(@start_path)
|
|
219
|
+
:local
|
|
220
|
+
elsif expanded.start_with?(home)
|
|
221
|
+
:home
|
|
222
|
+
else
|
|
223
|
+
:gem
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|