Module: Ast::Merge::RSpec::MergeGemRegistry

Defined in:
lib/ast/merge/rspec/merge_gem_registry.rb

Overview

Registry for merge gem dependency tag availability checkers

This module allows merge gems (like markly-merge, prism-merge, json-merge) to register their availability checker for RSpec dependency tags without ast-merge needing to know about them directly.

Purpose

When running RSpec tests with dependency tags (e.g., :markly_merge), ast-merge needs to know if each merge gem is available. The MergeGemRegistry provides a way for gems to register their availability checkers. Test bootstraps can also register known gem metadata before gems are loaded so RSpec filters are configured in time.

Registration

Each merge gem registers itself when loaded using MergeGemRegistry.register:

  • Tag name (e.g., :markly_merge)
  • Require path (e.g., "markly/merge")
  • Merger class name (e.g., "Markly::Merge::SmartMerger")
  • Test source code to verify the merger works
  • Optional category for grouping (e.g., :markdown, :data, :code)

When a tag is registered, an availability method is automatically defined on Ast::Merge::RSpec::DependencyTags.

Thread Safety

All operations are thread-safe using a Mutex for synchronization. Results are cached after first check for performance.

Examples:

Registering a merge gem (in your gem's lib file)

# In markly-merge/lib/markly/merge.rb
if defined?(Ast::Merge::RSpec::MergeGemRegistry)
  Ast::Merge::RSpec::MergeGemRegistry.register(
    :markly_merge,
    require_path: "markly/merge",
    merger_class: "Markly::Merge::SmartMerger",
    test_source: "# Test\n\nParagraph",
    category: :markdown
  )
end

Checking availability

Ast::Merge::RSpec::MergeGemRegistry.available?(:markly_merge)  # => true/false

Getting all registered gems

Ast::Merge::RSpec::MergeGemRegistry.registered_gems # => [:markly_merge, :prism_merge, ...]

See Also:

Constant Summary collapse

CATEGORIES =

Valid categories for merge gems

%i[markdown data code config other].freeze

Class Method Summary collapse

Class Method Details

.available?(tag_name) ⇒ Boolean

Check if a merge gem is available and functional

This method will try to load the gem if it was registered directly or predeclared by a spec bootstrap before the gem was explicitly loaded.

Parameters:

  • tag_name (Symbol)

    the tag name to check

Returns:

  • (Boolean)

    true if the merge gem is available and works



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 143

def available?(tag_name)
  tag_sym = tag_name.to_sym

  # Check cache first
  @mutex.synchronize do
    return @availability_cache[tag_sym] if @availability_cache.key?(tag_sym)
  end

  # Get registration info (from loaded registry or bootstrap metadata)
  info = @mutex.synchronize { @registry[tag_sym] }
  info ||= @mutex.synchronize { @known_gems[tag_sym] }

  return false unless info

  # Check if gem works
  result = gem_works?(
    info[:require_path],
    info[:merger_class],
    info[:test_source],
    info[:skip_instantiation]
  )

  # Cache result
  @mutex.synchronize do
    @availability_cache[tag_sym] = result
  end

  result
end

.clear!void

This method returns an undefined value.

Clear all registrations and cache



314
315
316
317
318
319
320
321
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 314

def clear!
  @mutex.synchronize do
    @registry.clear
    @known_gems.clear
    @availability_cache.clear
  end
  nil
end

.clear_cache!void

This method returns an undefined value.

Clear the availability cache



304
305
306
307
308
309
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 304

def clear_cache!
  @mutex.synchronize do
    @availability_cache.clear
  end
  nil
end

.force_check_availability!void

This method returns an undefined value.

Force availability checking for all registered gems

This method should be called AFTER SimpleCov is loaded (typically at the end of spec_helper.rb) to trigger gem loading and availability checking. Calling this ensures RSpec exclusion filters are properly configured based on which gems are actually available.

This is necessary because register_known_gems() only registers gems without checking availability. The actual availability check (which requires loading the gem) must happen AFTER coverage instrumentation is set up.

Examples:

At the end of spec_helper.rb (after SimpleCov loads)

# Force availability checking now that coverage is instrumented
Ast::Merge::RSpec::MergeGemRegistry.force_check_availability!


272
273
274
275
276
277
278
279
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 272

def force_check_availability!
  registered_gems.each do |tag|
    # This will trigger gem_works? which loads the gem
    # Results are cached, so subsequent calls are fast
    available?(tag)
  end
  nil
end

.gems_by_category(category) ⇒ Array<Symbol>

Get gems filtered by category

Parameters:

  • category (Symbol)

    one of :markdown, :data, :code, :config, :other

Returns:

  • (Array<Symbol>)

    list of tag names in that category



248
249
250
251
252
253
254
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 248

def gems_by_category(category)
  @mutex.synchronize do
    known = @known_gems.select { |_, info| info[:category] == category }.keys
    registered = @registry.select { |_, info| info[:category] == category }.keys
    (known + registered).uniq
  end
end

.info(tag_name) ⇒ Hash?

Get registration info for a gem

Parameters:

  • tag_name (Symbol)

    the tag name

Returns:

  • (Hash, nil)

    registration info or nil if not registered/known



285
286
287
288
289
290
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 285

def info(tag_name)
  tag_sym = tag_name.to_sym
  @mutex.synchronize do
    @registry[tag_sym]&.dup || @known_gems[tag_sym]&.dup
  end
end

.known_gemsObject



238
239
240
241
242
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 238

def known_gems
  @mutex.synchronize do
    @known_gems.transform_values(&:dup)
  end
end

.register(tag_name, require_path:, merger_class:, test_source:, category: :other, skip_instantiation: false) ⇒ void

This method returns an undefined value.

Register a merge gem for dependency tag support

When a gem is registered, this also dynamically defines a *_available? method on Ast::Merge::RSpec::DependencyTags if it doesn't already exist.

Examples:

Register a merge gem

Ast::Merge::RSpec::MergeGemRegistry.register(
  :markly_merge,
  require_path: "markly/merge",
  merger_class: "Markly::Merge::SmartMerger",
  test_source: "# Test\n\nParagraph",
  category: :markdown
)

Parameters:

  • tag_name (Symbol)

    the RSpec tag name (e.g., :markly_merge)

  • require_path (String)

    the require path for the gem (e.g., "markly/merge")

  • merger_class (String)

    the full class name of the SmartMerger

  • test_source (String)

    sample source code to test merging

  • category (Symbol) (defaults to: :other)

    category for grouping (:markdown, :data, :code, :config, :other)

  • skip_instantiation (Boolean) (defaults to: false)

    if true, only check class exists (for gems requiring backends)

Raises:

  • (ArgumentError)


90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 90

def register(tag_name, require_path:, merger_class:, test_source:, category: :other, skip_instantiation: false)
  raise ArgumentError, "Invalid category: #{category}" unless CATEGORIES.include?(category)

  tag_sym = tag_name.to_sym

  @mutex.synchronize do
    @registry[tag_sym] = {
      require_path: require_path,
      merger_class: merger_class,
      test_source: test_source,
      category: category,
      skip_instantiation: skip_instantiation
    }
    # Clear cache when re-registering
    @availability_cache.delete(tag_sym)
  end

  # Define availability method on DependencyTags
  define_availability_method(tag_sym)

  nil
end

.register_known_gem(tag_name, require_path:, merger_class:, test_source:, category: :other, skip_instantiation: false) ⇒ Object

Register metadata for a merge gem that may not be loaded yet.

This is intended for spec/bootstrap layers that need to declare the tag universe before RSpec filters examples. Runtime provider gems should still call register when loaded.

Raises:

  • (ArgumentError)


118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 118

def register_known_gem(tag_name, require_path:, merger_class:, test_source:, category: :other,
                       skip_instantiation: false)
  raise ArgumentError, "Invalid category: #{category}" unless CATEGORIES.include?(category)

  @mutex.synchronize do
    @known_gems[tag_name.to_sym] = {
      require_path: require_path,
      merger_class: merger_class,
      test_source: test_source,
      category: category,
      skip_instantiation: skip_instantiation
    }
  end

  define_availability_method(tag_name.to_sym)
  nil
end

.register_known_gems(*gem_names) ⇒ void

This method returns an undefined value.

Register one or more known gems for RSpec dependency tag support

This allows test suites to explicitly register only the merge gems they need for their tests, avoiding the overhead of registering all known gems.

Examples:

In spec/config/tree_haver.rb

# Only register the markdown merge gems that markly-merge tests depend on
Ast::Merge::RSpec::MergeGemRegistry.register_known_gems(:prism_merge)

Register multiple gems

Ast::Merge::RSpec::MergeGemRegistry.register_known_gems(
  :commonmarker_merge,
  :markly_merge
)

Parameters:

  • gem_names (Array<Symbol>)

    list of predeclared gem names to register



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 200

def register_known_gems(*gem_names)
  gem_names.each do |tag_name|
    tag_sym = tag_name.to_sym

    unless known_gems.key?(tag_sym)
      warn("Unknown gem: #{tag_name}. Available: #{known_gems.keys.join(', ')}")
      next
    end

    # Skip if already registered
    next if registered?(tag_sym)

     = known_gems.fetch(tag_sym)
    register(
      tag_sym,
      require_path: [:require_path],
      merger_class: [:merger_class],
      test_source: [:test_source],
      category: [:category],
      skip_instantiation: [:skip_instantiation]
    )
  end
end

.registered?(tag_name) ⇒ Boolean

Check if a tag is registered

Parameters:

  • tag_name (Symbol)

    the tag name

Returns:

  • (Boolean)

    true if the tag is registered



177
178
179
180
181
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 177

def registered?(tag_name)
  @mutex.synchronize do
    @registry.key?(tag_name.to_sym)
  end
end

.registered_gemsArray<Symbol>

Get all explicitly registered gem tag names

This returns ONLY gems that were explicitly registered via register() or register_known_gems(), NOT every predeclared gem. This prevents premature loading of gems during RSpec tag setup, which would happen before SimpleCov and ruin coverage reporting.

Returns:

  • (Array<Symbol>)

    list of registered tag names



232
233
234
235
236
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 232

def registered_gems
  @mutex.synchronize do
    @registry.keys
  end
end

.reset_availability!void

This method returns an undefined value.

Reset memoized availability on DependencyTags



326
327
328
329
330
331
332
333
334
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 326

def reset_availability!
  clear_cache!
  return unless defined?(DependencyTags)

  registered_gems.each do |tag|
    ivar = :"@#{tag}_available"
    DependencyTags.remove_instance_variable(ivar) if DependencyTags.instance_variable_defined?(ivar)
  end
end

.summaryHash{Symbol => Boolean}

Get a summary of all registered gems and their availability

Returns:

  • (Hash{Symbol => Boolean})

    map of tag name to availability



295
296
297
298
299
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 295

def summary
  registered_gems.each_with_object({}) do |tag, result|
    result[tag] = available?(tag)
  end
end