Module: TreeHaver::BackendRegistry

Defined in:
lib/tree_haver/backend_registry.rb

Overview

Registry for backend dependency tag availability checkers

This module allows external gems (like commonmarker-merge, markly-merge, rbs-merge)
to register their availability checker for RSpec dependency tags without
TreeHaver needing to know about them directly.

== Purpose

When running RSpec tests with dependency tags (e.g., :commonmarker_backend),
TreeHaver needs to know if each backend is available. Rather than hardcoding
checks like TreeHaver::Backends::Commonmarker.available? (which would fail
if the backend module doesn’t exist), the BackendRegistry provides a dynamic
way for backends to register their availability checkers.

== Built-in vs External Backends

  • Built-in backends (MRI, Rust, FFI, Java, Prism, Psych, Citrus) register
    their checkers automatically when loaded from tree_haver/backends/*.rb
  • External backends (commonmarker-merge, markly-merge, rbs-merge) register
    their checkers when their backend module is loaded

== Full Tag Registration

External gems can register complete tag support using BackendRegistry.register_tag:

  • Tag name (e.g., :commonmarker_backend)
  • Category (:backend, :gem, :parsing, :grammar)
  • Availability checker
  • Optional require path for lazy loading

This enables tree_haver/rspec/dependency_tags to automatically configure
RSpec exclusion filters for any registered tag without hardcoded knowledge.

== Thread Safety

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

Examples:

Registering a backend availability checker (simple form)

# In commonmarker-merge/lib/commonmarker/merge/backend.rb
TreeHaver::BackendRegistry.register_availability_checker(:commonmarker) do
  available?
end

Registering a full tag with require path (preferred for external gems)

TreeHaver::BackendRegistry.register_tag(
  :commonmarker_backend,
  category: :backend,
  backend_name: :commonmarker,
  require_path: "commonmarker/merge"
) { Commonmarker::Merge::Backend.available? }

Checking backend availability

TreeHaver::BackendRegistry.available?(:commonmarker)  # => true/false
TreeHaver::BackendRegistry.available?(:markly)        # => true/false
TreeHaver::BackendRegistry.available?(:rbs)           # => true/false

Getting all registered tags

TreeHaver::BackendRegistry.registered_tags # => [:commonmarker_backend, :markly_backend, ...]
TreeHaver::BackendRegistry.tags_by_category(:backend) # => [...]

See Also:

Constant Summary collapse

CATEGORIES =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Tag categories for organizing dependency tags

%i[backend gem parsing grammar engine other].freeze

Class Method Summary collapse

Class Method Details

.available?(backend_name) ⇒ Boolean

Check if a backend is available

If a checker was registered via register_availability_checker, it is called
(and the result cached). If no checker is registered, falls back to checking
TreeHaver::Backends::<Name>.available? for built-in backends.

Results are cached to avoid repeated expensive checks (e.g., requiring gems).
Use clear_cache! to reset the cache if backend availability may have changed.

Examples:

TreeHaver::BackendRegistry.available?(:commonmarker)  # => true
TreeHaver::BackendRegistry.available?(:nonexistent)   # => false

Parameters:

  • backend_name (Symbol, String)

    the backend name to check

Returns:

  • (Boolean)

    true if the backend is available, false otherwise



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/tree_haver/backend_registry.rb', line 199

def available?(backend_name)
  key = backend_name.to_sym

  # First, check cache and get checker without holding mutex for long
  checker = nil
  @mutex.synchronize do
    # Return cached result if available
    return @availability_cache[key] if @availability_cache.key?(key)

    # Get registered checker (if any)
    checker = @availability_checkers[key]
  end

  # Compute result OUTSIDE the mutex to avoid deadlock when loading backends
  # (loading a backend module triggers register_availability_checker which needs the mutex)
  result = if checker
    # Use the registered checker
    begin
      checker.call
    rescue StandardError
      false
    end
  else
    # Fall back to checking TreeHaver::Backends::<Name>
    # This may load the backend module, which will register its checker
    check_builtin_backend(key)
  end

  # Cache the result
  @mutex.synchronize do
    # Double-check cache in case another thread computed it
    return @availability_cache[key] if @availability_cache.key?(key)
    @availability_cache[key] = result
  end

  result
end

.clear!void

This method returns an undefined value.

Clear all registrations and cache

Removes all registered checkers and cached results.
Primarily useful for testing to reset state between test cases.

Examples:

TreeHaver::BackendRegistry.clear!


290
291
292
293
294
295
296
297
# File 'lib/tree_haver/backend_registry.rb', line 290

def clear!
  @mutex.synchronize do
    @availability_checkers.clear
    @availability_cache.clear
    @tag_registry.clear
  end
  nil
end

.clear_cache!void

This method returns an undefined value.

Clear the availability cache

Useful for testing or when backend availability may have changed
(e.g., after installing a gem mid-process).

Examples:

TreeHaver::BackendRegistry.clear_cache!
# Next call to available? will re-check


274
275
276
277
278
279
# File 'lib/tree_haver/backend_registry.rb', line 274

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

.register_availability_checker(backend_name, checker = nil) { ... } ⇒ void

This method returns an undefined value.

Register an availability checker for a backend (simple form)

The checker should be a callable (lambda/proc/block) that returns true if
the backend is available and can be used. The checker is called lazily
(only when available? is first called for this backend).

For full tag support including require paths, use register_tag instead.

Examples:

Register with a block

TreeHaver::BackendRegistry.register_availability_checker(:commonmarker) do
  require "commonmarker"
  true
rescue LoadError
  false
end

Register with a lambda

checker = -> { Commonmarker::Merge::Backend.available? }
TreeHaver::BackendRegistry.register_availability_checker(:commonmarker, checker)

Register referencing the module’s available? method

TreeHaver::BackendRegistry.register_availability_checker(:my_backend) do
  available?  # Calls the enclosing module's available? method
end

Parameters:

  • backend_name (Symbol, String)

    the backend name (e.g., :commonmarker, :markly)

  • checker (#call, nil) (defaults to: nil)

    a callable that returns true if the backend is available

Yields:

  • Block form of checker (alternative to passing a callable)

Yield Returns:

  • (Boolean)

    true if the backend is available

Raises:

  • (ArgumentError)

    if no checker callable or block is provided

  • (ArgumentError)

    if checker doesn’t respond to #call



171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/tree_haver/backend_registry.rb', line 171

def register_availability_checker(backend_name, checker = nil, &block)
  callable = checker || block
  raise ArgumentError, "Must provide a checker callable or block" unless callable
  raise ArgumentError, "Checker must respond to #call" unless callable.respond_to?(:call)

  @mutex.synchronize do
    @availability_checkers[backend_name.to_sym] = callable
    # Clear cache for this backend when re-registering
    @availability_cache.delete(backend_name.to_sym)
  end
  nil
end

.register_tag(tag_name, category:, backend_name: nil, require_path: nil, checker: nil) { ... } ⇒ void

This method returns an undefined value.

Register a full dependency tag with all metadata

This is the preferred method for external gems to register their availability
with complete tag support. It registers both the availability checker and
the tag metadata needed for RSpec configuration.

When a tag is registered, this also dynamically defines a *_available? method
on TreeHaver::RSpec::DependencyTags if it doesn’t already exist.

Examples:

Register a backend tag with require path

TreeHaver::BackendRegistry.register_tag(
  :commonmarker_backend,
  category: :backend,
  require_path: "commonmarker/merge"
) { Commonmarker::Merge::Backend.available? }

Register a gem tag

TreeHaver::BackendRegistry.register_tag(
  :toml_gem,
  category: :gem,
  require_path: "toml"
) { defined?(TOML) }

Parameters:

  • tag_name (Symbol)

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

  • category (Symbol)

    one of :backend, :gem, :parsing, :grammar, :engine, :other

  • backend_name (Symbol, nil) (defaults to: nil)

    the backend name for availability checks (defaults to tag without suffix)

  • require_path (String, nil) (defaults to: nil)

    optional require path to load before checking availability

  • checker (#call, nil) (defaults to: nil)

    a callable that returns true if available

Yields:

  • Block form of checker (alternative to passing a callable)

Yield Returns:

  • (Boolean)

    true if the tag’s dependency is available

Raises:

  • (ArgumentError)


109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/tree_haver/backend_registry.rb', line 109

def register_tag(tag_name, category:, backend_name: nil, require_path: nil, checker: nil, &block)
  callable = checker || block
  raise ArgumentError, "Must provide a checker callable or block" unless callable
  raise ArgumentError, "Checker must respond to #call" unless callable.respond_to?(:call)
  raise ArgumentError, "Invalid category: #{category}" unless CATEGORIES.include?(category)

  tag_sym = tag_name.to_sym
  # Derive backend_name from tag_name if not provided (e.g., :commonmarker_backend -> :commonmarker)
  derived_backend = backend_name || tag_sym.to_s.sub(/_backend$/, "").to_sym

  @mutex.synchronize do
    @tag_registry[tag_sym] = {
      category: category,
      backend_name: derived_backend,
      require_path: require_path,
      checker: callable,
    }
    # Also register as availability checker for the backend name
    @availability_checkers[derived_backend] = callable
    # Clear caches
    @availability_cache.delete(derived_backend)
  end

  # Dynamically define the availability method on DependencyTags
  # This happens outside the mutex to avoid potential deadlock
  define_availability_method(derived_backend, tag_sym)

  nil
end

.registered?(backend_name) ⇒ Boolean

Check if an availability checker is registered for a backend

Examples:

TreeHaver::BackendRegistry.registered?(:commonmarker)  # => true (if loaded)
TreeHaver::BackendRegistry.registered?(:nonexistent)   # => false

Parameters:

  • backend_name (Symbol, String)

    the backend name

Returns:

  • (Boolean)

    true if a checker is registered



245
246
247
248
249
# File 'lib/tree_haver/backend_registry.rb', line 245

def registered?(backend_name)
  @mutex.synchronize do
    @availability_checkers.key?(backend_name.to_sym)
  end
end

.registered_backendsArray<Symbol>

Get all registered backend names

Examples:

TreeHaver::BackendRegistry.registered_backends
# => [:mri, :rust, :ffi, :java, :prism, :psych, :citrus, :commonmarker, :markly]

Returns:

  • (Array<Symbol>)

    list of registered backend names



258
259
260
261
262
# File 'lib/tree_haver/backend_registry.rb', line 258

def registered_backends
  @mutex.synchronize do
    @availability_checkers.keys.dup
  end
end

.registered_tagsArray<Symbol>

Get all registered tag names

Examples:

TreeHaver::BackendRegistry.registered_tags
# => [:commonmarker_backend, :markly_backend, :toml_gem, ...]

Returns:

  • (Array<Symbol>)

    list of registered tag names



310
311
312
313
314
# File 'lib/tree_haver/backend_registry.rb', line 310

def registered_tags
  @mutex.synchronize do
    @tag_registry.keys.dup
  end
end

.tag_available?(tag_name) ⇒ Boolean

Check if a tag’s dependency is available

This method handles require paths: if the tag has a require_path, it will
attempt to load the gem before checking availability. This enables lazy
loading of external gems.

Examples:

TreeHaver::BackendRegistry.tag_available?(:commonmarker_backend)  # => true/false

Parameters:

  • tag_name (Symbol)

    the tag name to check

Returns:

  • (Boolean)

    true if the tag’s dependency is available



365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
# File 'lib/tree_haver/backend_registry.rb', line 365

def tag_available?(tag_name)
  tag_sym = tag_name.to_sym

  # Get tag metadata
  meta = @mutex.synchronize { @tag_registry[tag_sym] }

  # If tag not registered, check if it's a backend name with _backend suffix
  unless meta
    # Try to derive backend name (e.g., :commonmarker_backend -> :commonmarker)
    backend_name = tag_sym.to_s.sub(/_backend$/, "").to_sym
    return available?(backend_name) if backend_name != tag_sym
    return false
  end

  # Try to load the gem if require_path is specified
  if meta[:require_path]
    begin
      require meta[:require_path]
    rescue LoadError
      # Gem not available
      return false
    end
  end

  # Check availability using the backend name
  available?(meta[:backend_name])
end

.tag_metadata(tag_name) ⇒ Hash?

Get tag metadata

Examples:

TreeHaver::BackendRegistry.(:commonmarker_backend)
# => { category: :backend, backend_name: :commonmarker, require_path: "commonmarker/merge", checker: #<Proc> }

Parameters:

  • tag_name (Symbol)

    the tag name

Returns:

  • (Hash, nil)

    tag metadata or nil if not registered



338
339
340
341
342
# File 'lib/tree_haver/backend_registry.rb', line 338

def (tag_name)
  @mutex.synchronize do
    @tag_registry[tag_name.to_sym]&.dup
  end
end

.tag_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



348
349
350
351
352
# File 'lib/tree_haver/backend_registry.rb', line 348

def tag_registered?(tag_name)
  @mutex.synchronize do
    @tag_registry.key?(tag_name.to_sym)
  end
end

.tag_summaryHash{Symbol => Boolean}

Get a summary of all registered tags and their availability

Examples:

TreeHaver::BackendRegistry.tag_summary
# => { commonmarker_backend: true, markly_backend: false, ... }

Returns:

  • (Hash{Symbol => Boolean})

    map of tag name to availability



400
401
402
403
404
# File 'lib/tree_haver/backend_registry.rb', line 400

def tag_summary
  @mutex.synchronize { @tag_registry.keys.dup }.each_with_object({}) do |tag, result|
    result[tag] = tag_available?(tag)
  end
end

.tags_by_category(category) ⇒ Array<Symbol>

Get tags filtered by category

Examples:

TreeHaver::BackendRegistry.tags_by_category(:backend)
# => [:commonmarker_backend, :markly_backend, :mri_backend, ...]

Parameters:

  • category (Symbol)

    one of :backend, :gem, :parsing, :grammar, :engine, :other

Returns:

  • (Array<Symbol>)

    list of tag names in that category



324
325
326
327
328
# File 'lib/tree_haver/backend_registry.rb', line 324

def tags_by_category(category)
  @mutex.synchronize do
    @tag_registry.select { |_, meta| meta[:category] == category }.keys
  end
end