Skip to content
Open
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 @@ -13,6 +13,7 @@
* [#2783](https://github.com/ruby-grape/grape/pull/2783): Read a route's `version`, `anchor` and `requirements` from its pattern instead of storing them again on the route - [@ericproulx](https://github.com/ericproulx).
* [#2784](https://github.com/ruby-grape/grape/pull/2784): Move HEAD route creation into `Grape::Router::Route#to_head` - [@ericproulx](https://github.com/ericproulx).
* [#2785](https://github.com/ruby-grape/grape/pull/2785): Make route `params` a first-class endpoint input and split `Grape::Router::Route#params` into `#params` (declared definitions) and `#params_for(input)` (extracted values) - [@ericproulx](https://github.com/ericproulx).
* [#2786](https://github.com/ruby-grape/grape/pull/2786): Make route `requirements` and `anchor` explicit keyword arguments and first-class endpoint inputs instead of opaque `route_options` keys - [@ericproulx](https://github.com/ericproulx).
* Your contribution here.

#### Fixes
Expand Down
8 changes: 6 additions & 2 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ The owning API is no longer carried on the endpoint's public `options` Hash —

#### Route metadata is exposed through readers, not the `options` Hash

A route's computed metadata — `version`, `namespace`, `prefix`, `requirements`, `anchor` and `settings` — is now passed to `Grape::Router::Route` as explicit keyword arguments and exposed as plain readers, instead of being merged into the route's `options` Hash.
A route's computed metadata — `version`, `namespace`, `prefix`, `requirements`, `anchor` and `settings` — is now exposed through plain readers instead of being merged into the route's `options` Hash. `namespace`, `prefix` and `settings` are passed to `Grape::Router::Route` as explicit keyword arguments; `version`, `anchor` and `requirements` are read from the route's pattern (they shape how it matches).

The readers are unchanged — keep using them:

Expand All @@ -52,7 +52,7 @@ route.anchor # => true
route.settings # => { ... }
```

What changed is the raw bag. `route.options` now holds only what was declared for the route, so it no longer carries the *computed* values for these keys — `route.options[:version]`, `[:namespace]`, `[:prefix]` and `[:settings]` return `nil`. Nothing in Grape or grape-swagger read them that way (grape-swagger uses the `route.prefix` and `route.settings` readers). For `requirements` and `anchor`, which can be supplied as route options (e.g. a mount's `anchor: false`), any explicitly declared value still appears in `route.options`; the effective value always comes from the reader.
What changed is the raw bag. `route.options` now holds only what was declared for the route, so it no longer carries the *computed* values for these keys — `route.options[:version]`, `[:namespace]`, `[:prefix]` and `[:settings]` return `nil`. Nothing in Grape or grape-swagger read them that way (grape-swagger uses the `route.prefix` and `route.settings` readers). `requirements` and `anchor`which can be supplied as route options (e.g. a mount's `anchor: false`) — are covered separately below: they too are now first-class endpoint inputs and likewise no longer appear in `route.options`. The effective value always comes from the reader.

#### `Grape::Endpoint` no longer accepts a `format:` keyword

Expand All @@ -75,6 +75,10 @@ The no-argument form is unchanged — grape-swagger and other documentation cons

A route's declared params were previously carried inside the `route_options` bag and reachable as `route.options[:params]`. They are now composed into their own endpoint input (`Grape::Endpoint::Options` gains a `:params` member and `Grape::Endpoint.new` a `params:` keyword) and exposed only through `route.params`. `route.options[:params]` now returns `nil`. Nothing in Grape or grape-swagger read it that way — grape-swagger uses the `route.params` method — so this only affects code that reached into the options Hash for params directly.

#### `requirements` and `anchor` are first-class endpoint inputs, no longer in `route.options`

Like `params` above, a route's `requirements` and `anchor` were previously carried inside the `route_options` bag. They are now composed into their own endpoint inputs (`Grape::Endpoint::Options` gains `:requirements` and `:anchor` members, and `Grape::Endpoint.new` gains `requirements:` and `anchor:` keywords) and exposed only through the `route.requirements` and `route.anchor` readers. `route.options[:requirements]` and `route.options[:anchor]` now return `nil` — including for a mount's `anchor: false`. Nothing in Grape or grape-swagger read them that way, so this only affects code that reached into the options Hash for these keys directly.

### Upgrading to >= 3.3

#### Minimum required Ruby is now 3.3
Expand Down
16 changes: 11 additions & 5 deletions lib/grape/dsl/routing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def mount(mounts, *opts)
http_methods: :any,
path:,
app:,
route_options: { anchor: false },
anchor: false,
api: self
)
end
Expand All @@ -170,19 +170,23 @@ def mount(mounts, *opts)
#
# @param methods [HTTP Verb] One or more HTTP verbs that are accepted by this route. Set to `:any` if you want any verb to be accepted.
# @param paths [String] One or more strings representing the URL segment(s) for this route.
# @param requirements [Hash] Regular-expression constraints for named path params; the route matches only when every requirement is satisfied.
# @param anchor [Boolean] Whether the route is anchored to the whole path. Defaults to `true`; pass `false` for catch-all routes (e.g. `'/(*:path)'`).
# @param route_options [Hash] Any additional custom options, carried through to `route.options`.
#
# @example Defining a basic route.
# class MyAPI < Grape::API
# route(:any, '/hello') do
# {hello: 'world'}
# end
# end
def route(methods, paths = ['/'], route_options = {}, &)
def route(methods, paths = ['/'], requirements: nil, anchor: true, **route_options, &)
http_methods = methods == :any ? '*' : methods
endpoint_description = inheritable_setting.route[:description] || {}

# +params+ travels as its own endpoint input; the route-options bag keeps
# the description's other keys (+success+, +tags+, …) plus explicit options.
# +params+, +requirements+ and +anchor+ each travel as their own endpoint
# input; the route-options bag keeps the description's other keys
# (+success+, +tags+, …) plus any custom options.
params = prepare_params(endpoint_description[:params])
all_route_options = endpoint_description.except(:params)
all_route_options.deep_merge!(route_options) if route_options.present?
Expand All @@ -193,6 +197,8 @@ def route(methods, paths = ['/'], route_options = {}, &)
path: paths,
api: self,
params:,
requirements:,
anchor:,
route_options: all_route_options,
&
)
Expand All @@ -204,7 +210,7 @@ def route(methods, paths = ['/'], route_options = {}, &)

Grape::HTTP_SUPPORTED_METHODS.each do |supported_method|
define_method supported_method.downcase do |path = '/', **options, &block|
route(supported_method, path, options, &block)
route(supported_method, path, **options, &block)
end
end

Expand Down
12 changes: 8 additions & 4 deletions lib/grape/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,17 @@ def block_to_unbound_method(block)
# endpoint; +nil+ for a plain block endpoint. Exposed as {#mounted_app}.
# @param params [Hash] the declared params for this endpoint, keyed by name.
# Kept out of +route_options+ and read via +config.params+.
# @param requirements [Hash, nil] regular-expression constraints for named
# path params. Read via +config.requirements+.
# @param anchor [Boolean] whether the route anchors to the whole path
# (default +true+). Read via +config.anchor+.
# @param options [Hash] attributes of this endpoint, normalized into a
# +Grape::Endpoint::Options+ value object.
# @option options route_options [Hash]
# @note This happens at the time of API definition, so in this context the
# endpoint does not know if it will be mounted under a different endpoint.
# @yield a block defining what your API should do when this endpoint is hit
def initialize(new_settings, http_methods:, path:, api:, app: nil, params: {}, **options, &block)
def initialize(new_settings, http_methods:, path:, api:, app: nil, params: {}, requirements: nil, anchor: true, **options, &block)
self.inheritable_setting = new_settings.point_in_time_copy

# now +namespace_stackable(:declared_params)+ contains all params defined for
Expand All @@ -75,7 +79,7 @@ def initialize(new_settings, http_methods:, path:, api:, app: nil, params: {}, *
inheritable_setting.namespace_inheritable[:default_error_status] ||= 500

@options = options
@config = Options.new(http_methods:, path:, api:, app:, params:, **options)
@config = Options.new(http_methods:, path:, api:, app:, params:, requirements:, anchor:, **options)
# +:app+ is still surfaced on the public options Hash for backwards
# compatibility (e.g. grape-swagger); prefer the +mounted_app+ reader.
@options[:app] = app if app
Expand Down Expand Up @@ -289,8 +293,8 @@ def to_routes
forward_match = config.app && !config.app.respond_to?(:inheritable_setting)
version = prepare_version(inheritable_setting.namespace_inheritable[:version])
prefix = inheritable_setting.namespace_inheritable[:root_prefix]
requirements = prepare_routes_requirements(route_options[:requirements])
anchor = route_options.fetch(:anchor, true)
requirements = prepare_routes_requirements(config.requirements)
anchor = config.anchor
settings = inheritable_setting.route.except(:declared_params, :saved_validations)

config.http_methods.flat_map do |method|
Expand Down
4 changes: 2 additions & 2 deletions lib/grape/endpoint/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ class Endpoint
# +Grape::Endpoint.new+. Internal to {Grape::Endpoint}, which builds it
# from the +**options+ Hash in #initialize so the public +options+ reader
# stays a plain Hash for downstream gems (e.g. grape-swagger).
Options = Data.define(:path, :http_methods, :api, :route_options, :app, :params) do
def initialize(path:, http_methods:, api:, route_options: {}, app: nil, params: {})
Options = Data.define(:path, :http_methods, :api, :route_options, :app, :params, :requirements, :anchor) do
def initialize(path:, http_methods:, api:, route_options: {}, app: nil, params: {}, requirements: nil, anchor: true)
path = Array(path)
path << '/' if path.empty?
http_methods = Array(http_methods)
Expand Down
4 changes: 3 additions & 1 deletion spec/grape/dsl/routing_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,12 @@ class << self
expect(endpoint_options[:path]).to eq '/foo'
expect(endpoint_options[:api]).to eq subject
expect(endpoint_options[:params]).to eq(nuz: 'naz')
expect(endpoint_options[:requirements]).to eq(id: /\d/)
expect(endpoint_options[:anchor]).to be(false)
expect(endpoint_options[:route_options]).to eq(foo: 'bar', fiz: 'baz')
end.and_yield

subject.route(:get, '/foo', { foo: 'bar' }, &proc {})
subject.route(:get, '/foo', requirements: { id: /\d/ }, anchor: false, foo: 'bar', &proc {})
end
end

Expand Down
Loading