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, and also
pre-configures known merge gems so they can be checked before being loaded.

== Pre-configured Gems

The following merge gems are pre-configured so that their availability can
be checked before they are loaded (e.g., during RSpec setup):

  • :markly_merge, :commonmarker_merge, :markdown_merge (markdown)
  • :prism_merge, :bash_merge, :rbs_merge (code)
  • :json_merge, :jsonc_merge (data)
  • :toml_merge, :psych_merge, :dotenv_merge (config)

External merge gems can also register themselves by calling MergeGemRegistry.register
when loaded.

== 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
KNOWN_GEMS =

Pre-configured known merge gems
These can be checked before the gems are actually loaded

{
  # Markdown gems
  markly_merge: {
    require_path: "markly/merge",
    merger_class: "Markly::Merge::SmartMerger",
    test_source: "# Test\n\nParagraph",
    category: :markdown,
    skip_instantiation: false,
  },
  commonmarker_merge: {
    require_path: "commonmarker/merge",
    merger_class: "Commonmarker::Merge::SmartMerger",
    test_source: "# Test\n\nParagraph",
    category: :markdown,
    skip_instantiation: false,
  },
  markdown_merge: {
    require_path: "markdown/merge",
    merger_class: "Markdown::Merge::SmartMerger",
    test_source: "# Test\n\nParagraph",
    category: :markdown,
    skip_instantiation: true, # Requires backend
  },
  # Code gems
  prism_merge: {
    require_path: "prism/merge",
    merger_class: "Prism::Merge::SmartMerger",
    test_source: "def foo; end",
    category: :code,
    skip_instantiation: false,
  },
  bash_merge: {
    require_path: "bash/merge",
    merger_class: "Bash::Merge::SmartMerger",
    test_source: "#!/bin/bash\necho hello",
    category: :code,
    skip_instantiation: false,
  },
  rbs_merge: {
    require_path: "rbs/merge",
    merger_class: "Rbs::Merge::SmartMerger",
    test_source: "class Foo\nend",
    category: :code,
    skip_instantiation: false,
  },
  # Data gems
  json_merge: {
    require_path: "json/merge",
    merger_class: "Json::Merge::SmartMerger",
    test_source: '{"key": "value"}',
    category: :data,
    skip_instantiation: false,
  },
  jsonc_merge: {
    require_path: "jsonc/merge",
    merger_class: "Jsonc::Merge::SmartMerger",
    test_source: "// comment\n{\"key\": \"value\"}",
    category: :data,
    skip_instantiation: false,
  },
  # Config gems
  toml_merge: {
    require_path: "toml/merge",
    merger_class: "Toml::Merge::SmartMerger",
    test_source: "[section]\nkey = \"value\"",
    category: :config,
    skip_instantiation: false,
  },
  psych_merge: {
    require_path: "psych/merge",
    merger_class: "Psych::Merge::SmartMerger",
    test_source: "key: value",
    category: :config,
    skip_instantiation: false,
  },
  dotenv_merge: {
    require_path: "dotenv/merge",
    merger_class: "Dotenv::Merge::SmartMerger",
    test_source: "KEY=value",
    category: :config,
    skip_instantiation: false,
  },
}.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’s not yet registered but
is known (in KNOWN_GEMS). This allows availability checking before
the gem is explicitly loaded.

Parameters:

  • tag_name (Symbol)

    the tag name to check

Returns:

  • (Boolean)

    true if the merge gem is available and works



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 216

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 registry or known gems)
  info = @mutex.synchronize { @registry[tag_sym] }
  info ||= 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



382
383
384
385
386
387
388
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 382

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

.clear_cache!void

This method returns an undefined value.

Clear the availability cache



372
373
374
375
376
377
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 372

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!


340
341
342
343
344
345
346
347
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 340

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



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

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



353
354
355
356
357
358
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 353

def info(tag_name)
  tag_sym = tag_name.to_sym
  @mutex.synchronize do
    @registry[tag_sym]&.dup || KNOWN_GEMS[tag_sym]&.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)


185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 185

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_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 gem names from KNOWN_GEMS to register



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 273

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

    # Skip if not in KNOWN_GEMS
    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[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



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

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 all gems in KNOWN_GEMS. 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



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

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

.reset_availability!void

This method returns an undefined value.

Reset memoized availability on DependencyTags



393
394
395
396
397
398
399
400
401
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 393

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



363
364
365
366
367
# File 'lib/ast/merge/rspec/merge_gem_registry.rb', line 363

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