diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f088fad9..51d7e5697 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,7 @@ * [#2754](https://github.com/ruby-grape/grape/pull/2754): Merge routing args in place in `Router#process_route` instead of allocating a new Hash via `merge` - [@ericproulx](https://github.com/ericproulx). * [#2753](https://github.com/ruby-grape/grape/pull/2753): Lazy-allocate `Grape::Validations::ParamScopeTracker`'s identity-keyed hashes so validating requests that never use the index / qualifying-params trackers allocate no hash - [@ericproulx](https://github.com/ericproulx). * [#2752](https://github.com/ruby-grape/grape/pull/2752): Skip per-request `ActiveSupport::Notifications` payload and dispatch when no subscriber is listening, via private `instrument_` guards on `Endpoint`/`Middleware::Formatter` - [@ericproulx](https://github.com/ericproulx). +* [#2755](https://github.com/ruby-grape/grape/pull/2755): Inline `mustermann-grape` into `Grape::Router::MustermannPattern` and depend on `mustermann` directly - [@ericproulx](https://github.com/ericproulx). * [#2757](https://github.com/ruby-grape/grape/pull/2757): Build the `Grape::Cookies` jar only when a cookie is read or written (via a new `Grape::Request#cookies?` predicate gating response-cookie flushing), and drop the jar's now-redundant lazy-parse `Proc` - [@ericproulx](https://github.com/ericproulx). * [#2756](https://github.com/ruby-grape/grape/pull/2756): Tighten dependency lower bounds to their compatibility floors (`rack >= 2.2.4`, `zeitwerk >= 2.6`, `dry-configurable >= 1.0`) - [@ericproulx](https://github.com/ericproulx). * Your contribution here. diff --git a/UPGRADING.md b/UPGRADING.md index 8571c0445..03235066e 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -7,6 +7,16 @@ Upgrading Grape Grape no longer supports Ruby 3.2; 3.3 is now the minimum (`required_ruby_version = '>= 3.3'`). Upgrade your runtime to Ruby 3.3 or newer before bumping Grape. +#### `mustermann-grape` is no longer a dependency + +Grape's path-pattern grammar (previously the `mustermann-grape` gem) now lives in Grape itself as `Grape::Router::MustermannPattern`, and Grape depends on `mustermann` directly. This is transparent for normal Grape usage. + +The inlined class is no longer registered as a Mustermann type, so if your app called `Mustermann.new(pattern, type: :grape)` and relied on Grape loading `mustermann-grape` for you, add it to your Gemfile explicitly: + +```ruby +gem 'mustermann-grape' +``` + #### `Grape::Exceptions::ValidationErrors.new` keyword renamed `errors:` → `exceptions:` `Grape::Exceptions::ValidationErrors#initialize` now takes its input array under the `exceptions:` keyword instead of `errors:`. The kwarg accepts a mix of `Grape::Exceptions::Validation` and `Grape::Exceptions::ValidationArrayErrors` instances; `ValidationArrayErrors` wrappers are flattened internally via `flat_map(&:errors)`. The `errors` reader on the constructed instance (the grouped `{params => [Validation, ...]}` Hash) is unchanged. diff --git a/grape.gemspec b/grape.gemspec index dbfc51705..7aa0cd177 100644 --- a/grape.gemspec +++ b/grape.gemspec @@ -23,7 +23,7 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', '>= 7.2' s.add_dependency 'dry-configurable', '>= 1.0' s.add_dependency 'dry-types', '>= 1.1' - s.add_dependency 'mustermann-grape', '~> 1.1.0' + s.add_dependency 'mustermann', '>= 4.0' s.add_dependency 'rack', '>= 2.2.4' s.add_dependency 'zeitwerk', '>= 2.6' diff --git a/lib/grape.rb b/lib/grape.rb index f630cc0cb..fdd919e53 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -26,7 +26,8 @@ require 'dry-configurable' require 'forwardable' require 'json' -require 'mustermann/grape' +require 'mustermann' +require 'mustermann/ast/pattern' require 'rack' require 'rack/auth/basic' require 'rack/builder' diff --git a/lib/grape/router/mustermann_pattern.rb b/lib/grape/router/mustermann_pattern.rb new file mode 100644 index 000000000..64891ab09 --- /dev/null +++ b/lib/grape/router/mustermann_pattern.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Grape + class Router + # Grape-style path patterns for Mustermann: `:param`, `*splat`, `{name}` / + # `{+splat}`, `( )` optionals, `|`, and an Integer digit-only constraint + # (driven by Grape's `params` option). + # + # Inlined from the mustermann-grape gem (MIT) by namusyaka, Konstantin Haase + # and Daniel Doubrovkine. Grape instantiates this class directly (see + # {Grape::Router::Pattern}), so unlike the gem it is not registered as a + # Mustermann `type: :grape`. + class MustermannPattern < ::Mustermann::AST::Pattern + supported_options :params + + on(nil, '?', ')') { |c| unexpected(c) } + + on('*') { |_c| scan(/\w+/) ? node(:named_splat, buffer.matched) : node(:splat) } + on(':') do |_c| + param_name = scan(/\w+/) + # Integer params (declared via Grape's `params` option) match digits only; + # any other capture matches a single path segment (anything but / ? # .). + param_type = pattern&.options&.dig(:params, param_name, :type) + constraint = param_type == 'Integer' ? /\d/ : '[^/?#.]' + node(:capture, param_name, constraint:) { scan(/\w+/) } + end + on('\\') { |_c| node(:char, expect(/./)) } + on('(') { |_c| node(:optional, node(:group) { read unless scan(')') }) } + on('|') { |_c| node(:or) } + + on('{') do |_c| + type = scan('+') ? :named_splat : :capture + name = expect(/[\w.]+/) + type = :splat if (type == :named_splat) && (name == 'splat') + expect('}') + node(type, name) + end + + suffix('?') do |_c, element| + node(:optional, element) + end + end + end +end diff --git a/lib/grape/router/pattern.rb b/lib/grape/router/pattern.rb index 72bc9d617..503cb6488 100644 --- a/lib/grape/router/pattern.rb +++ b/lib/grape/router/pattern.rb @@ -16,7 +16,7 @@ class Pattern def initialize(origin:, suffix:, anchor:, params:, format:, version:, requirements:) @origin = origin @path = PatternCache[[build_path_from_pattern(@origin, anchor), suffix]] - @pattern = Mustermann::Grape.new(@path, uri_decode: true, params:, capture: extract_capture(format, version, requirements)) + @pattern = MustermannPattern.new(@path, uri_decode: true, params:, capture: extract_capture(format, version, requirements)) @to_regexp = @pattern.to_regexp end