Commit aa0236f
authored
feat: remove virtus dependency (#494)
## Summary
Replaces the unmaintained
[`virtus`](https://github.com/solnic/virtus#discontinued) gem with a
~90-line native `ValueObject` base class in `axe-core-api`. The
`ostruct` workaround in the gemspec — only present to keep `virtus`
working when `ostruct` exits stdlib in Ruby 3.5 — is also removed.
Both gems are dropped from every downstream lockfile (`axe-core-rspec`,
`axe-core-capybara`, `axe-core-cucumber`; `axe-core-selenium` and
`axe-core-watir` didn't reference them to begin with).
## What changed
- **`packages/axe-core-api/lib/axe/api/value_object.rb`** — replaced
`include Virtus.value_object …` with a native PORO base class
supporting:
- The existing `values do … end` DSL and `attribute :name, Type` syntax
(no call-site change in the five subclasses' declaration style).
- Virtus-compatible **lenient** type coercion for `::String`,
`::Symbol`, `::Integer`, `::Float`, `::TrueClass`/`::FalseClass`,
`ValueObject` subclasses, and `[T]` array literals. Non-coercible
strings pass through unchanged rather than raising (matches
Virtus/Coercible); boolean coercion recognizes Virtus's truthy/falsy
string sets (`%w[1 t T true TRUE]` / `%w[0 f F false FALSE]`).
- Read-only `attr_reader`s (preserves virtus's `mass_assignment: false`
semantics).
- String- and symbol-keyed hash construction; nil-tolerance at every
level (nil typed-Array → `[]`, nil scalar → `nil`).
- Attribute inheritance (`CheckedNode < Node` continues to work).
- `==`, `eql?`, `hash`, `inspect`, `[]`, `to_h`, and a `to_hash` that
dispatches dynamically to `to_h` (so subclass overrides flow through
both names).
- **Five result classes** (`results.rb`, `results/rule.rb`,
`results/check.rb`, `results/checked_node.rb`) — `::Array[T]`
(virtus-only sugar) rewritten as `[T]` literal. Internal-only; no
consumer-visible change.
- **`axe-core-api.gemspec`** — dropped `spec.add_dependency "virtus"`
and `spec.add_dependency "ostruct"` (plus its comment about the Ruby 3.5
stdlib removal).
- **Lockfiles** — surgical removal of `virtus`, `ostruct`, and
transitive deps (`axiom-types`, `coercible`, `descendants_tracker`,
`ice_nine`, `thread_safe`) for `axe-core-api`, `axe-core-rspec`,
`axe-core-capybara`, `axe-core-cucumber`. `axe-core-api` version bumped
to `4.11.3` to match `version.rb`. No `BUNDLED WITH` change, no platform
additions, no unrelated gem version changes.
- **`.gitignore`** — added `vendor/` (was missing; only added for
tidiness of local bundler installs).
## Ruby support impact
**Minimum Ruby version unchanged.** The gemspec still declares
`required_ruby_version >= 2.3.0`. The native `ValueObject` uses only
features available in Ruby 2.3+: `class << self`, `instance_eval`,
`instance_variable_set`, `attr_reader`, `each_with_object`,
`public_send`, `alias_method`, `case`/`when` with class matchers,
`Hash#key?`, `Array()` coercion, `=~` (not `String#match?`, which is
Ruby 2.4+). No `Data.define`, no pattern matching, no endless methods,
no kwarg shorthand.
**Forward compatibility improved:**
- **Ruby 3.5 / `ostruct` removal:** previously `axe-core-api` had to add
`ostruct` as a runtime dep because `virtus` pulled it in transitively.
With `virtus` gone, `ostruct` is no longer needed at all — one fewer
moving part when Ruby 3.5 ships.
- **`virtus` is unmaintained** (deprecated by its author in 2016).
Pinning to it left the gem and its consumers exposed to compatibility
breakage on each new Ruby release. The native replacement is owned by
this repo.
- **No new runtime deps.** Considered `dry-struct` + `dry-types`
(canonical virtus successor) and `anima`; both would swap one external
dep for one-or-more external deps. Given the tiny surface area used here
(no defaults, no validation, no custom coercers, no finalize hooks),
owning ~90 lines is cheaper than carrying an external library's
lifecycle.
**No consumer-visible behaviour change.** `Axe::API::Results.new(hash)`,
attribute readers, and `to_h` outputs are byte-identical to the
virtus-backed versions. Downstream gems (`axe-core-rspec`,
`axe-core-capybara`, etc.) need no changes.
## Test plan
- [x] `cd packages/axe-core-api && bundle exec rspec` — **162 examples,
0 failures, 4 pending** (preexisting "Not yet implemented" markers).
Includes 16 new `value_object_spec.rb` examples covering lenient
Integer/Float/Boolean coercion and `to_hash`/`inspect` behavior.
- [x] `spec/axe/api/results_spec.rb` exercises the hot paths:
string-keyed construction, nils at every level, nested `Rule →
CheckedNode → Check` graph instantiation, and untyped `target` accepting
`String` / `Array[String]` / `Array[Array[String]]`.
- [x] `cd packages/axe-core-rspec && bundle exec rspec` — 1/1 pass.
- [x] `cd packages/axe-core-cucumber && bundle exec rspec` — 38/38 pass.
- [x] `cd packages/axe-core-capybara && bundle exec rspec`,
`axe-core-selenium`, `axe-core-watir` — each has one preexisting failure
that requires a Chrome driver in the environment; unrelated to this
change.
- [x] `grep -RIn 'virtus\|ostruct' --include='Gemfile*'
--include='*.gemspec' --include='*.rb' .` — returns no matches.
- [ ] CI: confirm full matrix passes (the supported Ruby version matrix
is what ultimately validates the "min Ruby unchanged" claim and the
cleaned lockfiles).
## Backwards-compatibility analysis
After shipping the initial replacement, audited Virtus's public API
surface against repo usage and addressed the contract issues that
surfaced in review.
### Fixed in this PR
- **`==` symmetry / `==`/`hash` contract.** The first cut used
`other.is_a?(self.class)` which is asymmetric across subclass
hierarchies. Tightened to `instance_of?` so equality is strict on class,
matching Virtus's default value-object behavior. `hash` keys on
`self.class` to match.
- **`inspect` regression (+ empty-attribute fix).** Virtus value objects
render as `#<Axe::API::Results inapplicable=[…] violations=[…] …>`.
Restored an attribute-listing `inspect` after the initial cut;
subsequently fixed to suppress the trailing space when a value object
has no attributes (`"#<Foo>"` rather than `"#<Foo >"`).
- **`obj[name]` Hash-style attribute indexing.** Restored via a lenient
`def [](name)` on `ValueObject` (string or symbol keys; `nil` for
unknown attrs) — matches Virtus and `Hash` semantics. Caught by an
e2e/selenium spec calling `node["target"]`.
- **`obj.to_hash` preserved as dynamic-dispatch alias of `to_h`.**
Initially shipped as `alias_method :to_hash, :to_h`, which snapshots the
base-class body — so subclass `to_h` overrides (which every result class
has) did not flow through. Replaced with `def to_hash; to_h; end` so
dynamic dispatch routes calls through the subclass.
- **Integer/Float/Boolean coercion aligned with Virtus.** Initial
branches used `Integer(value)` / `Float(value)` (which raise on
non-numeric strings) and `!!value` (which treats `"false"` as truthy).
Replaced with lenient helpers that mirror Virtus/Coercible:
non-coercible strings pass through unchanged; booleans recognize
Virtus's truthy/falsy string sets.
### Disclosed divergences (no observed caller in this repo)
These were part of Virtus's value-object API surface and are not
provided by the native replacement. Verified via grep across `lib/`,
`spec/`, `e2e/`, and `features/` — no caller in this repo. External
consumers may want to speak up if they relied on any of these:
- **Default `to_h` no longer recurses through nested `ValueObject`s.**
Every `axe-core-api` subclass (`Results`, `Rule`, `Check`, `Node`,
`CheckedNode`) overrides `to_h` and recurses explicitly, so this is dead
code internally. An external consumer who subclasses
`Axe::API::ValueObject` (it's a public constant) and doesn't override
`to_h` would now get nested objects rather than hashes.
- **Removed instance/class API surface:** `obj.attributes` (instance
method), `obj.with(attrs)` (functional copy), `Klass.attribute_set` and
other Virtus introspection. None used internally — the `.to_hash` calls
that exist in the repo target `Axe::API::Options` / `Axe::API::Rules`,
which are not `ValueObject` subclasses and are unaffected.
- **Instance freezing.** Virtus's `value_object` can freeze instances
post-construction. The existing `Results#timestamp=` setter and the
`testEngine["name"] = …` mutation in `e2e/selenium/spec/api_spec.rb`
only work if instances are not frozen, so Virtus must already not have
been freezing here. The native replacement also does not freeze —
explicit preservation of behavior, but worth being clear about.
### Re-verified after fixes
- `cd packages/axe-core-api && bundle exec rspec` — 162 examples, 0
failures, 4 pending.
Closes: #4931 parent c9ac455 commit aa0236f
12 files changed
Lines changed: 276 additions & 93 deletions
File tree
- packages
- axe-core-api
- lib/axe/api
- results
- spec/axe/api
- axe-core-capybara
- axe-core-cucumber
- axe-core-rspec
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
| 20 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | | - | |
7 | 6 | | |
8 | 7 | | |
9 | 8 | | |
10 | 9 | | |
11 | 10 | | |
12 | 11 | | |
13 | 12 | | |
14 | | - | |
15 | | - | |
16 | | - | |
17 | | - | |
18 | 13 | | |
19 | 14 | | |
20 | 15 | | |
| |||
24 | 19 | | |
25 | 20 | | |
26 | 21 | | |
27 | | - | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | 22 | | |
32 | 23 | | |
33 | | - | |
34 | 24 | | |
35 | 25 | | |
36 | 26 | | |
| |||
85 | 75 | | |
86 | 76 | | |
87 | 77 | | |
88 | | - | |
89 | 78 | | |
90 | | - | |
91 | | - | |
92 | | - | |
93 | | - | |
94 | 79 | | |
95 | 80 | | |
96 | 81 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | | - | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | 27 | | |
32 | 28 | | |
33 | 29 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
10 | | - | |
11 | | - | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
| 18 | + | |
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | | - | |
| 13 | + | |
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
11 | | - | |
12 | | - | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
14 | | - | |
15 | | - | |
| 14 | + | |
| 15 | + | |
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
2 | | - | |
3 | 1 | | |
4 | 2 | | |
5 | 3 | | |
6 | | - | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
7 | 127 | | |
8 | 128 | | |
9 | 129 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
0 commit comments