ast-merge 3.1.0 → 4.0.1

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.
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "merge_gem_registry"
4
+
3
5
  # Ast::Merge RSpec Dependency Tags
4
6
  #
5
7
  # This module provides dependency detection helpers for conditional test execution
6
- # in the ast-merge gem family. It extends tree_haver's dependency tags with
7
- # merge-gem-specific checks.
8
+ # in the ast-merge gem family. It uses MergeGemRegistry for dynamic merge gem detection.
8
9
  #
9
10
  # @example Loading in spec_helper.rb
10
11
  # require "ast/merge/rspec/dependency_tags"
@@ -18,50 +19,26 @@
18
19
  # # This test only runs when prism-merge is available
19
20
  # end
20
21
  #
21
- # == Available Tags
22
- #
23
- # === Merge Gem Tags (run when dependency IS available)
24
- #
25
- # [:markly_merge]
26
- # markly-merge gem is available and functional.
27
- #
28
- # [:commonmarker_merge]
29
- # commonmarker-merge gem is available and functional.
30
- #
31
- # [:markdown_merge]
32
- # markdown-merge gem is available and functional.
33
- #
34
- # [:prism_merge]
35
- # prism-merge gem is available and functional.
36
- #
37
- # [:json_merge]
38
- # json-merge gem is available and functional.
22
+ # == Dynamic Tag Registration
39
23
  #
40
- # [:jsonc_merge]
41
- # jsonc-merge gem is available and functional.
24
+ # Merge gems register themselves with MergeGemRegistry, which automatically:
25
+ # - Defines `*_available?` methods on DependencyTags
26
+ # - Configures RSpec exclusion filters for the tag
27
+ # - Supports negated tags (`:not_*`)
42
28
  #
43
- # [:toml_merge]
44
- # toml-merge gem is available and functional.
29
+ # @example How merge gems register (in their lib file)
30
+ # Ast::Merge::RSpec::MergeGemRegistry.register(
31
+ # :markly_merge,
32
+ # require_path: "markly/merge",
33
+ # merger_class: "Markly::Merge::SmartMerger",
34
+ # test_source: "# Test\n\nParagraph",
35
+ # category: :markdown
36
+ # )
45
37
  #
46
- # [:bash_merge]
47
- # bash-merge gem is available and functional.
48
- #
49
- # [:psych_merge]
50
- # psych-merge gem is available and functional.
51
- #
52
- # [:rbs_merge]
53
- # rbs-merge gem is available and functional.
38
+ # == Built-in Composite Tags
54
39
  #
55
40
  # [:any_markdown_merge]
56
- # At least one markdown merge gem (markly-merge or commonmarker-merge) is available.
57
- #
58
- # === Negated Tags (run when dependency is NOT available)
59
- #
60
- # All positive tags have negated versions prefixed with `not_`:
61
- # - :not_markly_merge, :not_commonmarker_merge, :not_markdown_merge
62
- # - :not_prism_merge, :not_json_merge, :not_jsonc_merge
63
- # - :not_toml_merge, :not_bash_merge, :not_psych_merge, :not_rbs_merge
64
- # - :not_any_markdown_merge
41
+ # At least one markdown merge gem is available (category: :markdown).
65
42
 
66
43
  module Ast
67
44
  module Merge
@@ -70,96 +47,16 @@ module Ast
70
47
  module DependencyTags
71
48
  class << self
72
49
  # ============================================================
73
- # Merge Gem Availability
50
+ # Composite Availability Checks
74
51
  # ============================================================
75
52
 
76
- # rubocop:disable ThreadSafety/ClassInstanceVariable
77
- # Check if markly-merge is available and functional
78
- #
79
- # @return [Boolean] true if markly-merge works
80
- def markly_merge_available?
81
- return @markly_merge_available if defined?(@markly_merge_available)
82
- @markly_merge_available = merge_gem_works?("markly/merge", "Markly::Merge::SmartMerger", "# Test\n\nParagraph")
83
- end
84
-
85
- # Check if commonmarker-merge is available and functional
86
- #
87
- # @return [Boolean] true if commonmarker-merge works
88
- def commonmarker_merge_available?
89
- return @commonmarker_merge_available if defined?(@commonmarker_merge_available)
90
- @commonmarker_merge_available = merge_gem_works?("commonmarker/merge", "Commonmarker::Merge::SmartMerger", "# Test\n\nParagraph")
91
- end
92
-
93
- # Check if markdown-merge is available and functional
94
- #
95
- # @return [Boolean] true if markdown-merge works
96
- def markdown_merge_available?
97
- return @markdown_merge_available if defined?(@markdown_merge_available)
98
- @markdown_merge_available = merge_gem_works?("markdown/merge", "Markdown::Merge::SmartMerger", "# Test\n\nParagraph")
99
- end
100
-
101
- # Check if prism-merge is available and functional
102
- #
103
- # @return [Boolean] true if prism-merge works
104
- def prism_merge_available?
105
- return @prism_merge_available if defined?(@prism_merge_available)
106
- @prism_merge_available = merge_gem_works?("prism/merge", "Prism::Merge::SmartMerger", "puts 1")
107
- end
108
-
109
- # Check if json-merge is available and functional
110
- #
111
- # @return [Boolean] true if json-merge works
112
- def json_merge_available?
113
- return @json_merge_available if defined?(@json_merge_available)
114
- @json_merge_available = merge_gem_works?("json/merge", "Json::Merge::SmartMerger", '{"key": "value"}')
115
- end
116
-
117
- # Check if jsonc-merge is available and functional
118
- #
119
- # @return [Boolean] true if jsonc-merge works
120
- def jsonc_merge_available?
121
- return @jsonc_merge_available if defined?(@jsonc_merge_available)
122
- @jsonc_merge_available = merge_gem_works?("jsonc/merge", "Jsonc::Merge::SmartMerger", '{"key": "value" /* comment */}')
123
- end
124
-
125
- # Check if toml-merge is available and functional
126
- #
127
- # @return [Boolean] true if toml-merge works
128
- def toml_merge_available?
129
- return @toml_merge_available if defined?(@toml_merge_available)
130
- @toml_merge_available = merge_gem_works?("toml/merge", "Toml::Merge::SmartMerger", 'key = "value"')
131
- end
132
-
133
- # Check if bash-merge is available and functional
134
- #
135
- # @return [Boolean] true if bash-merge works
136
- def bash_merge_available?
137
- return @bash_merge_available if defined?(@bash_merge_available)
138
- @bash_merge_available = merge_gem_works?("bash/merge", "Bash::Merge::SmartMerger", "echo hello")
139
- end
140
-
141
- # Check if psych-merge is available and functional
142
- #
143
- # @return [Boolean] true if psych-merge works
144
- def psych_merge_available?
145
- return @psych_merge_available if defined?(@psych_merge_available)
146
- @psych_merge_available = merge_gem_works?("psych/merge", "Psych::Merge::SmartMerger", "key: value")
147
- end
148
-
149
- # Check if rbs-merge is available and functional
150
- #
151
- # @return [Boolean] true if rbs-merge works
152
- def rbs_merge_available?
153
- return @rbs_merge_available if defined?(@rbs_merge_available)
154
- @rbs_merge_available = merge_gem_works?("rbs/merge", "Rbs::Merge::SmartMerger", "class Foo end")
155
- end
156
- # rubocop:enable ThreadSafety/ClassInstanceVariable
157
-
158
53
  # Check if at least one markdown merge gem is available
159
54
  #
160
55
  # @return [Boolean] true if any markdown merge gem works
161
56
  def any_markdown_merge_available?
162
- markly_merge_available? || commonmarker_merge_available? || markdown_merge_available?
57
+ MergeGemRegistry.gems_by_category(:markdown).any? do |tag|
58
+ MergeGemRegistry.available?(tag)
59
+ end
163
60
  end
164
61
 
165
62
  # ============================================================
@@ -170,45 +67,16 @@ module Ast
170
67
  #
171
68
  # @return [Hash{Symbol => Boolean}] map of dependency name to availability
172
69
  def summary
173
- {
174
- markly_merge: markly_merge_available?,
175
- commonmarker_merge: commonmarker_merge_available?,
176
- markdown_merge: markdown_merge_available?,
177
- prism_merge: prism_merge_available?,
178
- json_merge: json_merge_available?,
179
- jsonc_merge: jsonc_merge_available?,
180
- toml_merge: toml_merge_available?,
181
- bash_merge: bash_merge_available?,
182
- psych_merge: psych_merge_available?,
183
- rbs_merge: rbs_merge_available?,
184
- any_markdown_merge: any_markdown_merge_available?,
185
- }
70
+ result = MergeGemRegistry.summary
71
+ result[:any_markdown_merge] = any_markdown_merge_available?
72
+ result
186
73
  end
187
74
 
188
75
  # Reset all memoized availability checks
189
76
  #
190
77
  # @return [void]
191
78
  def reset!
192
- instance_variables.each do |ivar|
193
- remove_instance_variable(ivar) if ivar.to_s.end_with?("_available")
194
- end
195
- end
196
-
197
- private
198
-
199
- # Generic helper to check if a merge gem is available and functional
200
- #
201
- # @param require_path [String] the require path for the gem
202
- # @param merger_class [String] the full class name of the SmartMerger
203
- # @param test_source [String] sample source code to test merging
204
- # @return [Boolean] true if the merger can be instantiated
205
- def merge_gem_works?(require_path, merger_class, test_source)
206
- require require_path
207
- klass = Object.const_get(merger_class)
208
- klass.new(test_source, test_source)
209
- true
210
- rescue LoadError, StandardError
211
- false
79
+ MergeGemRegistry.reset_availability!
212
80
  end
213
81
  end
214
82
  end
@@ -219,10 +87,11 @@ end
219
87
  # Configure RSpec with dependency-based exclusion filters
220
88
  RSpec.configure do |config|
221
89
  deps = Ast::Merge::RSpec::DependencyTags
90
+ registry = Ast::Merge::RSpec::MergeGemRegistry
222
91
 
223
92
  config.before(:suite) do
224
93
  # Print dependency summary if AST_MERGE_DEBUG is set
225
- if ENV["AST_MERGE_DEBUG"]
94
+ unless ENV.fetch("AST_MERGE_DEBUG", "false").casecmp?("false")
226
95
  puts "\n=== Ast::Merge Test Dependencies ==="
227
96
  deps.summary.each do |dep, available|
228
97
  status = available ? "✓ available" : "✗ not available"
@@ -233,34 +102,24 @@ RSpec.configure do |config|
233
102
  end
234
103
 
235
104
  # ============================================================
236
- # Merge Gem Tags
105
+ # Dynamic Merge Gem Tags
237
106
  # ============================================================
107
+ # Tags are configured dynamically based on what's registered in MergeGemRegistry.
108
+ # Each merge gem registers itself, and exclusion filters are set up automatically.
238
109
 
239
- config.filter_run_excluding(markly_merge: true) unless deps.markly_merge_available?
240
- config.filter_run_excluding(commonmarker_merge: true) unless deps.commonmarker_merge_available?
241
- config.filter_run_excluding(markdown_merge: true) unless deps.markdown_merge_available?
242
- config.filter_run_excluding(prism_merge: true) unless deps.prism_merge_available?
243
- config.filter_run_excluding(json_merge: true) unless deps.json_merge_available?
244
- config.filter_run_excluding(jsonc_merge: true) unless deps.jsonc_merge_available?
245
- config.filter_run_excluding(toml_merge: true) unless deps.toml_merge_available?
246
- config.filter_run_excluding(bash_merge: true) unless deps.bash_merge_available?
247
- config.filter_run_excluding(psych_merge: true) unless deps.psych_merge_available?
248
- config.filter_run_excluding(rbs_merge: true) unless deps.rbs_merge_available?
249
- config.filter_run_excluding(any_markdown_merge: true) unless deps.any_markdown_merge_available?
110
+ registry.registered_gems.each do |tag|
111
+ # Positive tag: run when gem IS available
112
+ config.filter_run_excluding(tag => true) unless registry.available?(tag)
113
+
114
+ # Negated tag: run when gem is NOT available
115
+ negated_tag = :"not_#{tag}"
116
+ config.filter_run_excluding(negated_tag => true) if registry.available?(tag)
117
+ end
250
118
 
251
119
  # ============================================================
252
- # Negated Tags (run when dependency is NOT available)
120
+ # Composite Tags
253
121
  # ============================================================
254
122
 
255
- config.filter_run_excluding(not_markly_merge: true) if deps.markly_merge_available?
256
- config.filter_run_excluding(not_commonmarker_merge: true) if deps.commonmarker_merge_available?
257
- config.filter_run_excluding(not_markdown_merge: true) if deps.markdown_merge_available?
258
- config.filter_run_excluding(not_prism_merge: true) if deps.prism_merge_available?
259
- config.filter_run_excluding(not_json_merge: true) if deps.json_merge_available?
260
- config.filter_run_excluding(not_jsonc_merge: true) if deps.jsonc_merge_available?
261
- config.filter_run_excluding(not_toml_merge: true) if deps.toml_merge_available?
262
- config.filter_run_excluding(not_bash_merge: true) if deps.bash_merge_available?
263
- config.filter_run_excluding(not_psych_merge: true) if deps.psych_merge_available?
264
- config.filter_run_excluding(not_rbs_merge: true) if deps.rbs_merge_available?
123
+ config.filter_run_excluding(any_markdown_merge: true) unless deps.any_markdown_merge_available?
265
124
  config.filter_run_excluding(not_any_markdown_merge: true) if deps.any_markdown_merge_available?
266
125
  end
@@ -0,0 +1,382 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ast
4
+ module Merge
5
+ module RSpec
6
+ # Registry for merge gem dependency tag availability checkers
7
+ #
8
+ # This module allows merge gems (like markly-merge, prism-merge, json-merge)
9
+ # to register their availability checker for RSpec dependency tags without
10
+ # ast-merge needing to know about them directly.
11
+ #
12
+ # == Purpose
13
+ #
14
+ # When running RSpec tests with dependency tags (e.g., `:markly_merge`),
15
+ # ast-merge needs to know if each merge gem is available. The MergeGemRegistry
16
+ # provides a way for gems to register their availability checkers, and also
17
+ # pre-configures known merge gems so they can be checked before being loaded.
18
+ #
19
+ # == Pre-configured Gems
20
+ #
21
+ # The following merge gems are pre-configured so that their availability can
22
+ # be checked before they are loaded (e.g., during RSpec setup):
23
+ # - :markly_merge, :commonmarker_merge, :markdown_merge (markdown)
24
+ # - :prism_merge, :bash_merge, :rbs_merge (code)
25
+ # - :json_merge, :jsonc_merge (data)
26
+ # - :toml_merge, :psych_merge, :dotenv_merge (config)
27
+ #
28
+ # External merge gems can also register themselves by calling {register}
29
+ # when loaded.
30
+ #
31
+ # == Registration
32
+ #
33
+ # Each merge gem registers itself when loaded using {register}:
34
+ # - Tag name (e.g., :markly_merge)
35
+ # - Require path (e.g., "markly/merge")
36
+ # - Merger class name (e.g., "Markly::Merge::SmartMerger")
37
+ # - Test source code to verify the merger works
38
+ # - Optional category for grouping (e.g., :markdown, :data, :code)
39
+ #
40
+ # When a tag is registered, an availability method is automatically defined
41
+ # on `Ast::Merge::RSpec::DependencyTags`.
42
+ #
43
+ # == Thread Safety
44
+ #
45
+ # All operations are thread-safe using a Mutex for synchronization.
46
+ # Results are cached after first check for performance.
47
+ #
48
+ # @example Registering a merge gem (in your gem's lib file)
49
+ # # In markly-merge/lib/markly/merge.rb
50
+ # if defined?(Ast::Merge::RSpec::MergeGemRegistry)
51
+ # Ast::Merge::RSpec::MergeGemRegistry.register(
52
+ # :markly_merge,
53
+ # require_path: "markly/merge",
54
+ # merger_class: "Markly::Merge::SmartMerger",
55
+ # test_source: "# Test\n\nParagraph",
56
+ # category: :markdown
57
+ # )
58
+ # end
59
+ #
60
+ # @example Checking availability
61
+ # Ast::Merge::RSpec::MergeGemRegistry.available?(:markly_merge) # => true/false
62
+ #
63
+ # @example Getting all registered gems
64
+ # Ast::Merge::RSpec::MergeGemRegistry.registered_gems # => [:markly_merge, :prism_merge, ...]
65
+ #
66
+ # @see Ast::Merge::RSpec::DependencyTags Uses MergeGemRegistry for dynamic gem detection
67
+ # @api public
68
+ module MergeGemRegistry
69
+ @mutex = Mutex.new
70
+ @registry = {} # rubocop:disable ThreadSafety/MutableClassInstanceVariable
71
+ @availability_cache = {} # rubocop:disable ThreadSafety/MutableClassInstanceVariable
72
+
73
+ # Valid categories for merge gems
74
+ CATEGORIES = %i[markdown data code config other].freeze
75
+
76
+ # Pre-configured known merge gems
77
+ # These can be checked before the gems are actually loaded
78
+ KNOWN_GEMS = {
79
+ # Markdown gems
80
+ markly_merge: {
81
+ require_path: "markly/merge",
82
+ merger_class: "Markly::Merge::SmartMerger",
83
+ test_source: "# Test\n\nParagraph",
84
+ category: :markdown,
85
+ skip_instantiation: false,
86
+ },
87
+ commonmarker_merge: {
88
+ require_path: "commonmarker/merge",
89
+ merger_class: "Commonmarker::Merge::SmartMerger",
90
+ test_source: "# Test\n\nParagraph",
91
+ category: :markdown,
92
+ skip_instantiation: false,
93
+ },
94
+ markdown_merge: {
95
+ require_path: "markdown/merge",
96
+ merger_class: "Markdown::Merge::SmartMerger",
97
+ test_source: "# Test\n\nParagraph",
98
+ category: :markdown,
99
+ skip_instantiation: true, # Requires backend
100
+ },
101
+ # Code gems
102
+ prism_merge: {
103
+ require_path: "prism/merge",
104
+ merger_class: "Prism::Merge::SmartMerger",
105
+ test_source: "def foo; end",
106
+ category: :code,
107
+ skip_instantiation: false,
108
+ },
109
+ bash_merge: {
110
+ require_path: "bash/merge",
111
+ merger_class: "Bash::Merge::SmartMerger",
112
+ test_source: "#!/bin/bash\necho hello",
113
+ category: :code,
114
+ skip_instantiation: false,
115
+ },
116
+ rbs_merge: {
117
+ require_path: "rbs/merge",
118
+ merger_class: "Rbs::Merge::SmartMerger",
119
+ test_source: "class Foo\nend",
120
+ category: :code,
121
+ skip_instantiation: false,
122
+ },
123
+ # Data gems
124
+ json_merge: {
125
+ require_path: "json/merge",
126
+ merger_class: "Json::Merge::SmartMerger",
127
+ test_source: '{"key": "value"}',
128
+ category: :data,
129
+ skip_instantiation: false,
130
+ },
131
+ jsonc_merge: {
132
+ require_path: "jsonc/merge",
133
+ merger_class: "Jsonc::Merge::SmartMerger",
134
+ test_source: "// comment\n{\"key\": \"value\"}",
135
+ category: :data,
136
+ skip_instantiation: false,
137
+ },
138
+ # Config gems
139
+ toml_merge: {
140
+ require_path: "toml/merge",
141
+ merger_class: "Toml::Merge::SmartMerger",
142
+ test_source: "[section]\nkey = \"value\"",
143
+ category: :config,
144
+ skip_instantiation: false,
145
+ },
146
+ psych_merge: {
147
+ require_path: "psych/merge",
148
+ merger_class: "Psych::Merge::SmartMerger",
149
+ test_source: "key: value",
150
+ category: :config,
151
+ skip_instantiation: false,
152
+ },
153
+ dotenv_merge: {
154
+ require_path: "dotenv/merge",
155
+ merger_class: "Dotenv::Merge::SmartMerger",
156
+ test_source: "KEY=value",
157
+ category: :config,
158
+ skip_instantiation: false,
159
+ },
160
+ }.freeze
161
+
162
+ module_function
163
+
164
+ # Register a merge gem for dependency tag support
165
+ #
166
+ # When a gem is registered, this also dynamically defines a `*_available?` method
167
+ # on `Ast::Merge::RSpec::DependencyTags` if it doesn't already exist.
168
+ #
169
+ # @param tag_name [Symbol] the RSpec tag name (e.g., :markly_merge)
170
+ # @param require_path [String] the require path for the gem (e.g., "markly/merge")
171
+ # @param merger_class [String] the full class name of the SmartMerger
172
+ # @param test_source [String] sample source code to test merging
173
+ # @param category [Symbol] category for grouping (:markdown, :data, :code, :config, :other)
174
+ # @param skip_instantiation [Boolean] if true, only check class exists (for gems requiring backends)
175
+ # @return [void]
176
+ #
177
+ # @example Register a merge gem
178
+ # Ast::Merge::RSpec::MergeGemRegistry.register(
179
+ # :markly_merge,
180
+ # require_path: "markly/merge",
181
+ # merger_class: "Markly::Merge::SmartMerger",
182
+ # test_source: "# Test\n\nParagraph",
183
+ # category: :markdown
184
+ # )
185
+ def register(tag_name, require_path:, merger_class:, test_source:, category: :other, skip_instantiation: false)
186
+ raise ArgumentError, "Invalid category: #{category}" unless CATEGORIES.include?(category)
187
+
188
+ tag_sym = tag_name.to_sym
189
+
190
+ @mutex.synchronize do
191
+ @registry[tag_sym] = {
192
+ require_path: require_path,
193
+ merger_class: merger_class,
194
+ test_source: test_source,
195
+ category: category,
196
+ skip_instantiation: skip_instantiation,
197
+ }
198
+ # Clear cache when re-registering
199
+ @availability_cache.delete(tag_sym)
200
+ end
201
+
202
+ # Define availability method on DependencyTags
203
+ define_availability_method(tag_sym)
204
+
205
+ nil
206
+ end
207
+
208
+ # Check if a merge gem is available and functional
209
+ #
210
+ # This method will try to load the gem if it's not yet registered but
211
+ # is known (in KNOWN_GEMS). This allows availability checking before
212
+ # the gem is explicitly loaded.
213
+ #
214
+ # @param tag_name [Symbol] the tag name to check
215
+ # @return [Boolean] true if the merge gem is available and works
216
+ def available?(tag_name)
217
+ tag_sym = tag_name.to_sym
218
+
219
+ # Check cache first
220
+ @mutex.synchronize do
221
+ return @availability_cache[tag_sym] if @availability_cache.key?(tag_sym)
222
+ end
223
+
224
+ # Get registration info (from registry or known gems)
225
+ info = @mutex.synchronize { @registry[tag_sym] }
226
+ info ||= KNOWN_GEMS[tag_sym]
227
+
228
+ return false unless info
229
+
230
+ # Check if gem works
231
+ result = gem_works?(
232
+ info[:require_path],
233
+ info[:merger_class],
234
+ info[:test_source],
235
+ info[:skip_instantiation],
236
+ )
237
+
238
+ # Cache result
239
+ @mutex.synchronize do
240
+ @availability_cache[tag_sym] = result
241
+ end
242
+
243
+ result
244
+ end
245
+
246
+ # Check if a tag is registered
247
+ #
248
+ # @param tag_name [Symbol] the tag name
249
+ # @return [Boolean] true if the tag is registered
250
+ def registered?(tag_name)
251
+ @mutex.synchronize do
252
+ @registry.key?(tag_name.to_sym)
253
+ end
254
+ end
255
+
256
+ # Get all registered gem tag names (including pre-configured known gems)
257
+ #
258
+ # @return [Array<Symbol>] list of registered tag names
259
+ def registered_gems
260
+ @mutex.synchronize do
261
+ (KNOWN_GEMS.keys + @registry.keys).uniq
262
+ end
263
+ end
264
+
265
+ # Get gems filtered by category
266
+ #
267
+ # @param category [Symbol] one of :markdown, :data, :code, :config, :other
268
+ # @return [Array<Symbol>] list of tag names in that category
269
+ def gems_by_category(category)
270
+ @mutex.synchronize do
271
+ known = KNOWN_GEMS.select { |_, info| info[:category] == category }.keys
272
+ registered = @registry.select { |_, info| info[:category] == category }.keys
273
+ (known + registered).uniq
274
+ end
275
+ end
276
+
277
+ # Get registration info for a gem
278
+ #
279
+ # @param tag_name [Symbol] the tag name
280
+ # @return [Hash, nil] registration info or nil if not registered/known
281
+ def info(tag_name)
282
+ tag_sym = tag_name.to_sym
283
+ @mutex.synchronize do
284
+ @registry[tag_sym]&.dup || KNOWN_GEMS[tag_sym]&.dup
285
+ end
286
+ end
287
+
288
+ # Get a summary of all registered gems and their availability
289
+ #
290
+ # @return [Hash{Symbol => Boolean}] map of tag name to availability
291
+ def summary
292
+ registered_gems.each_with_object({}) do |tag, result|
293
+ result[tag] = available?(tag)
294
+ end
295
+ end
296
+
297
+ # Clear the availability cache
298
+ #
299
+ # @return [void]
300
+ def clear_cache!
301
+ @mutex.synchronize do
302
+ @availability_cache.clear
303
+ end
304
+ nil
305
+ end
306
+
307
+ # Clear all registrations and cache
308
+ #
309
+ # @return [void]
310
+ def clear!
311
+ @mutex.synchronize do
312
+ @registry.clear
313
+ @availability_cache.clear
314
+ end
315
+ nil
316
+ end
317
+
318
+ # Reset memoized availability on DependencyTags
319
+ #
320
+ # @return [void]
321
+ def reset_availability!
322
+ clear_cache!
323
+ return unless defined?(DependencyTags)
324
+
325
+ registered_gems.each do |tag|
326
+ ivar = :"@#{tag}_available"
327
+ DependencyTags.remove_instance_variable(ivar) if DependencyTags.instance_variable_defined?(ivar)
328
+ end
329
+ end
330
+
331
+ # ============================================================
332
+ # Private Helpers
333
+ # ============================================================
334
+
335
+ # Check if a merge gem is available and functional
336
+ #
337
+ # @param require_path [String] the require path for the gem
338
+ # @param merger_class [String] the full class name of the SmartMerger
339
+ # @param test_source [String] sample source code to test merging
340
+ # @param skip_instantiation [Boolean] if true, only check class exists
341
+ # @return [Boolean] true if the merger can be loaded/instantiated
342
+ # @api private
343
+ def gem_works?(require_path, merger_class, test_source, skip_instantiation)
344
+ require require_path
345
+ klass = Object.const_get(merger_class)
346
+
347
+ if skip_instantiation
348
+ # Just check that the class exists and looks like a SmartMerger
349
+ klass.is_a?(Class) && klass.ancestors.any? { |a| a.name&.include?("SmartMergerBase") }
350
+ else
351
+ klass.new(test_source, test_source)
352
+ true
353
+ end
354
+ rescue LoadError, StandardError
355
+ false
356
+ end
357
+ private_class_method :gem_works?
358
+
359
+ # Dynamically define an availability method on DependencyTags
360
+ #
361
+ # @param tag_name [Symbol] the tag name (e.g., :markly_merge)
362
+ # @return [void]
363
+ # @api private
364
+ def define_availability_method(tag_name)
365
+ method_name = :"#{tag_name}_available?"
366
+
367
+ # Only define if DependencyTags is loaded
368
+ return unless defined?(DependencyTags)
369
+
370
+ # Don't override existing methods
371
+ return if DependencyTags.respond_to?(method_name)
372
+
373
+ # Define the method dynamically - MergeGemRegistry.available? handles caching
374
+ DependencyTags.define_singleton_method(method_name) do
375
+ MergeGemRegistry.available?(tag_name)
376
+ end
377
+ end
378
+ private_class_method :define_availability_method
379
+ end
380
+ end
381
+ end
382
+ end
@@ -5,7 +5,7 @@ module Ast
5
5
  # Version information for Ast::Merge
6
6
  module Version
7
7
  # Current version of the ast-merge gem
8
- VERSION = "3.1.0"
8
+ VERSION = "4.0.1"
9
9
  end
10
10
  VERSION = Version::VERSION # traditional location
11
11
  end