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 fromtree_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.
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
-
.available?(backend_name) ⇒ Boolean
Check if a backend is available.
-
.clear! ⇒ void
Clear all registrations and cache.
-
.clear_cache! ⇒ void
Clear the availability cache.
-
.register_availability_checker(backend_name, checker = nil) { ... } ⇒ void
Register an availability checker for a backend (simple form).
-
.register_tag(tag_name, category:, backend_name: nil, require_path: nil, checker: nil) { ... } ⇒ void
Register a full dependency tag with all metadata.
-
.registered?(backend_name) ⇒ Boolean
Check if an availability checker is registered for a backend.
-
.registered_backends ⇒ Array<Symbol>
Get all registered backend names.
-
.registered_tags ⇒ Array<Symbol>
Get all registered tag names.
-
.tag_available?(tag_name) ⇒ Boolean
Check if a tag’s dependency is available.
-
.tag_metadata(tag_name) ⇒ Hash?
Get tag metadata.
-
.tag_registered?(tag_name) ⇒ Boolean
Check if a tag is registered.
-
.tag_summary ⇒ Hash{Symbol => Boolean}
Get a summary of all registered tags and their availability.
-
.tags_by_category(category) ⇒ Array<Symbol>
Get tags filtered by category.
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.
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.
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).
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.
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.
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
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_backends ⇒ Array<Symbol>
Get all 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_tags ⇒ Array<Symbol>
Get all registered tag names
310 311 312 313 314 |
# File 'lib/tree_haver/backend_registry.rb', line 310 def @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.
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 = @mutex.synchronize { @tag_registry[tag_sym] } # If tag not registered, check if it's a backend name with _backend suffix unless # 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 [:require_path] begin require [:require_path] rescue LoadError # Gem not available return false end end # Check availability using the backend name available?([:backend_name]) end |
.tag_metadata(tag_name) ⇒ Hash?
Get tag metadata
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
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_summary ⇒ Hash{Symbol => Boolean}
Get a summary of all registered tags and their 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
324 325 326 327 328 |
# File 'lib/tree_haver/backend_registry.rb', line 324 def (category) @mutex.synchronize do @tag_registry.select { |_, | [:category] == category }.keys end end |