htm 0.0.30 → 0.0.31

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: 5765fbc2b92d89be3f2bae2f53b4353a76343efd3d092456138bea73ac7803d9
4
- data.tar.gz: e594cc16f789745267ce527dcc182fbd533b7484c36e09887f702007388803a3
3
+ metadata.gz: 48e67477eb98226aeae3ecfbf88e7784469631f08b635b343fc426e3cff21fd7
4
+ data.tar.gz: cbff8e499ac3a382f3e1310573a54535e96fe495172aab6d5df8ad29369cf1ff
5
5
  SHA512:
6
- metadata.gz: 78b4b7e226b9911b429e7e0d8735dd0d50d1e56abf7e9e5f494ab50321e33e574605d6a3991d4f0ce18a95ae6dd8ee7928004e3d455730aa388ee2515ffc53c3
7
- data.tar.gz: c19a0c15d79342d08e406724724cf64afc68c75ea26f1bf332b4a9b661f3ebe9d50e3f9dbf41db780c0542d4cd7a77ac0bb0da9bf45b5ea7d171def21b4ffc4a
6
+ metadata.gz: b7eb33fc51efcdb6ae28bec86897c57fb15604db0bd7a0648bcaed45506161815003c13d85dc5ba12365fc543b6900d088229242ee22d707717f97a7d60aa8c4
7
+ data.tar.gz: 51b0af166ba19f5f5c9ed633711fc611f83c75b5b769efea6f0ac80384d19a0dec003352668f1d16f4608770541f819c8700809ddad46c791c973016ebe4cb4d
@@ -49,7 +49,7 @@ defaults:
49
49
  # Access: HTM.config.embedding.provider, HTM.config.embedding.model, etc.
50
50
  # ---------------------------------------------------------------------------
51
51
  embedding:
52
- provider: ollama
52
+ provider: :ollama
53
53
  model: nomic-embed-text:latest
54
54
  dimensions: 768
55
55
  timeout: 120
@@ -65,7 +65,7 @@ defaults:
65
65
  # %{taxonomy_context} - existing taxonomy info or new taxonomy message
66
66
  # ---------------------------------------------------------------------------
67
67
  tag:
68
- provider: ollama
68
+ provider: :ollama
69
69
  model: gemma3:latest
70
70
  timeout: 180
71
71
  max_depth: 4
@@ -110,7 +110,7 @@ defaults:
110
110
  # %{text} - the content to extract propositions from
111
111
  # ---------------------------------------------------------------------------
112
112
  proposition:
113
- provider: ollama
113
+ provider: :ollama
114
114
  model: gemma3:latest
115
115
  timeout: 180
116
116
  enabled: true
@@ -164,11 +164,12 @@ defaults:
164
164
 
165
165
  # ---------------------------------------------------------------------------
166
166
  # Chunking Configuration (for file loading)
167
- # Access: HTM.config.chunking.size, HTM.config.chunking.overlap
167
+ # Access: HTM.config.chunking.chunk_size, HTM.config.chunking.chunk_overlap
168
+ # Note: Using chunk_size/chunk_overlap to avoid collision with Enumerable#size
168
169
  # ---------------------------------------------------------------------------
169
170
  chunking:
170
- size: 1024
171
- overlap: 64
171
+ chunk_size: 1024
172
+ chunk_overlap: 64
172
173
 
173
174
  # ---------------------------------------------------------------------------
174
175
  # Circuit Breaker Configuration
@@ -195,16 +196,16 @@ defaults:
195
196
  # Access: HTM.config.job.backend
196
197
  # ---------------------------------------------------------------------------
197
198
  job:
198
- backend: fiber
199
+ backend: :fiber
199
200
 
200
201
  # ---------------------------------------------------------------------------
201
202
  # General Settings
202
203
  # Access: HTM.config.week_start, HTM.config.connection_timeout, etc.
203
204
  # ---------------------------------------------------------------------------
204
- week_start: sunday
205
+ week_start: :sunday
205
206
  connection_timeout: 60
206
207
  telemetry_enabled: false
207
- log_level: info
208
+ log_level: :info
208
209
 
209
210
  # ---------------------------------------------------------------------------
210
211
  # Provider Credentials
@@ -250,7 +251,7 @@ defaults:
250
251
  development:
251
252
  database:
252
253
  name: htm_development
253
- log_level: debug
254
+ log_level: :debug
254
255
 
255
256
  # =============================================================================
256
257
  # Test Environment Overrides
@@ -259,8 +260,8 @@ test:
259
260
  database:
260
261
  name: htm_test
261
262
  job:
262
- backend: inline
263
- log_level: warn
263
+ backend: :inline
264
+ log_level: :warn
264
265
  telemetry_enabled: false
265
266
 
266
267
  # =============================================================================
@@ -270,5 +271,16 @@ production:
270
271
  database:
271
272
  pool_size: 25
272
273
  sslmode: require
273
- log_level: warn
274
+ log_level: :warn
274
275
  telemetry_enabled: true
276
+
277
+ # =============================================================================
278
+ # Examples Environment Overrides (for running example scripts)
279
+ # =============================================================================
280
+ examples:
281
+ database:
282
+ name: htm_examples
283
+ job:
284
+ backend: :inline
285
+ log_level: :info
286
+ telemetry_enabled: false
data/lib/htm/config.rb CHANGED
@@ -1,16 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'anyway_config'
3
+ require 'myway_config'
4
4
  require 'logger'
5
- require 'yaml'
6
5
 
7
6
  # Define Config class first to establish superclass
8
7
  class HTM
9
- class Config < Anyway::Config
8
+ class Config < MywayConfig::Base
10
9
  end
11
10
  end
12
11
 
13
- require_relative 'config/section'
14
12
  require_relative 'config/validator'
15
13
  require_relative 'config/database'
16
14
  require_relative 'config/builder'
@@ -61,35 +59,11 @@ class HTM
61
59
 
62
60
  config_name :htm
63
61
  env_prefix :htm
62
+ defaults_path File.expand_path('config/defaults.yml', __dir__)
64
63
 
65
- # ==========================================================================
66
- # Schema Definition (loaded from defaults.yml - single source of truth)
67
- # ==========================================================================
68
-
69
- # Path to bundled defaults file (defines both schema and default values)
70
- DEFAULTS_PATH = File.expand_path('config/defaults.yml', __dir__).freeze
71
-
72
- # Load schema from defaults.yml at class definition time
73
- begin
74
- defaults_content = File.read(DEFAULTS_PATH)
75
- raw_yaml = YAML.safe_load(
76
- defaults_content,
77
- permitted_classes: [Symbol],
78
- symbolize_names: true,
79
- aliases: true
80
- ) || {}
81
- SCHEMA = raw_yaml[:defaults] || {}
82
- rescue StandardError => e
83
- warn "HTM: Could not load schema from #{DEFAULTS_PATH}: #{e.message}"
84
- SCHEMA = {}
85
- end
86
-
87
- # Nested section attributes (defined as hashes, converted to ConfigSection)
88
- attr_config :database, :service, :embedding, :tag, :proposition,
89
- :chunking, :circuit_breaker, :relevance, :job, :providers
90
-
91
- # Top-level scalar attributes
92
- attr_config :week_start, :connection_timeout, :telemetry_enabled, :log_level
64
+ # Auto-configure attributes and coercions from defaults.yml schema
65
+ # This replaces manual attr_config and coerce_types declarations
66
+ auto_configure!
93
67
 
94
68
  # Custom environment detection: HTM_ENV > RAILS_ENV > RACK_ENV > 'development'
95
69
  class << self
@@ -102,60 +76,6 @@ class HTM
102
76
  end
103
77
  end
104
78
 
105
- # ==========================================================================
106
- # Type Coercion
107
- # ==========================================================================
108
-
109
- TO_SYMBOL = ->(v) { v.nil? ? nil : v.to_s.to_sym }
110
-
111
- # Create a coercion that merges incoming value with SCHEMA defaults for a section.
112
- # This ensures env vars like HTM_DATABASE__URL don't lose other defaults.
113
- def self.config_section_with_defaults(section_key)
114
- defaults = SCHEMA[section_key] || {}
115
- ->(v) {
116
- return v if v.is_a?(ConfigSection)
117
- incoming = v || {}
118
- # Deep merge: defaults first, then overlay incoming values
119
- merged = deep_merge_hashes(defaults.dup, incoming)
120
- ConfigSection.new(merged)
121
- }
122
- end
123
-
124
- # Deep merge helper for coercion
125
- def self.deep_merge_hashes(base, overlay)
126
- base.merge(overlay) do |_key, old_val, new_val|
127
- if old_val.is_a?(Hash) && new_val.is_a?(Hash)
128
- deep_merge_hashes(old_val, new_val)
129
- else
130
- new_val.nil? ? old_val : new_val
131
- end
132
- end
133
- end
134
-
135
- coerce_types(
136
- # Nested sections -> ConfigSection objects (with SCHEMA defaults merged)
137
- database: config_section_with_defaults(:database),
138
- service: config_section_with_defaults(:service),
139
- embedding: config_section_with_defaults(:embedding),
140
- tag: config_section_with_defaults(:tag),
141
- proposition: config_section_with_defaults(:proposition),
142
- chunking: config_section_with_defaults(:chunking),
143
- circuit_breaker: config_section_with_defaults(:circuit_breaker),
144
- relevance: config_section_with_defaults(:relevance),
145
- job: config_section_with_defaults(:job),
146
- providers: config_section_with_defaults(:providers),
147
-
148
- # Top-level symbols
149
- week_start: TO_SYMBOL,
150
- log_level: TO_SYMBOL,
151
-
152
- # Top-level integers
153
- connection_timeout: :integer,
154
-
155
- # Top-level booleans
156
- telemetry_enabled: :boolean
157
- )
158
-
159
79
  # ==========================================================================
160
80
  # Validation
161
81
  # ==========================================================================
@@ -256,11 +176,11 @@ class HTM
256
176
 
257
177
  # Chunking convenience accessors
258
178
  def chunk_size
259
- chunking.size.to_i
179
+ chunking.chunk_size.to_i
260
180
  end
261
181
 
262
182
  def chunk_overlap
263
- chunking.overlap.to_i
183
+ chunking.chunk_overlap.to_i
264
184
  end
265
185
 
266
186
  # Circuit breaker convenience accessors
@@ -375,17 +295,8 @@ class HTM
375
295
  # Environment Helpers
376
296
  # ==========================================================================
377
297
 
378
- def test?
379
- self.class.env == 'test'
380
- end
381
-
382
- def development?
383
- self.class.env == 'development'
384
- end
385
-
386
- def production?
387
- self.class.env == 'production'
388
- end
298
+ # Note: test?, development?, production? are auto-generated by MywayConfig::Base
299
+ # based on environment keys in defaults.yml
389
300
 
390
301
  def environment
391
302
  self.class.env
@@ -396,17 +307,16 @@ class HTM
396
307
  # ==========================================================================
397
308
 
398
309
  # Returns list of valid environment names from bundled defaults
310
+ # Inherited from MywayConfig::Base - delegates to DefaultsLoader
399
311
  #
400
312
  # @return [Array<Symbol>] valid environment names (e.g., [:development, :production, :test])
401
- def self.valid_environments
402
- HTM::Loaders::DefaultsLoader.valid_environments
403
- end
313
+ # Note: valid_environments is inherited from MywayConfig::Base
404
314
 
405
315
  # Check if current environment is valid (defined in config)
406
316
  #
407
317
  # @return [Boolean] true if environment has a config section
408
318
  def self.valid_environment?
409
- HTM::Loaders::DefaultsLoader.valid_environment?(env)
319
+ MywayConfig::Loaders::DefaultsLoader.valid_environment?(config_name, env)
410
320
  end
411
321
 
412
322
  # Validate that the current environment is configured
@@ -415,7 +325,7 @@ class HTM
415
325
  # @return [true] if environment is valid
416
326
  def self.validate_environment!
417
327
  current = env
418
- return true if HTM::Loaders::DefaultsLoader.valid_environment?(current)
328
+ return true if valid_environment?
419
329
 
420
330
  valid = valid_environments.map(&:to_s).join(', ')
421
331
  raise HTM::ConfigurationError,
@@ -438,7 +348,7 @@ class HTM
438
348
  # ==========================================================================
439
349
 
440
350
  def self.xdg_config_paths
441
- HTM::Loaders::XdgConfigLoader.config_paths
351
+ MywayConfig::Loaders::XdgConfigLoader.config_paths(config_name)
442
352
  end
443
353
 
444
354
  def self.xdg_config_file
@@ -452,7 +362,7 @@ class HTM
452
362
  end
453
363
 
454
364
  def self.active_xdg_config_file
455
- HTM::Loaders::XdgConfigLoader.find_config_file('htm')
365
+ MywayConfig::Loaders::XdgConfigLoader.find_config_file(config_name)
456
366
  end
457
367
 
458
368
  # ==========================================================================
@@ -544,10 +454,11 @@ class HTM
544
454
 
545
455
  def coerce_nested_types
546
456
  # Ensure nested provider sections are ConfigSections
547
- if providers.is_a?(ConfigSection)
457
+ # myway_config handles top-level sections, but we need to handle nested ones
458
+ if providers.is_a?(MywayConfig::ConfigSection)
548
459
  %i[openai anthropic gemini azure ollama huggingface openrouter bedrock deepseek].each do |provider|
549
460
  value = providers[provider]
550
- providers[provider] = ConfigSection.new(value) if value.is_a?(Hash)
461
+ providers[provider] = MywayConfig::ConfigSection.new(value) if value.is_a?(Hash)
551
462
  end
552
463
  end
553
464
 
@@ -586,7 +497,5 @@ class HTM
586
497
  end
587
498
  end
588
499
 
589
- # Register custom loaders after Config class is defined
590
- # Order matters: defaults (lowest priority) -> XDG -> project config -> ENV (highest)
591
- require_relative 'loaders/defaults_loader'
592
- require_relative 'loaders/xdg_config_loader'
500
+ # myway_config provides DefaultsLoader and XdgConfigLoader automatically
501
+ # Loaders are registered when MywayConfig.setup! is called (happens on require)
data/lib/htm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class HTM
4
- VERSION = '0.0.30'
4
+ VERSION = '0.0.31'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: htm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.30
4
+ version: 0.0.31
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
@@ -150,19 +150,19 @@ dependencies:
150
150
  - !ruby/object:Gem::Version
151
151
  version: '0'
152
152
  - !ruby/object:Gem::Dependency
153
- name: anyway_config
153
+ name: myway_config
154
154
  requirement: !ruby/object:Gem::Requirement
155
155
  requirements:
156
156
  - - ">="
157
157
  - !ruby/object:Gem::Version
158
- version: '2.6'
158
+ version: 0.1.2
159
159
  type: :runtime
160
160
  prerelease: false
161
161
  version_requirements: !ruby/object:Gem::Requirement
162
162
  requirements:
163
163
  - - ">="
164
164
  - !ruby/object:Gem::Version
165
- version: '2.6'
165
+ version: 0.1.2
166
166
  - !ruby/object:Gem::Dependency
167
167
  name: simple_flow
168
168
  requirement: !ruby/object:Gem::Requirement
@@ -617,7 +617,6 @@ files:
617
617
  - lib/htm/config/builder.rb
618
618
  - lib/htm/config/database.rb
619
619
  - lib/htm/config/defaults.yml
620
- - lib/htm/config/section.rb
621
620
  - lib/htm/config/validator.rb
622
621
  - lib/htm/database.rb
623
622
  - lib/htm/embedding_service.rb
@@ -627,10 +626,8 @@ files:
627
626
  - lib/htm/jobs/generate_embedding_job.rb
628
627
  - lib/htm/jobs/generate_propositions_job.rb
629
628
  - lib/htm/jobs/generate_tags_job.rb
630
- - lib/htm/loaders/defaults_loader.rb
631
629
  - lib/htm/loaders/markdown_chunker.rb
632
630
  - lib/htm/loaders/markdown_loader.rb
633
- - lib/htm/loaders/xdg_config_loader.rb
634
631
  - lib/htm/long_term_memory.rb
635
632
  - lib/htm/long_term_memory/fulltext_search.rb
636
633
  - lib/htm/long_term_memory/hybrid_search.rb
@@ -1,74 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class HTM
4
- # ConfigSection provides method access to nested configuration hashes
5
- #
6
- # @example
7
- # section = ConfigSection.new(host: 'localhost', port: 5432)
8
- # section.host # => 'localhost'
9
- # section.port # => 5432
10
- #
11
- class ConfigSection
12
- def initialize(hash = {})
13
- @data = {}
14
- (hash || {}).each do |key, value|
15
- @data[key.to_sym] = value.is_a?(Hash) ? ConfigSection.new(value) : value
16
- end
17
- end
18
-
19
- def method_missing(method, *args, &block)
20
- key = method.to_s
21
- if key.end_with?('=')
22
- @data[key.chomp('=').to_sym] = args.first
23
- elsif @data.key?(method)
24
- @data[method]
25
- else
26
- nil
27
- end
28
- end
29
-
30
- def respond_to_missing?(method, include_private = false)
31
- key = method.to_s.chomp('=').to_sym
32
- @data.key?(key) || super
33
- end
34
-
35
- def to_h
36
- @data.transform_values do |v|
37
- v.is_a?(ConfigSection) ? v.to_h : v
38
- end
39
- end
40
-
41
- def [](key)
42
- @data[key.to_sym]
43
- end
44
-
45
- def []=(key, value)
46
- @data[key.to_sym] = value
47
- end
48
-
49
- def merge(other)
50
- other_hash = other.is_a?(ConfigSection) ? other.to_h : other
51
- ConfigSection.new(deep_merge(to_h, other_hash || {}))
52
- end
53
-
54
- def keys
55
- @data.keys
56
- end
57
-
58
- def each(&block)
59
- @data.each(&block)
60
- end
61
-
62
- private
63
-
64
- def deep_merge(base, overlay)
65
- base.merge(overlay) do |_key, old_val, new_val|
66
- if old_val.is_a?(Hash) && new_val.is_a?(Hash)
67
- deep_merge(old_val, new_val)
68
- else
69
- new_val
70
- end
71
- end
72
- end
73
- end
74
- end
@@ -1,166 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'anyway_config'
4
- require 'yaml'
5
-
6
- class HTM
7
- module Loaders
8
- # Bundled Defaults Loader for Anyway Config
9
- #
10
- # Loads default configuration values from a YAML file bundled with the gem.
11
- # This ensures defaults are always available regardless of where HTM is installed.
12
- #
13
- # The defaults.yml file has this structure:
14
- # defaults: # Base values for all environments
15
- # database:
16
- # host: localhost
17
- # port: 5432
18
- # development: # Overrides for development
19
- # database:
20
- # name: htm_development
21
- # test: # Overrides for test
22
- # database:
23
- # name: htm_test
24
- # production: # Overrides for production
25
- # database:
26
- # sslmode: require
27
- #
28
- # This loader deep-merges `defaults` with the current environment's overrides.
29
- #
30
- # This loader runs at LOWEST priority (before XDG), so all other sources
31
- # can override these bundled defaults:
32
- # 1. Bundled defaults (this loader)
33
- # 2. XDG user config (~/.config/htm/htm.yml)
34
- # 3. Project config (./config/htm.yml)
35
- # 4. Local overrides (./config/htm.local.yml)
36
- # 5. Environment variables (HTM_*)
37
- # 6. Programmatic (configure block)
38
- #
39
- class DefaultsLoader < Anyway::Loaders::Base
40
- DEFAULTS_PATH = File.expand_path('../config/defaults.yml', __dir__).freeze
41
-
42
- class << self
43
- # Returns the path to the bundled defaults file
44
- #
45
- # @return [String] path to defaults.yml
46
- def defaults_path
47
- DEFAULTS_PATH
48
- end
49
-
50
- # Check if defaults file exists
51
- #
52
- # @return [Boolean]
53
- def defaults_exist?
54
- File.exist?(DEFAULTS_PATH)
55
- end
56
-
57
- # Load and parse the raw YAML content
58
- #
59
- # @return [Hash] parsed YAML with symbolized keys
60
- def load_raw_yaml
61
- return {} unless defaults_exist?
62
-
63
- content = File.read(defaults_path)
64
- YAML.safe_load(
65
- content,
66
- permitted_classes: [Symbol],
67
- symbolize_names: true,
68
- aliases: true
69
- ) || {}
70
- rescue Psych::SyntaxError => e
71
- warn "HTM: Failed to parse bundled defaults #{defaults_path}: #{e.message}"
72
- {}
73
- end
74
-
75
- # Extract the schema (attribute names) from the defaults section
76
- #
77
- # @return [Hash] the defaults section containing all attribute definitions
78
- def schema
79
- raw = load_raw_yaml
80
- raw[:defaults] || {}
81
- end
82
-
83
- # Returns valid environment names from the config file
84
- #
85
- # Valid environments are top-level keys in defaults.yml excluding 'defaults'.
86
- # For example, if defaults.yml has keys: defaults, development, test, production
87
- # this returns [:development, :test, :production]
88
- #
89
- # @return [Array<Symbol>] list of valid environment names
90
- def valid_environments
91
- raw = load_raw_yaml
92
- raw.keys.reject { |k| k == :defaults }.sort
93
- end
94
-
95
- # Check if a given environment name is valid
96
- #
97
- # @param env [String, Symbol] environment name to check
98
- # @return [Boolean] true if environment is valid
99
- def valid_environment?(env)
100
- return false if env.nil? || env.to_s.empty?
101
- return false if env.to_s == 'defaults'
102
-
103
- valid_environments.include?(env.to_sym)
104
- end
105
- end
106
-
107
- def call(name:, **_options)
108
- return {} unless self.class.defaults_exist?
109
-
110
- trace!(:bundled_defaults, path: self.class.defaults_path) do
111
- load_and_merge_for_environment
112
- end
113
- end
114
-
115
- private
116
-
117
- # Load defaults and deep merge with environment-specific overrides
118
- #
119
- # @return [Hash] merged configuration for current environment
120
- def load_and_merge_for_environment
121
- raw = self.class.load_raw_yaml
122
- return {} if raw.empty?
123
-
124
- # Start with the defaults section
125
- defaults = raw[:defaults] || {}
126
-
127
- # Deep merge with environment-specific overrides
128
- env = current_environment
129
- env_overrides = raw[env.to_sym] || {}
130
-
131
- deep_merge(defaults, env_overrides)
132
- end
133
-
134
- # Deep merge two hashes, with overlay taking precedence
135
- #
136
- # @param base [Hash] base configuration
137
- # @param overlay [Hash] overlay configuration (takes precedence)
138
- # @return [Hash] merged result
139
- def deep_merge(base, overlay)
140
- base.merge(overlay) do |_key, old_val, new_val|
141
- if old_val.is_a?(Hash) && new_val.is_a?(Hash)
142
- deep_merge(old_val, new_val)
143
- else
144
- new_val
145
- end
146
- end
147
- end
148
-
149
- # Determine the current environment
150
- #
151
- # Priority: HTM_ENV > RAILS_ENV > RACK_ENV > 'development'
152
- #
153
- # @return [String] current environment name
154
- def current_environment
155
- ENV['HTM_ENV'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
156
- end
157
- end
158
- end
159
- end
160
-
161
- # Register the defaults loader at LOWEST priority (before :yml loader)
162
- # This ensures bundled defaults are overridden by all other sources:
163
- # - XDG user config (registered after this, also before :yml)
164
- # - Project config (:yml loader)
165
- # - Environment variables (:env loader)
166
- Anyway.loaders.insert_before :yml, :bundled_defaults, HTM::Loaders::DefaultsLoader
@@ -1,116 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'anyway_config'
4
- require 'yaml'
5
-
6
- class HTM
7
- module Loaders
8
- # XDG Base Directory Specification loader for Anyway Config
9
- #
10
- # Loads configuration from XDG-compliant paths:
11
- # 1. $XDG_CONFIG_HOME/htm/htm.yml (if XDG_CONFIG_HOME is set)
12
- # 2. ~/.config/htm/htm.yml (XDG default fallback)
13
- #
14
- # On macOS, also checks:
15
- # 3. ~/Library/Application Support/htm/htm.yml
16
- #
17
- # This loader runs BEFORE the project-local config loader,
18
- # so project configs take precedence over user-global configs.
19
- #
20
- # @example XDG config file location
21
- # ~/.config/htm/htm.yml
22
- #
23
- # @example Custom XDG_CONFIG_HOME
24
- # export XDG_CONFIG_HOME=/my/config
25
- # # Looks for /my/config/htm/htm.yml
26
- #
27
- class XdgConfigLoader < Anyway::Loaders::Base
28
- class << self
29
- # Returns all XDG config paths to check, in order of priority (lowest first)
30
- #
31
- # Per XDG spec: If $XDG_CONFIG_HOME is set, use it; otherwise use ~/.config
32
- #
33
- # @return [Array<String>] list of potential config file paths
34
- def config_paths
35
- paths = []
36
-
37
- # macOS Application Support (lowest priority, only when XDG_CONFIG_HOME is not set)
38
- if macos? && (!ENV['XDG_CONFIG_HOME'] || ENV['XDG_CONFIG_HOME'].empty?)
39
- macos_path = File.expand_path('~/Library/Application Support/htm')
40
- paths << macos_path if Dir.exist?(File.dirname(macos_path))
41
- end
42
-
43
- # XDG_CONFIG_HOME takes precedence over default
44
- if ENV['XDG_CONFIG_HOME'] && !ENV['XDG_CONFIG_HOME'].empty?
45
- paths << File.join(ENV['XDG_CONFIG_HOME'], 'htm')
46
- else
47
- # XDG default: ~/.config/htm (only when XDG_CONFIG_HOME is not set)
48
- paths << File.expand_path('~/.config/htm')
49
- end
50
-
51
- paths
52
- end
53
-
54
- # Find the first existing config file
55
- #
56
- # @param name [String] config name (e.g., 'htm')
57
- # @return [String, nil] path to config file or nil if not found
58
- def find_config_file(name)
59
- config_paths.reverse_each do |dir|
60
- file = File.join(dir, "#{name}.yml")
61
- return file if File.exist?(file)
62
- end
63
- nil
64
- end
65
-
66
- private
67
-
68
- def macos?
69
- RUBY_PLATFORM.include?('darwin')
70
- end
71
- end
72
-
73
- def call(name:, **_options)
74
- config_file = self.class.find_config_file(name)
75
- return {} unless config_file
76
-
77
- trace!(:xdg, path: config_file) do
78
- load_yaml(config_file, name)
79
- end
80
- end
81
-
82
- private
83
-
84
- def load_yaml(path, name)
85
- return {} unless File.exist?(path)
86
-
87
- content = File.read(path)
88
- parsed = YAML.safe_load(content, permitted_classes: [Symbol], symbolize_names: true, aliases: true) || {}
89
-
90
- # Support environment-specific configs
91
- env = Anyway::Settings.current_environment ||
92
- ENV['HTM_ENV'] ||
93
- ENV['RAILS_ENV'] ||
94
- ENV['RACK_ENV'] ||
95
- 'development'
96
-
97
- # Check for environment key first, fall back to root level
98
- if parsed.key?(env.to_sym)
99
- parsed[env.to_sym] || {}
100
- elsif parsed.key?(env.to_s)
101
- parsed[env.to_s] || {}
102
- else
103
- # No environment key, treat as flat config
104
- parsed
105
- end
106
- rescue Psych::SyntaxError => e
107
- warn "HTM: Failed to parse XDG config #{path}: #{e.message}"
108
- {}
109
- end
110
- end
111
- end
112
- end
113
-
114
- # Register the XDG loader with Anyway Config
115
- # Insert before :yml so project-local config takes precedence
116
- Anyway.loaders.insert_before :yml, :xdg, HTM::Loaders::XdgConfigLoader