From f3be4cd5c94108bf1c4389417d3fcd0240a17011 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 1 Jun 2026 15:38:19 +0200 Subject: [PATCH] Inline mustermann-grape into Grape::Router::MustermannPattern mustermann-grape is a ~40-line Mustermann::AST::Pattern subclass that exists only for Grape's path syntax, and it had already grown Grape-specific: the Integer constraint reads Grape's own `params` option. Maintaining it as a separate gem meant a two-step release for every syntax tweak (cut a mustermann-grape release, then bump Grape's pin) plus its own CI/versioning. Move the grammar into Grape as Grape::Router::MustermannPattern (attribution to the original authors preserved), depend on `mustermann` directly, and instantiate it directly from Grape::Router::Pattern as Grape always did. The `mustermann` core dependency is unchanged; the graph collapses from grape -> mustermann-grape -> mustermann to grape -> mustermann. The class is no longer registered as a Mustermann `type: :grape` (Grape never used the registry). Apps that called `Mustermann.new(_, type: :grape)` relying on Grape pulling in mustermann-grape transitively should depend on it directly; see UPGRADING. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 1 + UPGRADING.md | 10 ++++++ grape.gemspec | 2 +- lib/grape.rb | 3 +- lib/grape/router/mustermann_pattern.rb | 44 ++++++++++++++++++++++++++ lib/grape/router/pattern.rb | 2 +- 6 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 lib/grape/router/mustermann_pattern.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a93d89c0..49ed97d6f 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). * Your contribution here. #### Fixes 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 ac917449a..8cb1c664b 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' 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' s.add_dependency 'zeitwerk' 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