tree_haver 5.0.0 → 5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c483d5a811ccb8c64191bc8341e94ecc41996d1aab8a78ef81fea2833231a048
4
- data.tar.gz: 0c0e1ef09ac515f0faf00d8d5f88568acd573007bd31a94d04706f8016cedf86
3
+ metadata.gz: bf24354cab86e375d66426a05aa6afa5d852bc1941a8fe1235f0647a89891e77
4
+ data.tar.gz: 25f91798a18044f1e0b1b5deb83cc61fe734bd05f7391225a36dd11dbb47a941
5
5
  SHA512:
6
- metadata.gz: 98548924f413e5ee8c39bd35af68debb5f3463c088768d991635ad50f2dc649231c60ff8cae12efce4569bfbfbdd61ed2873a225fbcbb6289930490a7ec6a195
7
- data.tar.gz: edaa9affbcc349cf59dd57b02821345e4319d6666fcf01fcbfb8bd0eb490010545ff38aa175b108f0178aa24c8ca0573f653a5adc91449cd834b71aff7ac9acc
6
+ metadata.gz: dcd6d8d3e481bd1e5201a83441d63d41af9be368eb8c746e7d27a44dfa7cb76345a014f7a426557e40c1becfecf9f9e9f60176d8c6d53a57978077146137f13d
7
+ data.tar.gz: 5f1dc4d8844b5da005d5d1ef59ac3a74c1794ee95d25de1c5c67a13198f671a06ad45ce82d3530e875230374339c5a749f2a3e96ffe555b194a05b7a6cc4a4ef
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -1,4 +1,4 @@
1
- ed# Changelog
1
+ # Changelog
2
2
 
3
3
  [![SemVer 2.0.0][📌semver-img]][📌semver] [![Keep-A-Changelog 1.0.0][📗keep-changelog-img]][📗keep-changelog]
4
4
 
@@ -30,6 +30,54 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [5.0.1] - 2026-01-11
34
+
35
+ - TAG: [v5.0.1][5.0.1t]
36
+ - COVERAGE: 90.79% -- 2308/2542 lines in 30 files
37
+ - BRANCH COVERAGE: 78.09% -- 930/1191 branches in 30 files
38
+ - 94.76% documented
39
+
40
+ ### Added
41
+
42
+ - `TreeHaver::RSpec::TestableNode` - A testable node class for creating mock TreeHaver::Node instances
43
+ in tests without requiring an actual parser backend. Available via `require "tree_haver/rspec/testable_node"`
44
+ or automatically when using `require "tree_haver/rspec"`.
45
+ - `TestableNode.create(type:, text:, ...)` - Create a single test node
46
+ - `TestableNode.create_list(...)` - Create multiple test nodes
47
+ - `MockInnerNode` - The underlying mock that simulates backend-specific nodes
48
+ - Top-level `TestableNode` constant for convenience in specs
49
+ - **Fully Dynamic Tag Registration** in `TreeHaver::BackendRegistry`:
50
+ - `register_tag(tag_name, category:, backend_name:, require_path:)` - Register a complete dependency tag
51
+ with lazy loading support. External gems can now get full RSpec tag support without any hardcoded
52
+ knowledge in tree_haver.
53
+ - `tag_available?(tag_name)` - Check if a tag's dependency is available, with automatic lazy loading
54
+ via the registered `require_path`
55
+ - `registered_tags` - Get all registered tag names
56
+ - `tags_by_category(category)` - Get tags filtered by category (:backend, :gem, :parsing, :grammar, :engine, :other)
57
+ - `tag_metadata(tag_name)` - Get full metadata for a registered tag
58
+ - `tag_summary` - Get availability status of all registered tags
59
+
60
+ ### Changed
61
+
62
+ - **Fully Dynamic Backend Availability** in `BackendRegistry` and `DependencyTags`:
63
+ - `register_tag` now dynamically defines `*_available?` methods on `DependencyTags` at registration time
64
+ - External gems automatically get availability methods when they call `register_tag`
65
+ - No changes to tree_haver are needed for new external backend gems
66
+ - Built-in backends (prism, psych, citrus, parslet) retain explicit methods
67
+ - `summary` method dynamically includes registered backends from BackendRegistry
68
+ - `backend_availability_methods` and `backend_tags` hashes are built dynamically
69
+ - RSpec exclusion filters for backend tags are configured dynamically from BackendRegistry
70
+
71
+ ### Fixed
72
+
73
+ - **`TreeHaver::Parser#unwrap_language` bug fix for MRI and Rust backends**
74
+ - `:mri` and `:rust` cases were not returning the unwrapped language value
75
+ - The code called `lang.to_language` / `lang.inner_language` / `lang.name` but didn't `return` the result
76
+ - Now properly returns the unwrapped language for all backend types
77
+ - `any_markdown_backend_available?` now uses `BackendRegistry.tag_available?` instead of calling
78
+ `markly_available?` and `commonmarker_available?` directly. This fixes `NoMethodError` when
79
+ the external markdown backend gems haven't registered their tags yet.
80
+
33
81
  ## [5.0.0] - 2026-01-11
34
82
 
35
83
  - TAG: [v5.0.0][5.0.0t]
@@ -1211,7 +1259,9 @@ Despite the major version bump to 3.0.0 (following semver due to the breaking `L
1211
1259
 
1212
1260
  - Initial release
1213
1261
 
1214
- [Unreleased]: https://github.com/kettle-rb/tree_haver/compare/v5.0.0...HEAD
1262
+ [Unreleased]: https://github.com/kettle-rb/tree_haver/compare/v5.0.1...HEAD
1263
+ [5.0.1]: https://github.com/kettle-rb/tree_haver/compare/v5.0.0...v5.0.1
1264
+ [5.0.1t]: https://github.com/kettle-rb/tree_haver/releases/tag/v5.0.1
1215
1265
  [5.0.0]: https://github.com/kettle-rb/tree_haver/compare/v4.0.5...v5.0.0
1216
1266
  [5.0.0t]: https://github.com/kettle-rb/tree_haver/releases/tag/v5.0.0
1217
1267
  [4.0.5]: https://github.com/kettle-rb/tree_haver/compare/v4.0.4...v4.0.5
data/README.md CHANGED
@@ -31,7 +31,7 @@
31
31
  [rubygems-maint-policy]: https://github.com/ruby/rubygems/blob/b1ab33a3d52310a84d16b193991af07f5a6a07c0/doc/rubygems/POLICIES.md?plain=1#L187-L196
32
32
  [policy-fail]: https://www.reddit.com/r/ruby/comments/1ove9vp/rubycentral_hates_this_one_fact/
33
33
 
34
- [![Galtzo FLOSS Logo by Aboling0, CC BY-SA 4.0][🖼️galtzo-i]](https://discord.gg/3qme4XHNKN) [![ruby-lang Logo, Yukihiro Matsumoto, Ruby Visual Identity Team, CC BY-SA 2.5][🖼️ruby-lang-i]](https://www.ruby-lang.org/) [![kettle-rb Logo by Aboling0, CC BY-SA 4.0][🖼️kettle-rb-i]](https://github.com/kettle-rb)
34
+ [![Galtzo FLOSS Logo by Aboling0, CC BY-SA 4.0][🖼️galtzo-i]][🖼️galtzo-discord] [![ruby-lang Logo, Yukihiro Matsumoto, Ruby Visual Identity Team, CC BY-SA 2.5][🖼️ruby-lang-i]][🖼️ruby-lang] [![kettle-rb Logo by Aboling0, CC BY-SA 4.0][🖼️kettle-rb-i]][🖼️kettle-rb]
35
35
 
36
36
  [🖼️galtzo-i]: https://logos.galtzo.com/assets/images/galtzo-floss/avatar-192px.svg
37
37
  [🖼️galtzo-discord]: https://discord.gg/3qme4XHNKN
@@ -42,7 +42,7 @@
42
42
 
43
43
  # 🌴 TreeHaver
44
44
 
45
- [![Version][👽versioni]](https://bestgems.org/gems/tree_haver) [![GitHub tag (latest SemVer)][⛳️tag-img]](http://github.com/kettle-rb/tree_haver/releases) [![License: MIT][📄license-img]](https://opensource.org/licenses/MIT) [![Downloads Rank][👽dl-ranki]](https://bestgems.org/gems/tree_haver) [![Open Source Helpers][👽oss-helpi]](https://www.codetriage.com/kettle-rb/tree_haver) [![CodeCov Test Coverage][🏀codecovi]](https://codecov.io/gh/kettle-rb/tree_haver) [![Coveralls Test Coverage][🏀coveralls-img]](https://coveralls.io/github/kettle-rb/tree_haver?branch=main) [![QLTY Test Coverage][🏀qlty-covi]](https://qlty.sh/gh/kettle-rb/projects/tree_haver/metrics/code?sort=coverageRating) [![QLTY Maintainability][🏀qlty-mnti]](https://qlty.sh/gh/kettle-rb/projects/tree_haver) [![CI Heads][🚎3-hd-wfi]](https://github.com/kettle-rb/tree_haver/actions/workflows/heads.yml) [![CI Runtime Dependencies @ HEAD][🚎12-crh-wfi]](https://github.com/kettle-rb/tree_haver/actions/workflows/dep-heads.yml) [![CI Current][🚎11-c-wfi]](https://github.com/kettle-rb/tree_haver/actions/workflows/current.yml) [![CI Truffle Ruby][🚎9-t-wfi]](https://github.com/kettle-rb/tree_haver/actions/workflows/truffle.yml) [![Deps Locked][🚎13-🔒️-wfi]](https://github.com/kettle-rb/tree_haver/actions/workflows/locked_deps.yml) [![Deps Unlocked][🚎14-🔓️-wfi]](https://github.com/kettle-rb/tree_haver/actions/workflows/unlocked_deps.yml) [![CI Supported][🚎6-s-wfi]](https://github.com/kettle-rb/tree_haver/actions/workflows/supported.yml) [![CI Test Coverage][🚎2-cov-wfi]](https://github.com/kettle-rb/tree_haver/actions/workflows/coverage.yml) [![CI Style][🚎5-st-wfi]](https://github.com/kettle-rb/tree_haver/actions/workflows/style.yml) [![CodeQL][🖐codeQL-img]](https://github.com/kettle-rb/tree_haver/security/code-scanning) [![Apache SkyWalking Eyes License Compatibility Check][🚎15-🪪-wfi]](https://github.com/kettle-rb/tree_haver/actions/workflows/license-eye.yml)
45
+ [![Version][👽versioni]][👽dl-rank] [![GitHub tag (latest SemVer)][⛳️tag-img]][⛳️tag] [![License: MIT][📄license-img]][📄license-ref] [![Downloads Rank][👽dl-ranki]][👽dl-rank] [![Open Source Helpers][👽oss-helpi]][👽oss-help] [![CodeCov Test Coverage][🏀codecovi]][🏀codecov] [![Coveralls Test Coverage][🏀coveralls-img]][🏀coveralls] [![QLTY Test Coverage][🏀qlty-covi]][🏀qlty-cov] [![QLTY Maintainability][🏀qlty-mnti]][🏀qlty-mnt] [![CI Heads][🚎3-hd-wfi]][🚎3-hd-wf] [![CI Runtime Dependencies @ HEAD][🚎12-crh-wfi]][🚎12-crh-wf] [![CI Current][🚎11-c-wfi]][🚎11-c-wf] [![CI Truffle Ruby][🚎9-t-wfi]][🚎9-t-wf] [![Deps Locked][🚎13-🔒️-wfi]][🚎13-🔒️-wf] [![Deps Unlocked][🚎14-🔓️-wfi]][🚎14-🔓️-wf] [![CI Supported][🚎6-s-wfi]][🚎6-s-wf] [![CI Test Coverage][🚎2-cov-wfi]][🚎2-cov-wf] [![CI Style][🚎5-st-wfi]][🚎5-st-wf] [![CodeQL][🖐codeQL-img]][🖐codeQL] [![Apache SkyWalking Eyes License Compatibility Check][🚎15-🪪-wfi]][🚎15-🪪-wf]
46
46
 
47
47
  `if ci_badges.map(&:color).detect { it != "green"}` ☝️ [let me know][🖼️galtzo-discord], as I may have missed the [discord notification][🖼️galtzo-discord].
48
48
 
@@ -50,7 +50,7 @@
50
50
 
51
51
  `if ci_badges.map(&:color).all? { it == "green"}` 👇️ send money so I can do more of this. FLOSS maintenance is now my full-time job.
52
52
 
53
- [![OpenCollective Backers][🖇osc-backers-i]](https://opencollective.com/kettle-rb#backer) [![OpenCollective Sponsors][🖇osc-sponsors-i]](https://opencollective.com/kettle-rb#sponsor) [![Sponsor Me on Github][🖇sponsor-img]](https://github.com/sponsors/pboling) [![Liberapay Goal Progress][⛳liberapay-img]](https://liberapay.com/pboling/donate) [![Donate on PayPal][🖇paypal-img]](https://www.paypal.com/paypalme/peterboling) [![Buy me a coffee][🖇buyme-small-img]](https://www.buymeacoffee.com/pboling) [![Donate on Polar][🖇polar-img]](https://polar.sh/pboling) [![Donate at ko-fi.com][🖇kofi-img]](https://ko-fi.com/O5O86SNP4)
53
+ [![OpenCollective Backers][🖇osc-backers-i]][🖇osc-backers] [![OpenCollective Sponsors][🖇osc-sponsors-i]][🖇osc-sponsors] [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Donate on PayPal][🖇paypal-img]][🖇paypal] [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate at ko-fi.com][🖇kofi-img]][🖇kofi]
54
54
 
55
55
  ## 🌻 Synopsis
56
56
 
@@ -525,18 +525,18 @@ tree_haver supports multiple parsing backends, but not all backends work on all
525
525
 
526
526
  ## 💡 Info you can shake a stick at
527
527
 
528
- | Tokens to Remember | [![Gem name][⛳️name-img]](https://bestgems.org/gems/tree_haver) [![Gem namespace][⛳️namespace-img]](https://github.com/kettle-rb/tree_haver) |
528
+ | Tokens to Remember | [![Gem name][⛳️name-img]][👽dl-rank] [![Gem namespace][⛳️namespace-img]][📜src-gh] |
529
529
  |-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
530
- | Works with JRuby | [![JRuby 10.0 Compat][💎jruby-c-i]](https://github.com/kettle-rb/tree_haver/actions/workflows/current.yml) [![JRuby HEAD Compat][💎jruby-headi]](https://github.com/kettle-rb/tree_haver/actions/workflows/heads.yml) |
531
- | Works with Truffle Ruby | [![Truffle Ruby 23.1 Compat][💎truby-23.1i]](https://github.com/kettle-rb/tree_haver/actions/workflows/truffle.yml) [![Truffle Ruby 24.1 Compat][💎truby-c-i]](https://github.com/kettle-rb/tree_haver/actions/workflows/current.yml) |
532
- | Works with MRI Ruby 3 | [![Ruby 3.2 Compat][💎ruby-3.2i]](https://github.com/kettle-rb/tree_haver/actions/workflows/supported.yml) [![Ruby 3.3 Compat][💎ruby-3.3i]](https://github.com/kettle-rb/tree_haver/actions/workflows/supported.yml) [![Ruby 3.4 Compat][💎ruby-c-i]](https://github.com/kettle-rb/tree_haver/actions/workflows/current.yml) [![Ruby HEAD Compat][💎ruby-headi]](https://github.com/kettle-rb/tree_haver/actions/workflows/heads.yml) |
533
- | Support & Community | [![Join Me on Daily.dev's RubyFriends][✉️ruby-friends-img]](https://app.daily.dev/squads/rubyfriends) [![Live Chat on Discord][✉️discord-invite-img-ftb]](https://discord.gg/3qme4XHNKN) [![Get help from me on Upwork][👨🏼‍🏫expsup-upwork-img]](https://www.upwork.com/freelancers/~014942e9b056abdf86?mp_source=share) [![Get help from me on Codementor][👨🏼‍🏫expsup-codementor-img]](https://www.codementor.io/peterboling?utm_source=github&utm_medium=button&utm_term=peterboling&utm_campaign=github) |
534
- | Source | [![Source on GitLab.com][📜src-gl-img]](https://gitlab.com/kettle-rb/tree_haver/) [![Source on CodeBerg.org][📜src-cb-img]](https://codeberg.org/kettle-rb/tree_haver) [![Source on Github.com][📜src-gh-img]](https://github.com/kettle-rb/tree_haver) [![The best SHA: dQw4w9WgXcQ\!](https://img.shields.io/badge/KLOC-2.484-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue)](https://www.youtube.com/watch?v=dQw4w9WgXcQ) |
535
- | Documentation | [![Current release on RubyDoc.info][📜docs-cr-rd-img]](http://rubydoc.info/gems/tree_haver) [![YARD on Galtzo.com][📜docs-head-rd-img]](https://tree-haver.galtzo.com) [![Maintainer Blog][🚂maint-blog-img]](http://www.railsbling.com/tags/tree_haver) [![GitLab Wiki][📜gl-wiki-img]](https://gitlab.com/kettle-rb/tree_haver/-/wikis/home) [![GitHub Wiki][📜gh-wiki-img]](https://github.com/kettle-rb/tree_haver/wiki) |
536
- | Compliance | [![License: MIT][📄license-img]](https://opensource.org/licenses/MIT) [![Compatible with Apache Software Projects: Verified by SkyWalking Eyes][📄license-compat-img]](https://dev.to/galtzo/how-to-check-license-compatibility-41h0) [![📄ilo-declaration-img][📄ilo-declaration-img]](https://www.ilo.org/declaration/lang--en/index.htm) [![Security Policy][🔐security-img]](SECURITY.md) [![Contributor Covenant 2.1][🪇conduct-img]](CODE_OF_CONDUCT.md) [![SemVer 2.0.0][📌semver-img]](https://semver.org/spec/v2.0.0.html) |
537
- | Style | [![Enforced Code Style Linter][💎rlts-img]](https://github.com/rubocop-lts/rubocop-lts) [![Keep-A-Changelog 1.0.0][📗keep-changelog-img]](https://keepachangelog.com/en/1.0.0/) [![Gitmoji Commits][📌gitmoji-img]](https://gitmoji.dev) [![Compatibility appraised by: appraisal2][💎appraisal2-img]](https://github.com/appraisal-rb/appraisal2) |
538
- | Maintainer 🎖️ | [![Follow Me on LinkedIn][💖🖇linkedin-img]](http://www.linkedin.com/in/peterboling) [![Follow Me on Ruby.Social][💖🐘ruby-mast-img]](https://ruby.social/@galtzo) [![Follow Me on Bluesky][💖🦋bluesky-img]](https://bsky.app/profile/galtzo.com) [![Contact Maintainer][🚂maint-contact-img]](http://www.railsbling.com/contact) [![My technical writing][💖💁🏼‍♂️devto-img]](https://dev.to/galtzo) |
539
- | `...` 💖 | [![Find Me on WellFound:][💖✌️wellfound-img]](https://wellfound.com/u/peter-boling) [![Find Me on CrunchBase][💖💲crunchbase-img]](https://www.crunchbase.com/person/peter-boling) [![My LinkTree][💖🌳linktree-img]](https://linktr.ee/galtzo) [![More About Me][💖💁🏼‍♂️aboutme-img]](https://about.me/peter.boling) [🧊][💖🧊berg] [🐙][💖🐙hub] [🛖][💖🛖hut] [🧪][💖🧪lab] |
530
+ | Works with JRuby | [![JRuby 10.0 Compat][💎jruby-c-i]][🚎11-c-wf] [![JRuby HEAD Compat][💎jruby-headi]][🚎3-hd-wf] |
531
+ | Works with Truffle Ruby | [![Truffle Ruby 23.1 Compat][💎truby-23.1i]][🚎9-t-wf] [![Truffle Ruby 24.1 Compat][💎truby-c-i]][🚎11-c-wf] |
532
+ | Works with MRI Ruby 3 | [![Ruby 3.2 Compat][💎ruby-3.2i]][🚎6-s-wf] [![Ruby 3.3 Compat][💎ruby-3.3i]][🚎6-s-wf] [![Ruby 3.4 Compat][💎ruby-c-i]][🚎11-c-wf] [![Ruby HEAD Compat][💎ruby-headi]][🚎3-hd-wf] |
533
+ | Support & Community | [![Join Me on Daily.dev's RubyFriends][✉️ruby-friends-img]][✉️ruby-friends] [![Live Chat on Discord][✉️discord-invite-img-ftb]][🖼️galtzo-discord] [![Get help from me on Upwork][👨🏼‍🏫expsup-upwork-img]][👨🏼‍🏫expsup-upwork] [![Get help from me on Codementor][👨🏼‍🏫expsup-codementor-img]][👨🏼‍🏫expsup-codementor] |
534
+ | Source | [![Source on GitLab.com][📜src-gl-img]][📜src-gl] [![Source on CodeBerg.org][📜src-cb-img]][📜src-cb] [![Source on Github.com][📜src-gh-img]][📜src-gh] [![The best SHA: dQw4w9WgXcQ\!](https://img.shields.io/badge/KLOC-2.484-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue)][🧮kloc] |
535
+ | Documentation | [![Current release on RubyDoc.info][📜docs-cr-rd-img]][🚎yard-current] [![YARD on Galtzo.com][📜docs-head-rd-img]][🚎yard-head] [![Maintainer Blog][🚂maint-blog-img]][🚂maint-blog] [![GitLab Wiki][📜gl-wiki-img]][📜gl-wiki] [![GitHub Wiki][📜gh-wiki-img]][📜gh-wiki] |
536
+ | Compliance | [![License: MIT][📄license-img]][📄license-ref] [![Compatible with Apache Software Projects: Verified by SkyWalking Eyes][📄license-compat-img]][📄license-compat] [![📄ilo-declaration-img][📄ilo-declaration-img]][📄ilo-declaration] [![Security Policy][🔐security-img]][🔐security] [![Contributor Covenant 2.1][🪇conduct-img]][🪇conduct] [![SemVer 2.0.0][📌semver-img]][📌semver] |
537
+ | Style | [![Enforced Code Style Linter][💎rlts-img]][💎rlts] [![Keep-A-Changelog 1.0.0][📗keep-changelog-img]][📗keep-changelog] [![Gitmoji Commits][📌gitmoji-img]][📌gitmoji] [![Compatibility appraised by: appraisal2][💎appraisal2-img]][💎appraisal2] |
538
+ | Maintainer 🎖️ | [![Follow Me on LinkedIn][💖🖇linkedin-img]][💖🖇linkedin] [![Follow Me on Ruby.Social][💖🐘ruby-mast-img]][💖🐘ruby-mast] [![Follow Me on Bluesky][💖🦋bluesky-img]][💖🦋bluesky] [![Contact Maintainer][🚂maint-contact-img]][🚂maint-contact] [![My technical writing][💖💁🏼‍♂️devto-img]][💖💁🏼‍♂️devto] |
539
+ | `...` 💖 | [![Find Me on WellFound:][💖✌️wellfound-img]][💖✌️wellfound] [![Find Me on CrunchBase][💖💲crunchbase-img]][💖💲crunchbase] [![My LinkTree][💖🌳linktree-img]][💖🌳linktree] [![More About Me][💖💁🏼‍♂️aboutme-img]][💖💁🏼‍♂️aboutme] [🧊][💖🧊berg] [🐙][💖🐙hub] [🛖][💖🛖hut] [🧪][💖🧪lab] |
540
540
 
541
541
  ### Compatibility
542
542
 
@@ -556,13 +556,13 @@ Compatible with MRI Ruby 3.2.0+, and concordant releases of JRuby, and TruffleRu
556
556
  | 🧪 [kettle-rb/tree\_haver on GitLab][📜src-gl] | The Truth | [💚][🤝gl-issues] | [💚][🤝gl-pulls] | [💚][📜gl-wiki] | 🐭 Tiny Matrix | ➖ |
557
557
  | 🧊 [kettle-rb/tree\_haver on CodeBerg][📜src-cb] | An Ethical Mirror ([Donate][🤝cb-donate]) | [💚][🤝cb-issues] | [💚][🤝cb-pulls] | ➖ | ⭕️ No Matrix | ➖ |
558
558
  | 🐙 [kettle-rb/tree\_haver on GitHub][📜src-gh] | Another Mirror | [💚][🤝gh-issues] | [💚][🤝gh-pulls] | [💚][📜gh-wiki] | 💯 Full Matrix | [💚][gh-discussions] |
559
- | 🎮️ [Discord Server][🖼️galtzo-discord] | [![Live Chat on Discord][✉️discord-invite-img-ftb]](https://discord.gg/3qme4XHNKN) | [Let's][🖼️galtzo-discord] | [talk][🖼️galtzo-discord] | [about][🖼️galtzo-discord] | [this][🖼️galtzo-discord] | [library\!][🖼️galtzo-discord] |
559
+ | 🎮️ [Discord Server][🖼️galtzo-discord] | [![Live Chat on Discord][✉️discord-invite-img-ftb]][🖼️galtzo-discord] | [Let's][🖼️galtzo-discord] | [talk][🖼️galtzo-discord] | [about][🖼️galtzo-discord] | [this][🖼️galtzo-discord] | [library\!][🖼️galtzo-discord] |
560
560
 
561
561
  </details>
562
562
 
563
563
  [gh-discussions]: https://github.com/kettle-rb/tree_haver/discussions
564
564
 
565
- ### Enterprise Support [![Tidelift](https://tidelift.com/badges/package/rubygems/tree_haver)](https://tidelift.com/subscription/pkg/rubygems-tree_haver?utm_source=rubygems-tree_haver&utm_medium=referral&utm_campaign=readme)
565
+ ### Enterprise Support [![Tidelift](https://tidelift.com/badges/package/rubygems/tree_haver)][🏙️entsup-tidelift]
566
566
 
567
567
  Available as part of the Tidelift Subscription.
568
568
 
@@ -571,7 +571,7 @@ Available as part of the Tidelift Subscription.
571
571
 
572
572
  The maintainers of this and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use.
573
573
 
574
- [![Get help from me on Tidelift][🏙️entsup-tidelift-img]](https://tidelift.com/subscription/pkg/rubygems-tree_haver?utm_source=rubygems-tree_haver&utm_medium=referral&utm_campaign=readme)
574
+ [![Get help from me on Tidelift][🏙️entsup-tidelift-img]][🏙️entsup-tidelift]
575
575
 
576
576
  - 💡Subscribe for support guarantees covering *all* your FLOSS dependencies
577
577
 
@@ -580,11 +580,11 @@ The maintainers of this and thousands of other packages are working with Tidelif
580
580
  - 💡Tidelift pays maintainers to maintain the software you depend on\!<br/>📊`@`Pointy Haired Boss: An [enterprise support][🏙️entsup-tidelift] subscription is "[never gonna let you down][🧮kloc]", and *supports* open source maintainers
581
581
  Alternatively:
582
582
 
583
- - [![Live Chat on Discord][✉️discord-invite-img-ftb]](https://discord.gg/3qme4XHNKN)
583
+ - [![Live Chat on Discord][✉️discord-invite-img-ftb]][🖼️galtzo-discord]
584
584
 
585
- - [![Get help from me on Upwork][👨🏼‍🏫expsup-upwork-img]](https://www.upwork.com/freelancers/~014942e9b056abdf86?mp_source=share)
585
+ - [![Get help from me on Upwork][👨🏼‍🏫expsup-upwork-img]][👨🏼‍🏫expsup-upwork]
586
586
 
587
- - [![Get help from me on Codementor][👨🏼‍🏫expsup-codementor-img]](https://www.codementor.io/peterboling?utm_source=github&utm_medium=button&utm_term=peterboling&utm_campaign=github)
587
+ - [![Get help from me on Codementor][👨🏼‍🏫expsup-codementor-img]][👨🏼‍🏫expsup-codementor]
588
588
 
589
589
  </details>
590
590
 
@@ -2033,7 +2033,7 @@ I’m developing a new library, [floss\_funding][🖇floss-funding-gem], designe
2033
2033
 
2034
2034
  **[Floss-Funding.dev][🖇floss-funding.dev]: 👉️ No network calls. 👉️ No tracking. 👉️ No oversight. 👉️ Minimal crypto hashing. 💡 Easily disabled nags**
2035
2035
 
2036
- [![OpenCollective Backers][🖇osc-backers-i]](https://opencollective.com/kettle-rb#backer) [![OpenCollective Sponsors][🖇osc-sponsors-i]](https://opencollective.com/kettle-rb#sponsor) [![Sponsor Me on Github][🖇sponsor-img]](https://github.com/sponsors/pboling) [![Liberapay Goal Progress][⛳liberapay-img]](https://liberapay.com/pboling/donate) [![Donate on PayPal][🖇paypal-img]](https://www.paypal.com/paypalme/peterboling) [![Buy me a coffee][🖇buyme-small-img]](https://www.buymeacoffee.com/pboling) [![Donate on Polar][🖇polar-img]](https://polar.sh/pboling) [![Donate to my FLOSS efforts at ko-fi.com][🖇kofi-img]](https://ko-fi.com/O5O86SNP4) [![Donate to my FLOSS efforts using Patreon][🖇patreon-img]](https://patreon.com/galtzo)
2036
+ [![OpenCollective Backers][🖇osc-backers-i]][🖇osc-backers] [![OpenCollective Sponsors][🖇osc-sponsors-i]][🖇osc-sponsors] [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Donate on PayPal][🖇paypal-img]][🖇paypal] [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate to my FLOSS efforts at ko-fi.com][🖇kofi-img]][🖇kofi] [![Donate to my FLOSS efforts using Patreon][🖇patreon-img]][🖇patreon]
2037
2037
 
2038
2038
  ## 🔐 Security
2039
2039
 
@@ -2045,7 +2045,7 @@ If you need some ideas of where to help, you could work on adding more code cove
2045
2045
  or if it is already 💯 (see [below](#code-coverage)) check [reek](REEK), [issues][🤝gh-issues], or [PRs][🤝gh-pulls],
2046
2046
  or use the gem and think about how it could be better.
2047
2047
 
2048
- We [![Keep A Changelog][📗keep-changelog-img]](https://keepachangelog.com/en/1.0.0/) so if you make changes, remember to update it.
2048
+ We [![Keep A Changelog][📗keep-changelog-img]][📗keep-changelog] so if you make changes, remember to update it.
2049
2049
 
2050
2050
  See [CONTRIBUTING.md][🤝contributing] for more detailed instructions.
2051
2051
 
@@ -2055,20 +2055,20 @@ See [CONTRIBUTING.md][🤝contributing].
2055
2055
 
2056
2056
  ### Code Coverage
2057
2057
 
2058
- [![Coverage Graph][🏀codecov-g]](https://codecov.io/gh/kettle-rb/tree_haver)
2058
+ [![Coverage Graph][🏀codecov-g]][🏀codecov]
2059
2059
 
2060
- [![Coveralls Test Coverage][🏀coveralls-img]](https://coveralls.io/github/kettle-rb/tree_haver?branch=main)
2060
+ [![Coveralls Test Coverage][🏀coveralls-img]][🏀coveralls]
2061
2061
 
2062
- [![QLTY Test Coverage][🏀qlty-covi]](https://qlty.sh/gh/kettle-rb/projects/tree_haver/metrics/code?sort=coverageRating)
2062
+ [![QLTY Test Coverage][🏀qlty-covi]][🏀qlty-cov]
2063
2063
 
2064
2064
  ### 🪇 Code of Conduct
2065
2065
 
2066
2066
  Everyone interacting with this project's codebases, issue trackers,
2067
- chat rooms and mailing lists agrees to follow the [![Contributor Covenant 2.1][🪇conduct-img]](CODE_OF_CONDUCT.md).
2067
+ chat rooms and mailing lists agrees to follow the [![Contributor Covenant 2.1][🪇conduct-img]][🪇conduct].
2068
2068
 
2069
2069
  ## 🌈 Contributors
2070
2070
 
2071
- [![Contributors][🖐contributors-img]](https://github.com/kettle-rb/tree_haver/graphs/contributors)
2071
+ [![Contributors][🖐contributors-img]][🖐contributors]
2072
2072
 
2073
2073
  Made with [contributors-img][🖐contrib-rocks].
2074
2074
 
@@ -2089,7 +2089,7 @@ Also see GitLab Contributors: <https://gitlab.com/kettle-rb/tree_haver/-/graphs/
2089
2089
 
2090
2090
  ## 📌 Versioning
2091
2091
 
2092
- This Library adheres to [![Semantic Versioning 2.0.0][📌semver-img]](https://semver.org/spec/v2.0.0.html).
2092
+ This Library adheres to [![Semantic Versioning 2.0.0][📌semver-img]][📌semver].
2093
2093
  Violations of this scheme should be reported as bugs.
2094
2094
  Specifically, if a minor or patch version is released that breaks backward compatibility,
2095
2095
  a new version should be immediately released that restores compatibility.
@@ -2127,7 +2127,7 @@ See [CHANGELOG.md][📌changelog] for a list of releases.
2127
2127
  ## 📄 License
2128
2128
 
2129
2129
  The gem is available as open source under the terms of
2130
- the [MIT License][📄license] [![License: MIT][📄license-img]](https://opensource.org/licenses/MIT).
2130
+ the [MIT License][📄license] [![License: MIT][📄license-img]][📄license-ref].
2131
2131
  See [LICENSE.txt][📄license] for the official [Copyright Notice][📄copyright-notice-explainer].
2132
2132
 
2133
2133
  ### © Copyright
@@ -2155,11 +2155,11 @@ Please consider sponsoring me or the project.
2155
2155
 
2156
2156
  To join the community or get help 👇️ Join the Discord.
2157
2157
 
2158
- [![Live Chat on Discord][✉️discord-invite-img-ftb]](https://discord.gg/3qme4XHNKN)
2158
+ [![Live Chat on Discord][✉️discord-invite-img-ftb]][🖼️galtzo-discord]
2159
2159
 
2160
2160
  To say "thanks\!" ☝️ Join the Discord or 👇️ send money.
2161
2161
 
2162
- [![Sponsor kettle-rb/tree\_haver on Open Source Collective][🖇osc-all-bottom-img]](https://opencollective.com/kettle-rb) 💌 [![Sponsor me on GitHub Sponsors][🖇sponsor-bottom-img]](https://github.com/sponsors/pboling) 💌 [![Sponsor me on Liberapay][⛳liberapay-bottom-img]](https://liberapay.com/pboling/donate) 💌 [![Donate on PayPal][🖇paypal-bottom-img]](https://www.paypal.com/paypalme/peterboling)
2162
+ [![Sponsor kettle-rb/tree\_haver on Open Source Collective][🖇osc-all-bottom-img]][🖇osc] 💌 [![Sponsor me on GitHub Sponsors][🖇sponsor-bottom-img]][🖇sponsor] 💌 [![Sponsor me on Liberapay][⛳liberapay-bottom-img]][⛳liberapay] 💌 [![Donate on PayPal][🖇paypal-bottom-img]][🖇paypal]
2163
2163
 
2164
2164
  ### Please give the project a star ⭐ ♥.
2165
2165
 
@@ -2323,7 +2323,7 @@ Thanks for RTFM. ☺️
2323
2323
  [📌gitmoji]: https://gitmoji.dev
2324
2324
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
2325
2325
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
2326
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.487-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
2326
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.542-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
2327
2327
  [🔐security]: SECURITY.md
2328
2328
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
2329
2329
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -22,27 +22,44 @@ module TreeHaver
22
22
  # - **External backends** (commonmarker-merge, markly-merge, rbs-merge) register
23
23
  # their checkers when their backend module is loaded
24
24
  #
25
+ # == Full Tag Registration
26
+ #
27
+ # External gems can register complete tag support using {register_tag}:
28
+ # - Tag name (e.g., :commonmarker_backend)
29
+ # - Category (:backend, :gem, :parsing, :grammar)
30
+ # - Availability checker
31
+ # - Optional require path for lazy loading
32
+ #
33
+ # This enables tree_haver/rspec/dependency_tags to automatically configure
34
+ # RSpec exclusion filters for any registered tag without hardcoded knowledge.
35
+ #
25
36
  # == Thread Safety
26
37
  #
27
38
  # All operations are thread-safe using a Mutex for synchronization.
28
39
  # Results are cached after first check for performance.
29
40
  #
30
- # @example Registering a backend availability checker (in your gem)
41
+ # @example Registering a backend availability checker (simple form)
31
42
  # # In commonmarker-merge/lib/commonmarker/merge/backend.rb
32
43
  # TreeHaver::BackendRegistry.register_availability_checker(:commonmarker) do
33
44
  # available?
34
45
  # end
35
46
  #
47
+ # @example Registering a full tag with require path (preferred for external gems)
48
+ # TreeHaver::BackendRegistry.register_tag(
49
+ # :commonmarker_backend,
50
+ # category: :backend,
51
+ # backend_name: :commonmarker,
52
+ # require_path: "commonmarker/merge"
53
+ # ) { Commonmarker::Merge::Backend.available? }
54
+ #
36
55
  # @example Checking backend availability
37
56
  # TreeHaver::BackendRegistry.available?(:commonmarker) # => true/false
38
57
  # TreeHaver::BackendRegistry.available?(:markly) # => true/false
39
58
  # TreeHaver::BackendRegistry.available?(:rbs) # => true/false
40
59
  #
41
- # @example Checking if a checker is registered
42
- # TreeHaver::BackendRegistry.registered?(:commonmarker) # => true/false
43
- #
44
- # @example Getting all registered backends
45
- # TreeHaver::BackendRegistry.registered_backends # => [:mri, :rust, :ffi, ...]
60
+ # @example Getting all registered tags
61
+ # TreeHaver::BackendRegistry.registered_tags # => [:commonmarker_backend, :markly_backend, ...]
62
+ # TreeHaver::BackendRegistry.tags_by_category(:backend) # => [...]
46
63
  #
47
64
  # @see TreeHaver::RSpec::DependencyTags Uses BackendRegistry for dynamic backend detection
48
65
  # @api public
@@ -50,15 +67,83 @@ module TreeHaver
50
67
  @mutex = Mutex.new
51
68
  @availability_checkers = {} # rubocop:disable ThreadSafety/MutableClassInstanceVariable
52
69
  @availability_cache = {} # rubocop:disable ThreadSafety/MutableClassInstanceVariable
70
+ @tag_registry = {} # rubocop:disable ThreadSafety/MutableClassInstanceVariable
71
+
72
+ # Tag categories for organizing dependency tags
73
+ # @api private
74
+ CATEGORIES = %i[backend gem parsing grammar engine other].freeze
53
75
 
54
76
  module_function
55
77
 
56
- # Register an availability checker for a backend
78
+ # Register a full dependency tag with all metadata
79
+ #
80
+ # This is the preferred method for external gems to register their availability
81
+ # with complete tag support. It registers both the availability checker and
82
+ # the tag metadata needed for RSpec configuration.
83
+ #
84
+ # When a tag is registered, this also dynamically defines a `*_available?` method
85
+ # on `TreeHaver::RSpec::DependencyTags` if it doesn't already exist.
86
+ #
87
+ # @param tag_name [Symbol] the RSpec tag name (e.g., :commonmarker_backend)
88
+ # @param category [Symbol] one of :backend, :gem, :parsing, :grammar, :engine, :other
89
+ # @param backend_name [Symbol, nil] the backend name for availability checks (defaults to tag without suffix)
90
+ # @param require_path [String, nil] optional require path to load before checking availability
91
+ # @param checker [#call, nil] a callable that returns true if available
92
+ # @yield Block form of checker (alternative to passing a callable)
93
+ # @yieldreturn [Boolean] true if the tag's dependency is available
94
+ # @return [void]
95
+ #
96
+ # @example Register a backend tag with require path
97
+ # TreeHaver::BackendRegistry.register_tag(
98
+ # :commonmarker_backend,
99
+ # category: :backend,
100
+ # require_path: "commonmarker/merge"
101
+ # ) { Commonmarker::Merge::Backend.available? }
102
+ #
103
+ # @example Register a gem tag
104
+ # TreeHaver::BackendRegistry.register_tag(
105
+ # :toml_gem,
106
+ # category: :gem,
107
+ # require_path: "toml"
108
+ # ) { defined?(TOML) }
109
+ def register_tag(tag_name, category:, backend_name: nil, require_path: nil, checker: nil, &block)
110
+ callable = checker || block
111
+ raise ArgumentError, "Must provide a checker callable or block" unless callable
112
+ raise ArgumentError, "Checker must respond to #call" unless callable.respond_to?(:call)
113
+ raise ArgumentError, "Invalid category: #{category}" unless CATEGORIES.include?(category)
114
+
115
+ tag_sym = tag_name.to_sym
116
+ # Derive backend_name from tag_name if not provided (e.g., :commonmarker_backend -> :commonmarker)
117
+ derived_backend = backend_name || tag_sym.to_s.sub(/_backend$/, "").to_sym
118
+
119
+ @mutex.synchronize do
120
+ @tag_registry[tag_sym] = {
121
+ category: category,
122
+ backend_name: derived_backend,
123
+ require_path: require_path,
124
+ checker: callable,
125
+ }
126
+ # Also register as availability checker for the backend name
127
+ @availability_checkers[derived_backend] = callable
128
+ # Clear caches
129
+ @availability_cache.delete(derived_backend)
130
+ end
131
+
132
+ # Dynamically define the availability method on DependencyTags
133
+ # This happens outside the mutex to avoid potential deadlock
134
+ define_availability_method(derived_backend, tag_sym)
135
+
136
+ nil
137
+ end
138
+
139
+ # Register an availability checker for a backend (simple form)
57
140
  #
58
141
  # The checker should be a callable (lambda/proc/block) that returns true if
59
142
  # the backend is available and can be used. The checker is called lazily
60
143
  # (only when {available?} is first called for this backend).
61
144
  #
145
+ # For full tag support including require paths, use {register_tag} instead.
146
+ #
62
147
  # @param backend_name [Symbol, String] the backend name (e.g., :commonmarker, :markly)
63
148
  # @param checker [#call, nil] a callable that returns true if the backend is available
64
149
  # @yield Block form of checker (alternative to passing a callable)
@@ -206,10 +291,118 @@ module TreeHaver
206
291
  @mutex.synchronize do
207
292
  @availability_checkers.clear
208
293
  @availability_cache.clear
294
+ @tag_registry.clear
209
295
  end
210
296
  nil
211
297
  end
212
298
 
299
+ # ============================================================
300
+ # Tag Registry Methods
301
+ # ============================================================
302
+
303
+ # Get all registered tag names
304
+ #
305
+ # @return [Array<Symbol>] list of registered tag names
306
+ #
307
+ # @example
308
+ # TreeHaver::BackendRegistry.registered_tags
309
+ # # => [:commonmarker_backend, :markly_backend, :toml_gem, ...]
310
+ def registered_tags
311
+ @mutex.synchronize do
312
+ @tag_registry.keys.dup
313
+ end
314
+ end
315
+
316
+ # Get tags filtered by category
317
+ #
318
+ # @param category [Symbol] one of :backend, :gem, :parsing, :grammar, :engine, :other
319
+ # @return [Array<Symbol>] list of tag names in that category
320
+ #
321
+ # @example
322
+ # TreeHaver::BackendRegistry.tags_by_category(:backend)
323
+ # # => [:commonmarker_backend, :markly_backend, :mri_backend, ...]
324
+ def tags_by_category(category)
325
+ @mutex.synchronize do
326
+ @tag_registry.select { |_, meta| meta[:category] == category }.keys
327
+ end
328
+ end
329
+
330
+ # Get tag metadata
331
+ #
332
+ # @param tag_name [Symbol] the tag name
333
+ # @return [Hash, nil] tag metadata or nil if not registered
334
+ #
335
+ # @example
336
+ # TreeHaver::BackendRegistry.tag_metadata(:commonmarker_backend)
337
+ # # => { category: :backend, backend_name: :commonmarker, require_path: "commonmarker/merge", checker: #<Proc> }
338
+ def tag_metadata(tag_name)
339
+ @mutex.synchronize do
340
+ @tag_registry[tag_name.to_sym]&.dup
341
+ end
342
+ end
343
+
344
+ # Check if a tag is registered
345
+ #
346
+ # @param tag_name [Symbol] the tag name
347
+ # @return [Boolean] true if the tag is registered
348
+ def tag_registered?(tag_name)
349
+ @mutex.synchronize do
350
+ @tag_registry.key?(tag_name.to_sym)
351
+ end
352
+ end
353
+
354
+ # Check if a tag's dependency is available
355
+ #
356
+ # This method handles require paths: if the tag has a require_path, it will
357
+ # attempt to load the gem before checking availability. This enables lazy
358
+ # loading of external gems.
359
+ #
360
+ # @param tag_name [Symbol] the tag name to check
361
+ # @return [Boolean] true if the tag's dependency is available
362
+ #
363
+ # @example
364
+ # TreeHaver::BackendRegistry.tag_available?(:commonmarker_backend) # => true/false
365
+ def tag_available?(tag_name)
366
+ tag_sym = tag_name.to_sym
367
+
368
+ # Get tag metadata
369
+ meta = @mutex.synchronize { @tag_registry[tag_sym] }
370
+
371
+ # If tag not registered, check if it's a backend name with _backend suffix
372
+ unless meta
373
+ # Try to derive backend name (e.g., :commonmarker_backend -> :commonmarker)
374
+ backend_name = tag_sym.to_s.sub(/_backend$/, "").to_sym
375
+ return available?(backend_name) if backend_name != tag_sym
376
+ return false
377
+ end
378
+
379
+ # Try to load the gem if require_path is specified
380
+ if meta[:require_path]
381
+ begin
382
+ require meta[:require_path]
383
+ rescue LoadError
384
+ # Gem not available
385
+ return false
386
+ end
387
+ end
388
+
389
+ # Check availability using the backend name
390
+ available?(meta[:backend_name])
391
+ end
392
+
393
+ # Get a summary of all registered tags and their availability
394
+ #
395
+ # @return [Hash{Symbol => Boolean}] map of tag name to availability
396
+ #
397
+ # @example
398
+ # TreeHaver::BackendRegistry.tag_summary
399
+ # # => { commonmarker_backend: true, markly_backend: false, ... }
400
+ def tag_summary
401
+ @mutex.synchronize { @tag_registry.keys.dup }.each_with_object({}) do |tag, result|
402
+ result[tag] = tag_available?(tag)
403
+ end
404
+ end
405
+
213
406
  # Check a built-in TreeHaver backend
214
407
  #
215
408
  # Attempts to find the backend module at `TreeHaver::Backends::<Name>` and
@@ -230,5 +423,35 @@ module TreeHaver
230
423
  false
231
424
  end
232
425
  private_class_method :check_builtin_backend
426
+
427
+ # Dynamically define an availability method on DependencyTags
428
+ #
429
+ # This creates a `*_available?` method that checks tag_available? with
430
+ # memoization. The method is only defined if DependencyTags is loaded
431
+ # and doesn't already have a method with that name.
432
+ #
433
+ # @param backend_name [Symbol] the backend name (e.g., :commonmarker)
434
+ # @param tag_name [Symbol] the tag name (e.g., :commonmarker_backend)
435
+ # @return [void]
436
+ # @api private
437
+ def define_availability_method(backend_name, tag_name)
438
+ method_name = :"#{backend_name}_available?"
439
+
440
+ # Only define if DependencyTags is loaded
441
+ return unless defined?(TreeHaver::RSpec::DependencyTags)
442
+
443
+ deps = TreeHaver::RSpec::DependencyTags
444
+
445
+ # Don't override existing methods (built-in backends have explicit methods)
446
+ return if deps.respond_to?(method_name)
447
+
448
+ # Define the method dynamically
449
+ ivar = :"@#{backend_name}_available"
450
+ deps.define_singleton_method(method_name) do
451
+ return instance_variable_get(ivar) if instance_variable_defined?(ivar)
452
+ instance_variable_set(ivar, TreeHaver::BackendRegistry.tag_available?(tag_name))
453
+ end
454
+ end
455
+ private_class_method :define_availability_method
233
456
  end
234
457
  end
@@ -364,10 +364,12 @@ module TreeHaver
364
364
 
365
365
  case lang.backend
366
366
  when :mri
367
- lang.to_language if lang.respond_to?(:to_language)
368
- lang.inner_language if lang.respond_to?(:inner_language)
367
+ return lang.to_language if lang.respond_to?(:to_language)
368
+ return lang.inner_language if lang.respond_to?(:inner_language)
369
+ lang
369
370
  when :rust
370
- lang.name if lang.respond_to?(:name)
371
+ return lang.name if lang.respond_to?(:name)
372
+ lang
371
373
  when :ffi
372
374
  lang # FFI needs wrapper for to_ptr
373
375
  when :java
@@ -162,7 +162,7 @@ require "set"
162
162
  # ==== Language Parsing Capability Tags (*_parsing)
163
163
  #
164
164
  # [:toml_parsing]
165
- # At least one TOML parser (tree-sitter-toml OR toml-rb/Citrus) is available.
165
+ # At least one TOML parser (tree-sitter-toml OR toml-rb/Citrus OR toml/Parslet) is available.
166
166
  #
167
167
  # [:markdown_parsing]
168
168
  # At least one markdown parser (commonmarker OR markly) is available.
@@ -531,25 +531,26 @@ module TreeHaver
531
531
  nil
532
532
  end
533
533
 
534
- # Check if commonmarker gem is available
534
+ # ============================================================
535
+ # Dynamic Backend Availability (via BackendRegistry)
536
+ # ============================================================
535
537
  #
536
- # Uses BackendRegistry which allows commonmarker-merge to register its checker.
538
+ # External gems register tags with BackendRegistry.register_tag which
539
+ # dynamically defines *_available? methods on this module.
537
540
  #
538
- # @return [Boolean] true if commonmarker gem is available
539
- def commonmarker_available?
540
- return @commonmarker_available if defined?(@commonmarker_available)
541
- @commonmarker_available = TreeHaver::BackendRegistry.available?(:commonmarker)
542
- end
543
-
544
- # Check if markly gem is available
541
+ # @example External gem registers a tag
542
+ # TreeHaver::BackendRegistry.register_tag(
543
+ # :my_backend_backend,
544
+ # category: :backend,
545
+ # require_path: "my_backend/merge"
546
+ # ) { MyBackend::Merge::Backend.available? }
545
547
  #
546
- # Uses BackendRegistry which allows markly-merge to register its checker.
548
+ # # The registration automatically defines:
549
+ # TreeHaver::RSpec::DependencyTags.my_backend_available? # => true/false
547
550
  #
548
- # @return [Boolean] true if markly gem is available
549
- def markly_available?
550
- return @markly_available if defined?(@markly_available)
551
- @markly_available = TreeHaver::BackendRegistry.available?(:markly)
552
- end
551
+ # Built-in backends (prism, psych, citrus, parslet) have explicit methods
552
+ # defined below. External backends get methods defined dynamically when
553
+ # their gem calls register_tag.
553
554
 
554
555
  # Check if prism gem is available
555
556
  #
@@ -738,9 +739,13 @@ module TreeHaver
738
739
 
739
740
  # Check if at least one markdown backend is available
740
741
  #
742
+ # Uses BackendRegistry.tag_available? to check external backends that may
743
+ # not have their methods defined yet (registered by external gems).
744
+ #
741
745
  # @return [Boolean] true if any markdown backend works
742
746
  def any_markdown_backend_available?
743
- markly_available? || commonmarker_available?
747
+ TreeHaver::BackendRegistry.tag_available?(:markly_backend) ||
748
+ TreeHaver::BackendRegistry.tag_available?(:commonmarker_backend)
744
749
  end
745
750
 
746
751
  def any_native_grammar_available?
@@ -812,46 +817,69 @@ module TreeHaver
812
817
  # Use stored blocked_backends if available, otherwise compute dynamically
813
818
  blocked = @blocked_backends || compute_blocked_backends
814
819
 
815
- {
820
+ result = {
816
821
  # Backend selection from environment variables
817
822
  selected_backend: selected_backend,
818
823
  allowed_native_backends: allowed_native_backends,
819
824
  allowed_ruby_backends: allowed_ruby_backends,
820
- # TreeHaver backends (*_backend) - skip blocked backends to avoid loading them
821
- ffi_backend: blocked.include?(:ffi) ? :blocked : ffi_available?,
822
- mri_backend: blocked.include?(:mri) ? :blocked : mri_backend_available?,
823
- rust_backend: blocked.include?(:rust) ? :blocked : rust_backend_available?,
824
- java_backend: blocked.include?(:java) ? :blocked : java_backend_available?,
825
- prism_backend: blocked.include?(:prism) ? :blocked : prism_available?,
826
- psych_backend: blocked.include?(:psych) ? :blocked : psych_available?,
827
- commonmarker_backend: blocked.include?(:commonmarker) ? :blocked : commonmarker_available?,
828
- markly_backend: blocked.include?(:markly) ? :blocked : markly_available?,
829
- citrus_backend: blocked.include?(:citrus) ? :blocked : citrus_available?,
830
- parslet_backend: blocked.include?(:parslet) ? :blocked : parslet_available?,
831
- rbs_backend: blocked.include?(:rbs) ? :blocked : rbs_backend_available?,
832
- # Ruby engines (*_engine)
833
- ruby_engine: RUBY_ENGINE,
834
- mri_engine: mri?,
835
- jruby_engine: jruby?,
836
- truffleruby_engine: truffleruby?,
837
- # Tree-sitter grammars (*_grammar) - also respect blocked backends
838
- # since grammar checks may load backends
839
- libtree_sitter: libtree_sitter_available?,
840
- bash_grammar: blocked.include?(:mri) ? :blocked : tree_sitter_bash_available?,
841
- toml_grammar: blocked.include?(:mri) ? :blocked : tree_sitter_toml_available?,
842
- json_grammar: blocked.include?(:mri) ? :blocked : tree_sitter_json_available?,
843
- jsonc_grammar: blocked.include?(:mri) ? :blocked : tree_sitter_jsonc_available?,
844
- rbs_grammar: blocked.include?(:mri) ? :blocked : tree_sitter_rbs_available?,
845
- any_native_grammar: blocked.include?(:mri) ? :blocked : any_native_grammar_available?,
846
- # Language parsing capabilities (*_parsing)
847
- toml_parsing: any_toml_backend_available?,
848
- markdown_parsing: any_markdown_backend_available?,
849
- rbs_parsing: any_rbs_backend_available?,
850
- # Specific libraries (*_gem)
851
- toml_rb_gem: toml_rb_gem_available?,
852
- toml_gem: toml_gem_available?,
853
- rbs_gem: rbs_gem_available?,
854
825
  }
826
+
827
+ # Built-in TreeHaver backends (*_backend) - skip blocked backends to avoid loading them
828
+ builtin_backends = {
829
+ ffi: :ffi_available?,
830
+ mri: :mri_backend_available?,
831
+ rust: :rust_backend_available?,
832
+ java: :java_backend_available?,
833
+ prism: :prism_available?,
834
+ psych: :psych_available?,
835
+ citrus: :citrus_available?,
836
+ parslet: :parslet_available?,
837
+ rbs: :rbs_backend_available?,
838
+ }
839
+
840
+ builtin_backends.each do |backend, method|
841
+ tag = :"#{backend}_backend"
842
+ result[tag] = blocked.include?(backend) ? :blocked : public_send(method)
843
+ end
844
+
845
+ # Dynamically registered backends from BackendRegistry
846
+ TreeHaver::BackendRegistry.registered_tags.each do |tag_name|
847
+ next if result.key?(tag_name) # Don't override built-ins
848
+
849
+ meta = TreeHaver::BackendRegistry.tag_metadata(tag_name)
850
+ next unless meta && meta[:category] == :backend
851
+
852
+ backend = meta[:backend_name]
853
+ result[tag_name] = blocked.include?(backend) ? :blocked : TreeHaver::BackendRegistry.tag_available?(tag_name)
854
+ end
855
+
856
+ # Ruby engines (*_engine)
857
+ result[:ruby_engine] = RUBY_ENGINE
858
+ result[:mri_engine] = mri?
859
+ result[:jruby_engine] = jruby?
860
+ result[:truffleruby_engine] = truffleruby?
861
+
862
+ # Tree-sitter grammars (*_grammar) - also respect blocked backends
863
+ # since grammar checks may load backends
864
+ result[:libtree_sitter] = libtree_sitter_available?
865
+ result[:bash_grammar] = blocked.include?(:mri) ? :blocked : tree_sitter_bash_available?
866
+ result[:toml_grammar] = blocked.include?(:mri) ? :blocked : tree_sitter_toml_available?
867
+ result[:json_grammar] = blocked.include?(:mri) ? :blocked : tree_sitter_json_available?
868
+ result[:jsonc_grammar] = blocked.include?(:mri) ? :blocked : tree_sitter_jsonc_available?
869
+ result[:rbs_grammar] = blocked.include?(:mri) ? :blocked : tree_sitter_rbs_available?
870
+ result[:any_native_grammar] = blocked.include?(:mri) ? :blocked : any_native_grammar_available?
871
+
872
+ # Language parsing capabilities (*_parsing)
873
+ result[:toml_parsing] = any_toml_backend_available?
874
+ result[:markdown_parsing] = any_markdown_backend_available?
875
+ result[:rbs_parsing] = any_rbs_backend_available?
876
+
877
+ # Specific libraries (*_gem)
878
+ result[:toml_rb_gem] = toml_rb_gem_available?
879
+ result[:toml_gem] = toml_gem_available?
880
+ result[:rbs_gem] = rbs_gem_available?
881
+
882
+ result
855
883
  end
856
884
 
857
885
  # Get environment variable summary for debugging
@@ -907,7 +935,7 @@ module TreeHaver
907
935
  # @param test_source [String] sample source code to parse
908
936
  # @return [Boolean] true if parsing works without errors
909
937
  def grammar_works?(language, test_source)
910
- debug = ENV["TREE_HAVER_DEBUG"]
938
+ debug = !ENV.fetch("TREE_HAVER_DEBUG", "false").casecmp?("false")
911
939
  env_var = "TREE_SITTER_#{language.to_s.upcase}_PATH"
912
940
  env_value = ENV[env_var]
913
941
 
@@ -972,7 +1000,7 @@ RSpec.configure do |config|
972
1000
 
973
1001
  config.before(:suite) do
974
1002
  # Print dependency summary if TREE_HAVER_DEBUG is set
975
- if ENV["TREE_HAVER_DEBUG"]
1003
+ unless ENV.fetch("TREE_HAVER_DEBUG", "false").casecmp?("false")
976
1004
  puts "\n=== TreeHaver Environment Variables ==="
977
1005
  deps.env_summary.each do |var, value|
978
1006
  puts " #{var}: #{value.inspect}"
@@ -1063,35 +1091,35 @@ RSpec.configure do |config|
1063
1091
  #
1064
1092
  # This is dynamic based on TreeHaver::Backends::BLOCKED_BY configuration.
1065
1093
 
1066
- # Map of backend symbols to their availability check methods
1067
- backend_availability_methods = {
1068
- mri: :mri_backend_available?,
1069
- rust: :rust_backend_available?,
1070
- ffi: :ffi_available?,
1071
- java: :java_backend_available?,
1072
- prism: :prism_available?,
1073
- psych: :psych_available?,
1074
- commonmarker: :commonmarker_available?,
1075
- markly: :markly_available?,
1076
- citrus: :citrus_available?,
1077
- parslet: :parslet_available?,
1078
- rbs: :rbs_backend_available?,
1079
- }
1080
-
1081
- # Map of backend symbols to their RSpec tag names
1082
- backend_tags = {
1083
- mri: :mri_backend,
1084
- rust: :rust_backend,
1085
- ffi: :ffi_backend,
1086
- java: :java_backend,
1087
- prism: :prism_backend,
1088
- psych: :psych_backend,
1089
- commonmarker: :commonmarker_backend,
1090
- markly: :markly_backend,
1091
- citrus: :citrus_backend,
1092
- parslet: :parslet_backend,
1093
- rbs: :rbs_backend,
1094
- }
1094
+ # Build backend maps dynamically from BackendRegistry and built-in backends
1095
+ # This allows external gems to register and automatically get tag support
1096
+ backend_availability_methods = {}
1097
+ backend_tags = {}
1098
+
1099
+ # Built-in backends (always present in tree_haver)
1100
+ builtin_backends = %i[mri rust ffi java prism psych citrus parslet rbs]
1101
+ builtin_backends.each do |backend|
1102
+ # Special case for ffi which uses ffi_available? not ffi_backend_available?
1103
+ availability_method = (backend == :ffi) ? :ffi_available? : :"#{backend}_available?"
1104
+ # Special case for backends that use *_backend_available? naming
1105
+ availability_method = :"#{backend}_backend_available?" if %i[mri rust java rbs].include?(backend)
1106
+
1107
+ backend_availability_methods[backend] = availability_method
1108
+ backend_tags[backend] = :"#{backend}_backend"
1109
+ end
1110
+
1111
+ # Add dynamically registered backends from BackendRegistry
1112
+ # This picks up external gems like commonmarker-merge, markly-merge, etc.
1113
+ TreeHaver::BackendRegistry.registered_tags.each do |tag_name|
1114
+ meta = TreeHaver::BackendRegistry.tag_metadata(tag_name)
1115
+ next unless meta && meta[:category] == :backend
1116
+
1117
+ backend_name = meta[:backend_name]
1118
+ next if backend_availability_methods.key?(backend_name) # Don't override built-ins
1119
+
1120
+ backend_availability_methods[backend_name] = :"#{backend_name}_available?"
1121
+ backend_tags[backend_name] = tag_name
1122
+ end
1095
1123
 
1096
1124
  # Determine which backends should NOT have availability checked
1097
1125
  # based on which *_backend_only tag is being run OR which backend is
@@ -1219,7 +1247,7 @@ RSpec.configure do |config|
1219
1247
  # Language Parsing Capability Tags
1220
1248
  # ============================================================
1221
1249
  # Tags: *_parsing - require ANY parser for a language (any backend that can parse it)
1222
- # :toml_parsing - any TOML parser (tree-sitter-toml OR toml-rb/Citrus)
1250
+ # :toml_parsing - any TOML parser (tree-sitter-toml OR toml-rb/Citrus OR toml/Parslet)
1223
1251
  # :markdown_parsing - any Markdown parser (commonmarker OR markly)
1224
1252
  # :rbs_parsing - any RBS parser (rbs gem OR tree-sitter-rbs)
1225
1253
  # :native_parsing - any native tree-sitter backend + grammar
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ensure TreeHaver::Node and TreeHaver::Point are loaded
4
+ require "tree_haver"
5
+
6
+ module TreeHaver
7
+ module RSpec
8
+ # A mock inner node that provides the minimal interface TreeHaver::Node expects.
9
+ #
10
+ # This is what TreeHaver::Node wraps - it simulates the backend-specific node
11
+ # (like tree-sitter's Node, Markly::Node, etc.)
12
+ #
13
+ # @api private
14
+ class MockInnerNode
15
+ attr_reader :type, :start_byte, :end_byte, :children_data
16
+
17
+ def initialize(
18
+ type:,
19
+ text: nil,
20
+ start_byte: 0,
21
+ end_byte: nil,
22
+ start_row: 0,
23
+ start_column: 0,
24
+ end_row: nil,
25
+ end_column: nil,
26
+ children: []
27
+ )
28
+ @type = type.to_s
29
+ @text_content = text
30
+ @start_byte = start_byte
31
+ @end_byte = end_byte || (text ? start_byte + text.length : start_byte)
32
+ @start_row = start_row
33
+ @start_column = start_column
34
+ @end_row = end_row || start_row
35
+ @end_column = end_column || (text ? start_column + text.length : start_column)
36
+ @children_data = children
37
+ end
38
+
39
+ def start_point
40
+ TreeHaver::Point.new(@start_row, @start_column)
41
+ end
42
+
43
+ def end_point
44
+ TreeHaver::Point.new(@end_row, @end_column)
45
+ end
46
+
47
+ def child_count
48
+ @children_data.length
49
+ end
50
+
51
+ def child(index)
52
+ return if index.nil? || index < 0 || index >= @children_data.length
53
+
54
+ @children_data[index]
55
+ end
56
+
57
+ # Return children array (for enumerable behavior)
58
+ def children
59
+ @children_data
60
+ end
61
+
62
+ def first_child
63
+ @children_data.first
64
+ end
65
+
66
+ def last_child
67
+ @children_data.last
68
+ end
69
+
70
+ # Iterate over children
71
+ def each(&block)
72
+ return enum_for(:each) unless block
73
+
74
+ @children_data.each(&block)
75
+ end
76
+
77
+ def named?
78
+ true
79
+ end
80
+
81
+ # Test nodes are always valid (no parse errors)
82
+ def has_error?
83
+ false
84
+ end
85
+
86
+ # Test nodes are never missing (not error recovery insertions)
87
+ def missing?
88
+ false
89
+ end
90
+
91
+ # Some backends provide text directly
92
+ def text
93
+ @text_content
94
+ end
95
+
96
+ # For backends that use string_content (like Markly/Commonmarker)
97
+ def string_content
98
+ @text_content
99
+ end
100
+ end
101
+
102
+ # A real TreeHaver::Node that wraps a MockInnerNode.
103
+ #
104
+ # This gives us full TreeHaver::Node behavior (#text, #type, #source_position, etc.)
105
+ # while allowing us to control the underlying data for testing.
106
+ #
107
+ # TestableNode is designed for testing code that works with TreeHaver nodes
108
+ # without requiring an actual parser backend. It creates real TreeHaver::Node
109
+ # instances with controlled, predictable data.
110
+ #
111
+ # @example Creating a testable node
112
+ # node = TreeHaver::RSpec::TestableNode.create(
113
+ # type: :heading,
114
+ # text: "## My Heading",
115
+ # start_line: 1
116
+ # )
117
+ # node.text # => "## My Heading"
118
+ # node.type # => "heading"
119
+ # node.start_line # => 1
120
+ #
121
+ # @example Creating with children
122
+ # parent = TreeHaver::RSpec::TestableNode.create(
123
+ # type: :document,
124
+ # text: "# Title\n\nParagraph",
125
+ # children: [
126
+ # { type: :heading, text: "# Title", start_line: 1 },
127
+ # { type: :paragraph, text: "Paragraph", start_line: 3 },
128
+ # ]
129
+ # )
130
+ #
131
+ # @example Using the convenience constant
132
+ # # After requiring tree_haver/rspec/testable_node, you can use:
133
+ # node = TestableNode.create(type: :paragraph, text: "Hello")
134
+ #
135
+ class TestableNode < TreeHaver::Node
136
+ class << self
137
+ # Create a TestableNode with the given attributes.
138
+ #
139
+ # @param type [Symbol, String] Node type (e.g., :heading, :paragraph)
140
+ # @param text [String] The text content of the node
141
+ # @param start_line [Integer] 1-based start line number (default: 1)
142
+ # @param end_line [Integer, nil] 1-based end line number (default: calculated from text)
143
+ # @param start_column [Integer] 0-based start column (default: 0)
144
+ # @param end_column [Integer, nil] 0-based end column (default: calculated from text)
145
+ # @param start_byte [Integer] Start byte offset (default: 0)
146
+ # @param end_byte [Integer, nil] End byte offset (default: calculated from text)
147
+ # @param children [Array<Hash>] Child node specifications
148
+ # @param source [String, nil] Full source text (default: uses text param)
149
+ # @return [TestableNode]
150
+ def create(
151
+ type:,
152
+ text: "",
153
+ start_line: 1,
154
+ end_line: nil,
155
+ start_column: 0,
156
+ end_column: nil,
157
+ start_byte: 0,
158
+ end_byte: nil,
159
+ children: [],
160
+ source: nil
161
+ )
162
+ # Convert 1-based line to 0-based row
163
+ start_row = start_line - 1
164
+ end_row = end_line ? end_line - 1 : start_row + text.count("\n")
165
+
166
+ # Calculate end_column if not provided
167
+ if end_column.nil?
168
+ lines = text.split("\n", -1)
169
+ end_column = lines.last&.length || 0
170
+ end
171
+
172
+ # Build children as MockInnerNodes
173
+ child_nodes = children.map do |child_spec|
174
+ MockInnerNode.new(**child_spec)
175
+ end
176
+
177
+ inner = MockInnerNode.new(
178
+ type: type,
179
+ text: text,
180
+ start_byte: start_byte,
181
+ end_byte: end_byte,
182
+ start_row: start_row,
183
+ start_column: start_column,
184
+ end_row: end_row,
185
+ end_column: end_column,
186
+ children: child_nodes,
187
+ )
188
+
189
+ # Create a real TreeHaver::Node wrapping our mock
190
+ # Pass source so TreeHaver::Node can extract text if needed
191
+ new(inner, source: source || text)
192
+ end
193
+
194
+ # Create multiple nodes from an array of specifications.
195
+ #
196
+ # @param specs [Array<Hash>] Array of node specifications
197
+ # @return [Array<TestableNode>]
198
+ def create_list(*specs)
199
+ specs.flatten.map { |spec| create(**spec) }
200
+ end
201
+ end
202
+
203
+ # Additional test helper methods
204
+
205
+ # Check if this is a testable node (for test assertions)
206
+ #
207
+ # @return [Boolean] true
208
+ def testable?
209
+ true
210
+ end
211
+ end
212
+ end
213
+ end
214
+
215
+ # Make TestableNode available at top level for convenience in specs.
216
+ # This allows specs to use `TestableNode.create(...)` without the full namespace.
217
+ TestableNode = TreeHaver::RSpec::TestableNode
@@ -9,7 +9,7 @@
9
9
  #
10
10
  # This will load:
11
11
  # - Dependency tags for conditional test execution
12
- # - (Future) Additional test helpers as needed
12
+ # - TestableNode for creating mock nodes in tests
13
13
  #
14
14
  # @example spec_helper.rb
15
15
  # require "tree_haver/rspec"
@@ -18,6 +18,16 @@
18
18
  # # Your additional configuration...
19
19
  # end
20
20
  #
21
+ # @example Using TestableNode
22
+ # node = TestableNode.create(
23
+ # type: :heading,
24
+ # text: "## My Heading",
25
+ # start_line: 1
26
+ # )
27
+ # expect(node.type).to eq("heading")
28
+ #
21
29
  # @see TreeHaver::RSpec::DependencyTags
30
+ # @see TreeHaver::RSpec::TestableNode
22
31
 
23
32
  require_relative "rspec/dependency_tags"
33
+ require_relative "rspec/testable_node"
@@ -10,7 +10,7 @@ module TreeHaver
10
10
  # Current version of the tree_haver gem
11
11
  #
12
12
  # @return [String] the version string
13
- VERSION = "5.0.0"
13
+ VERSION = "5.0.1"
14
14
  end
15
15
 
16
16
  # Traditional location for VERSION constant
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tree_haver
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.0
4
+ version: 5.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -288,6 +288,7 @@ files:
288
288
  - lib/tree_haver/point.rb
289
289
  - lib/tree_haver/rspec.rb
290
290
  - lib/tree_haver/rspec/dependency_tags.rb
291
+ - lib/tree_haver/rspec/testable_node.rb
291
292
  - lib/tree_haver/tree.rb
292
293
  - lib/tree_haver/version.rb
293
294
  - sig/tree_haver.rbs
@@ -299,10 +300,10 @@ licenses:
299
300
  - MIT
300
301
  metadata:
301
302
  homepage_uri: https://tree-haver.galtzo.com/
302
- source_code_uri: https://github.com/kettle-rb/tree_haver/tree/v5.0.0
303
- changelog_uri: https://github.com/kettle-rb/tree_haver/blob/v5.0.0/CHANGELOG.md
303
+ source_code_uri: https://github.com/kettle-rb/tree_haver/tree/v5.0.1
304
+ changelog_uri: https://github.com/kettle-rb/tree_haver/blob/v5.0.1/CHANGELOG.md
304
305
  bug_tracker_uri: https://github.com/kettle-rb/tree_haver/issues
305
- documentation_uri: https://www.rubydoc.info/gems/tree_haver/5.0.0
306
+ documentation_uri: https://www.rubydoc.info/gems/tree_haver/5.0.1
306
307
  funding_uri: https://github.com/sponsors/pboling
307
308
  wiki_uri: https://github.com/kettle-rb/tree_haver/wiki
308
309
  news_uri: https://www.railsbling.com/tags/tree_haver
metadata.gz.sig CHANGED
Binary file