Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
* [#2977](https://github.com/ruby-grape/grape/pull/2977): Stop copying the query params of a request whose params builder builds a new object - [@ericproulx](https://github.com/ericproulx).
* [#2974](https://github.com/ruby-grape/grape/pull/2974): Run every endpoint's Rack stack through one shared lambda instead of one allocated per endpoint, and make `Grape::Endpoint#run` public - [@ericproulx](https://github.com/ericproulx).
* [#2975](https://github.com/ruby-grape/grape/pull/2975): Hold a static `values` or `except_values` collection as it came instead of behind a lambda that only hands it back - [@ericproulx](https://github.com/ericproulx).
* [#2976](https://github.com/ruby-grape/grape/pull/2976): Replace a registry on registration instead of writing into it, so a registered formatter, parser, params builder, validator or versioner cannot be changed underneath a reader - [@ericproulx](https://github.com/ericproulx).
* Your contribution here.

### 4.0.1 (2026-09-15)
Expand Down
11 changes: 9 additions & 2 deletions lib/grape/util/registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def register(klass)
return if short_name.nil?

warn "#{short_name} is already registered with class #{registry[short_name]}. It will be overridden globally with the following: #{klass.name}" if registry.key?(short_name)
registry[short_name] = registry[short_name.to_sym] = klass
@registry = registry.merge(short_name => klass, short_name.to_sym => klass).freeze
end

private
Expand All @@ -29,8 +29,15 @@ def build_short_name(klass)
#
# +register+ derives the short name as a String, so the Symbol is the
# alias.
#
# Registration replaces the Hash instead of writing into it, and freezes
# the replacement: a registry is written when a formatter, parser,
# params builder, validator or versioner class is defined, and from then
# on it is only ever looked up -- on the request path, at that. Nothing
# holding a registry can find it changed underneath, and nothing can
# write to one without going through +register+.
def registry
@registry ||= {}
@registry ||= {}.freeze
end
end
end
Expand Down
13 changes: 13 additions & 0 deletions spec/grape/util/registry_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ def registry_empty?
def registry_get(key)
registry[key]
end

def registry_frozen?
registry.frozen?
end
end
end

Expand Down Expand Up @@ -83,6 +87,15 @@ def self.name
expect(subject.registry_get(:test_class)).to eq(test_class)
expect(subject.registry_get('test_class')).to eq(test_class)
end

it 'hands out a frozen registry before anything is registered' do
expect(subject.registry_frozen?).to be true
end

it 'hands out a frozen registry after registering' do
subject.register(test_class)
expect(subject.registry_frozen?).to be true
end
end

context 'with invalid class names' do
Expand Down
5 changes: 3 additions & 2 deletions spec/support/deregister.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
module Deregister
# A registration lives under both its String and its Symbol spelling
# (see Grape::Util::Registry#register), so undoing one takes both.
# Registration replaces the registry rather than writing into it, and the
# replacement is frozen, so undoing one replaces it in turn.
def deregister(key)
registry.delete(key.to_s)
registry.delete(key.to_sym)
@registry = registry.except(key.to_s, key.to_sym).freeze
end
end
Loading