Skip to content

Normalize == / eql? aliasing across value-like classes#2715

Merged
ericproulx merged 2 commits into
masterfrom
chore/normalize-eql-aliases
May 14, 2026
Merged

Normalize == / eql? aliasing across value-like classes#2715
ericproulx merged 2 commits into
masterfrom
chore/normalize-eql-aliases

Conversation

@ericproulx
Copy link
Copy Markdown
Contributor

@ericproulx ericproulx commented May 14, 2026

Summary

Endpoint already followed the canonical Ruby pattern (def == then alias eql? ==), but several value-like classes either defined == without an eql? alias (so they wouldn't compare correctly as hash keys / set members), or defined the two as separate methods that drifted. Normalizing every one to the same shape:

def ==(other)
  ...
end
alias eql? ==

Equality changes

  • Grape::Middleware::Stack::Middleware — add alias eql? ==.
  • Grape::ServeStream::StreamResponse — same.
  • Grape::ServeStream::FileBody — same.
  • Grape::Util::MediaType — collapse the def ==(other); eql?(other); end trampoline + separate def eql? into a single == body with alias eql? ==. Same behaviour, one fewer dispatch.
  • Grape::Exceptions::ErrorResponse — same collapse.
  • Grape::Namespace — flip from def eql? ... alias == eql? to def == ... alias eql? == so the codebase is consistent on one direction.
  • Grape::Util::InheritableSetting — add def ==(other) (compares via to_hash internally) and alias eql? ==. The class previously had no equality at all, so Endpoint#== had to call inheritable_setting.to_hash == other.inheritable_setting.to_hash to compare. With this in place:
  • Grape::Endpoint#== simplifies from inheritable_setting.to_hash == other.inheritable_setting.to_hash to plain inheritable_setting == other.inheritable_setting.

Grape::Endpoint's own == was already canonical and remains so.

Encapsulation cleanup in InheritableSetting

While in the file, tightened the public surface — none of route, api_class, namespace, namespace_inheritable, namespace_stackable, namespace_reverse_stackable, parent, point_in_time_copies is written from outside the class anywhere in lib/ or spec/. They were attr_accessor but only ever needed attr_reader.

  • Switched attr_accessorattr_reader for all eight.
  • initialize and inherit_from now write to @ivar directly instead of the awkward self.foo = … setter syntax (matching route_end's existing @route = {}).
  • point_in_time_copy previously mutated the new instance by calling its public setters from the outside. Replaced that with a single protected copy_state_from(source) helper on InheritableSetting itself — the new instance pulls its state from source from inside the class, with no public writers required.

Why it matters

Without the alias, [obj1].include?(obj2) and Set#include? use eql? (which falls back to equal? / object identity) instead of the custom ==, producing surprising false results when two value-equal instances are compared by hash-based collections. The InheritableSetting encapsulation cleanup is a related-but-separate readability win — the class no longer leaks writable state, and internal mutations go through the canonical @ivar = … form.

Test plan

  • bundle exec rspec — 2292 examples, 0 failures
  • RuboCop clean on touched files
  • CI green across Gemfile variants

🤖 Generated with Claude Code

@ericproulx ericproulx force-pushed the chore/normalize-eql-aliases branch from 83804c7 to 3134edd Compare May 14, 2026 12:27
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 14, 2026

Danger Report

No issues found.

View run

@ericproulx ericproulx force-pushed the chore/normalize-eql-aliases branch from 3134edd to 3170e86 Compare May 14, 2026 12:31
@ericproulx ericproulx requested a review from dblock May 14, 2026 12:50
Several classes defined `==` without aliasing it to `eql?`, leaving
hash-table membership (`include?`, `Set`, `Hash` keys) inconsistent with
their custom equality. Normalize every value-like class to the same
shape: `def ==(other); ...; end; alias eql? ==`.

- middleware/stack.rb (Stack::Middleware): add `alias eql? ==`.
- serve_stream/stream_response.rb: same.
- serve_stream/file_body.rb: same.
- util/media_type.rb: collapse the `def ==(other); eql?(other); end`
  trampoline + separate `def eql?` into a single `==` body with
  `alias eql? ==`. Same behaviour, less indirection.
- exceptions/error_response.rb: same collapse.
- namespace.rb: flip from `def eql? ... alias == eql?` to
  `def == ... alias eql? ==` so the project follows one direction
  consistently.

`endpoint.rb` already used the canonical pattern; left untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ericproulx ericproulx force-pushed the chore/normalize-eql-aliases branch from 3170e86 to b058fce Compare May 14, 2026 12:52
@ericproulx ericproulx merged commit 4e2fc0f into master May 14, 2026
79 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants