mustermann 3.0.4 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +1 -2
- data/README.md +238 -261
- data/lib/mustermann/ast/compiler.rb +128 -30
- data/lib/mustermann/ast/converters.rb +41 -0
- data/lib/mustermann/ast/expander.rb +4 -5
- data/lib/mustermann/ast/fast_pattern.rb +122 -0
- data/lib/mustermann/ast/param_scanner.rb +38 -6
- data/lib/mustermann/ast/parser.rb +2 -3
- data/lib/mustermann/ast/pattern.rb +26 -4
- data/lib/mustermann/ast/transformer.rb +7 -0
- data/lib/mustermann/ast/translator.rb +11 -7
- data/lib/mustermann/composite.rb +25 -6
- data/lib/mustermann/concat.rb +16 -5
- data/lib/mustermann/error.rb +1 -0
- data/lib/mustermann/expander.rb +26 -4
- data/lib/mustermann/hybrid.rb +50 -0
- data/lib/mustermann/match.rb +155 -0
- data/lib/mustermann/pattern.rb +27 -32
- data/lib/mustermann/rails.rb +63 -0
- data/lib/mustermann/regexp_based.rb +70 -9
- data/lib/mustermann/router.rb +104 -0
- data/lib/mustermann/set/cache.rb +48 -0
- data/lib/mustermann/set/linear.rb +32 -0
- data/lib/mustermann/set/match.rb +23 -0
- data/lib/mustermann/set/strict_order.rb +29 -0
- data/lib/mustermann/set/trie.rb +270 -0
- data/lib/mustermann/set.rb +445 -0
- data/lib/mustermann/sinatra/safe_renderer.rb +1 -1
- data/lib/mustermann/sinatra/try_convert.rb +49 -11
- data/lib/mustermann/sinatra.rb +35 -11
- data/lib/mustermann/version.rb +1 -1
- data/lib/mustermann/versions.rb +47 -0
- data/lib/mustermann.rb +0 -15
- metadata +31 -45
- data/bench/capturing.rb +0 -57
- data/bench/regexp.rb +0 -21
- data/bench/simple_vs_sinatra.rb +0 -23
- data/bench/template_vs_addressable.rb +0 -26
- data/bench/uri_parser_object.rb +0 -16
- data/lib/mustermann/extension.rb +0 -3
- data/lib/mustermann/mapper.rb +0 -91
- data/lib/mustermann/pattern_cache.rb +0 -50
- data/lib/mustermann/simple_match.rb +0 -49
- data/lib/mustermann/to_pattern.rb +0 -51
- data/mustermann.gemspec +0 -18
- data/spec/ast_spec.rb +0 -15
- data/spec/composite_spec.rb +0 -163
- data/spec/concat_spec.rb +0 -127
- data/spec/equality_map_spec.rb +0 -42
- data/spec/expander_spec.rb +0 -123
- data/spec/identity_spec.rb +0 -127
- data/spec/mapper_spec.rb +0 -77
- data/spec/mustermann_spec.rb +0 -81
- data/spec/pattern_spec.rb +0 -54
- data/spec/regexp_based_spec.rb +0 -9
- data/spec/regular_spec.rb +0 -119
- data/spec/simple_match_spec.rb +0 -11
- data/spec/sinatra_spec.rb +0 -836
- data/spec/to_pattern_spec.rb +0 -70
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a1787d2b11e609844ac6a3085ca8943c1b94e9026b14f7efadded04f63f6d299
|
|
4
|
+
data.tar.gz: f8fb8748dbd82305d12b23cc4a227c210e3f748cd6e23fdb97b1126ea582c62d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5a429bddc03f3744c091f8b322c799f71276f35db5e190818ec57890012f52d4d3248c388b1db2997bd7d8c18bdb13fad58808e9961e1cd4accb832d8ddf9251
|
|
7
|
+
data.tar.gz: ac56fdcb2c30b07578ffa163af51a402190ec9ee75be89378347c2e9954caaadf942340ce15819f291bac4a1139d2e239fab4ff058df35c719777902f521620a
|
data/LICENSE
CHANGED
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# The Amazing Mustermann
|
|
2
2
|
|
|
3
|
-
*Make sure you view the correct docs: [latest release](
|
|
3
|
+
*Make sure you view the correct docs: [latest release](https://rubydoc.info/gems/mustermann/), [master](http://rubydoc.info/github/sinatra/mustermann).*
|
|
4
4
|
|
|
5
5
|
Welcome to [Mustermann](http://en.wikipedia.org/wiki/List_of_placeholder_names_by_language#German). Mustermann is your personal string matching expert. As an expert in the field of strings and patterns, Mustermann keeps its runtime dependencies to a minimum and is fully covered with specs and documentation.
|
|
6
6
|
|
|
@@ -41,8 +41,8 @@ pattern.params('/a/b.c') # => { "prefix" => "a", splat => ["b", "c"] }
|
|
|
41
41
|
|
|
42
42
|
These features are included in the library, but not loaded by default
|
|
43
43
|
|
|
44
|
-
* **[
|
|
45
|
-
* **
|
|
44
|
+
* **[Pattern Set](#-pattern-set):** A collection of patterns with associated values, designed for building routing tables that dispatch efficiently as the number of routes grows.
|
|
45
|
+
* **Mustermann::Router:** A very basic rack router built on top of `Mustermann::Set` for demonstration purposes or small-footprint routing in Rack middleware. Simple and fast.
|
|
46
46
|
|
|
47
47
|
<a name="-pattern-types"></a>
|
|
48
48
|
## Pattern Types
|
|
@@ -56,7 +56,7 @@ require 'mustermann'
|
|
|
56
56
|
pattern = Mustermann.new('/*/**', type: :shell)
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
Note that this will use the type as suggestion: When passing in a string argument, it will create a pattern of the given type, but it might choose a different type for other objects (a regular expression argument will always result in a [regexp](
|
|
59
|
+
Note that this will use the type as suggestion: When passing in a string argument, it will create a pattern of the given type, but it might choose a different type for other objects (a regular expression argument will always result in a [regexp](../docs/patterns/regexp.md) pattern, a symbol always in a [sinatra](../docs/patterns/sinatra.md) pattern, etc).
|
|
60
60
|
|
|
61
61
|
Alternatively, you can also load and instantiate the pattern type directly:
|
|
62
62
|
|
|
@@ -65,7 +65,7 @@ require 'mustermann/shell'
|
|
|
65
65
|
pattern = Mustermann::Shell.new('/*/**')
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
Mustermann itself includes the [sinatra](
|
|
68
|
+
Mustermann itself includes the [sinatra](../docs/patterns/sinatra.md), [identity](../docs/patterns/identity.md) and [regexp](../docs/patterns/regexp.md) pattern types. Other pattern types are available as separate gems.
|
|
69
69
|
|
|
70
70
|
<a name="-binary-operators"></a>
|
|
71
71
|
## Binary Operators
|
|
@@ -120,19 +120,15 @@ require 'mustermann'
|
|
|
120
120
|
|
|
121
121
|
pattern = Mustermann.new('/:page')
|
|
122
122
|
pattern.match('/') # => nil
|
|
123
|
-
pattern.match('/home') # => #<
|
|
123
|
+
pattern.match('/home') # => #<Mustermann::Match>
|
|
124
124
|
pattern =~ '/home' # => 0
|
|
125
125
|
pattern === '/home' # => true (this allows using it in case statements)
|
|
126
|
-
pattern.names # => ['page']
|
|
127
|
-
pattern.names # => {"page"=>[1]}
|
|
128
126
|
|
|
129
127
|
pattern = Mustermann.new('/home', type: :identity)
|
|
130
128
|
pattern.match('/') # => nil
|
|
131
|
-
pattern.match('/home') # => #<Mustermann::
|
|
129
|
+
pattern.match('/home') # => #<Mustermann::Match ...>
|
|
132
130
|
pattern =~ '/home' # => 0
|
|
133
131
|
pattern === '/home' # => true (this allows using it in case statements)
|
|
134
|
-
pattern.names # => []
|
|
135
|
-
pattern.names # => {}
|
|
136
132
|
```
|
|
137
133
|
|
|
138
134
|
Moreover, patterns based on regular expressions (all but `identity` and `shell`) automatically convert to regular expressions when needed:
|
|
@@ -179,7 +175,7 @@ Peeking gives the option to match a pattern against the beginning of a string ra
|
|
|
179
175
|
|
|
180
176
|
* `peek` returns the matching substring.
|
|
181
177
|
* `peek_size` returns the number of characters matching.
|
|
182
|
-
* `peek_match` will return a `
|
|
178
|
+
* `peek_match` will return a `Mustermann::Match` (just like `match` does for the full string)
|
|
183
179
|
* `peek_params` will return the `params` hash parsed from the substring and the number of characters.
|
|
184
180
|
|
|
185
181
|
All of the above will turn `nil` if there was no match.
|
|
@@ -311,7 +307,7 @@ require 'mustermann'
|
|
|
311
307
|
|
|
312
308
|
Mustermann.new("/:name").to_templates # => ["/{name}"]
|
|
313
309
|
Mustermann.new("/:foo(@:bar)?/*baz").to_templates # => ["/{foo}@{bar}/{+baz}", "/{foo}/{+baz}"]
|
|
314
|
-
Mustermann.new("/{name}", type: :template).to_templates # => ["/{name}"
|
|
310
|
+
Mustermann.new("/{name}", type: :template).to_templates # => ["/{name}"]
|
|
315
311
|
```
|
|
316
312
|
|
|
317
313
|
Union Composite patterns (with the | operator) support template generation if all patterns they are composed of also support it.
|
|
@@ -359,119 +355,165 @@ pattern = Mustermann.new(":name@:domain.:tld")
|
|
|
359
355
|
email = list.detect(&pattern) # => "example@email.com"
|
|
360
356
|
```
|
|
361
357
|
|
|
362
|
-
<a name="-
|
|
363
|
-
##
|
|
358
|
+
<a name="-pattern-set"></a>
|
|
359
|
+
## Pattern Set
|
|
364
360
|
|
|
365
|
-
|
|
366
|
-
You can use a mapper to transform strings according to two or more mappings:
|
|
361
|
+
`Mustermann::Set` is a collection of patterns where each pattern is associated with an arbitrary value — typically a handler or action. A single call to `match` returns both the captured parameters and the value for the first matching pattern, making it straightforward to build a routing table.
|
|
367
362
|
|
|
368
363
|
``` ruby
|
|
369
|
-
require 'mustermann/
|
|
364
|
+
require 'mustermann/set'
|
|
365
|
+
|
|
366
|
+
set = Mustermann::Set.new
|
|
367
|
+
set.add('/users/:id', :users_show)
|
|
368
|
+
set.add('/posts/:id', :posts_show)
|
|
369
|
+
set.add('/posts', :posts_index)
|
|
370
370
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
371
|
+
m = set.match('/users/42')
|
|
372
|
+
m.value # => :users_show
|
|
373
|
+
m.params['id'] # => '42'
|
|
374
|
+
|
|
375
|
+
set.match('/unknown') # => nil
|
|
375
376
|
```
|
|
376
377
|
|
|
377
|
-
|
|
378
|
-
## Sinatra Integration
|
|
378
|
+
You can supply the initial mapping directly to the constructor:
|
|
379
379
|
|
|
380
|
-
|
|
380
|
+
``` ruby
|
|
381
|
+
set = Mustermann::Set.new(
|
|
382
|
+
'/users/:id' => :users_show,
|
|
383
|
+
'/posts/:id' => :posts_show
|
|
384
|
+
)
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Or use a block for imperative setup:
|
|
381
388
|
|
|
382
|
-
|
|
389
|
+
``` ruby
|
|
390
|
+
set = Mustermann::Set.new do |s|
|
|
391
|
+
s.add('/users/:id', :users_show)
|
|
392
|
+
s.add('/posts/:id', :posts_show)
|
|
393
|
+
end
|
|
394
|
+
```
|
|
383
395
|
|
|
384
|
-
|
|
396
|
+
Pattern options such as `type:` are passed as keyword arguments and apply to all patterns in the set:
|
|
385
397
|
|
|
386
398
|
``` ruby
|
|
387
|
-
|
|
388
|
-
|
|
399
|
+
set = Mustermann::Set.new(type: :rails)
|
|
400
|
+
set.add('/:controller(/:action(/:id))', :route)
|
|
401
|
+
```
|
|
389
402
|
|
|
390
|
-
|
|
391
|
-
register Mustermann
|
|
392
|
-
set :pattern, type: :shell
|
|
403
|
+
### Values
|
|
393
404
|
|
|
394
|
-
|
|
395
|
-
send_file request.path_info
|
|
396
|
-
end
|
|
405
|
+
Each pattern can be associated with multiple values. `match` returns the first, while `match_all` returns one match per value:
|
|
397
406
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
407
|
+
``` ruby
|
|
408
|
+
set = Mustermann::Set.new
|
|
409
|
+
set.add('/users/:id', :admin_handler, :user_handler)
|
|
410
|
+
|
|
411
|
+
set.match('/users/1').value # => :admin_handler
|
|
412
|
+
set.match_all('/users/1').map(&:value) # => [:admin_handler, :user_handler]
|
|
402
413
|
```
|
|
403
414
|
|
|
404
|
-
|
|
415
|
+
When no value is given, a match still succeeds but `value` is `nil`:
|
|
405
416
|
|
|
406
417
|
``` ruby
|
|
407
|
-
|
|
408
|
-
|
|
418
|
+
set = Mustermann::Set.new
|
|
419
|
+
set.add('/ping')
|
|
420
|
+
set.match('/ping').value # => nil
|
|
421
|
+
```
|
|
409
422
|
|
|
410
|
-
|
|
411
|
-
set :pattern, capture: { ext: %w[png jpg html txt] }
|
|
423
|
+
### Peeking
|
|
412
424
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
425
|
+
`peek_match` matches a prefix of the input rather than the full string. The unmatched remainder is available via `post_match`:
|
|
426
|
+
|
|
427
|
+
``` ruby
|
|
428
|
+
set = Mustermann::Set.new
|
|
429
|
+
set.add('/users/:id', :users)
|
|
430
|
+
|
|
431
|
+
m = set.peek_match('/users/42/posts')
|
|
432
|
+
m.to_s # => '/users/42'
|
|
433
|
+
m.post_match # => '/posts'
|
|
434
|
+
m.value # => :users
|
|
419
435
|
```
|
|
420
436
|
|
|
421
|
-
|
|
437
|
+
`peek_match_all` returns every pattern that matches a prefix:
|
|
422
438
|
|
|
423
439
|
``` ruby
|
|
424
|
-
|
|
425
|
-
|
|
440
|
+
results = set.peek_match_all('/users/42/posts')
|
|
441
|
+
results.map(&:value) # => [:users]
|
|
442
|
+
results.map(&:post_match) # => ['/posts']
|
|
443
|
+
```
|
|
426
444
|
|
|
427
|
-
|
|
445
|
+
### Expanding
|
|
428
446
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
447
|
+
A set can generate strings from parameter hashes using the same interface as individual pattern expansion:
|
|
448
|
+
|
|
449
|
+
``` ruby
|
|
450
|
+
set = Mustermann::Set.new
|
|
451
|
+
set.add('/users/:id', :users)
|
|
452
|
+
set.add('/posts/:id', :posts)
|
|
453
|
+
|
|
454
|
+
set.expand(id: '5') # => '/users/5' (first applicable pattern)
|
|
455
|
+
set.expand(:posts, id: '5') # => '/posts/5' (patterns for a specific value)
|
|
435
456
|
```
|
|
436
457
|
|
|
437
|
-
|
|
438
|
-
Moreover, the `capture` and the `except` option can be passed to route directly.
|
|
439
|
-
And yes, this also works with `before` and `after` filters.
|
|
458
|
+
### Match order
|
|
440
459
|
|
|
441
|
-
|
|
442
|
-
require 'sinatra/base'
|
|
443
|
-
require 'sinatra/respond_with'
|
|
444
|
-
require 'mustermann'
|
|
460
|
+
A set can match patterns and values in loose or strict insertion order.
|
|
445
461
|
|
|
446
|
-
|
|
447
|
-
register Mustermann, Sinatra::RespondWith
|
|
448
|
-
set :pattern, capture: { id: /\d+/ } # id will only match digits
|
|
462
|
+
You have the following guarantees without strict ordering:
|
|
449
463
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
content_type params[:ext] # set Content-Type
|
|
453
|
-
request.path_info = params[:splat].first # drop the extension
|
|
454
|
-
end
|
|
464
|
+
* Patterns with dynamic segments in the same position and equal static parts will always match in the order they were added.
|
|
465
|
+
* Multiple values for the same pattern will retain their insertion order in regards to that pattern.
|
|
455
466
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
467
|
+
Trade-offs without strict ordering:
|
|
468
|
+
|
|
469
|
+
* Static segments may be favored over dynamic segments. If you want to guarantee this behavior, enable trie-mode proactively.
|
|
470
|
+
* When a pattern has multiple values, these will follow each other directly when using `match_all` or `peek_match_all`.
|
|
471
|
+
|
|
472
|
+
Strict ordering comes with both a performance overhead and marginally increased memory usage.
|
|
473
|
+
How big the performance overhead is depends on the number of patterns that overlap in the strings they successfully match against.
|
|
474
|
+
It does use Ruby's built-in sorting, which on MRI is based on quicksort. The memory overhead grows linear with the number
|
|
475
|
+
of pattern and value combinations, but is generally small compared to the memory used by the patterns and values themselves.
|
|
476
|
+
|
|
477
|
+
With strict ordering enabled, patterns and values are guaranteed to occur in insertion order.
|
|
478
|
+
|
|
479
|
+
Without strict ordering, not using a trie:
|
|
480
|
+
|
|
481
|
+
```ruby
|
|
482
|
+
set = Mustermann::Set.new(use_trie: false)
|
|
483
|
+
|
|
484
|
+
set.add("/:path", :first)
|
|
485
|
+
set.add("/static", :second)
|
|
486
|
+
set.add("/:path", :third)
|
|
487
|
+
|
|
488
|
+
set.match("/static").value # => :first
|
|
489
|
+
set.match_all("/static").map(&:value) # => [:first, :third, :second]
|
|
461
490
|
```
|
|
462
491
|
|
|
463
|
-
|
|
492
|
+
Without strict ordering, using a trie:
|
|
493
|
+
|
|
494
|
+
```ruby
|
|
495
|
+
set = Mustermann::Set.new(use_trie: true)
|
|
464
496
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
497
|
+
set.add("/:path", :first)
|
|
498
|
+
set.add("/static", :second)
|
|
499
|
+
set.add("/:path", :third)
|
|
500
|
+
|
|
501
|
+
set.match("/static").value # => :second
|
|
502
|
+
set.match_all("/static").map(&:value) # => [:second, :first, :third]
|
|
503
|
+
```
|
|
470
504
|
|
|
471
|
-
|
|
505
|
+
With strict ordering enabled, regardless of whether a trie is used or not:
|
|
472
506
|
|
|
473
|
-
|
|
474
|
-
|
|
507
|
+
```ruby
|
|
508
|
+
set = Mustermann::Set.new(strict_order: true)
|
|
509
|
+
|
|
510
|
+
set.add("/:path", :first)
|
|
511
|
+
set.add("/static", :second)
|
|
512
|
+
set.add("/:path", :third)
|
|
513
|
+
|
|
514
|
+
set.match("/static").value # => :first
|
|
515
|
+
set.match_all("/static").map(&:value) # => [:first, :second, :third]
|
|
516
|
+
```
|
|
475
517
|
|
|
476
518
|
<a name="-duck-typing"></a>
|
|
477
519
|
## Duck Typing
|
|
@@ -494,33 +536,6 @@ object = MyObject.new
|
|
|
494
536
|
Mustermann.new(object, type: :rails) # => #<Mustermann::Rails:"/foo">
|
|
495
537
|
```
|
|
496
538
|
|
|
497
|
-
It might also be that you want to call `to_pattern` yourself instead of `Mustermann.new`. You can load `mustermann/to_pattern` to implement this method for strings, regular expressions and pattern objects:
|
|
498
|
-
|
|
499
|
-
``` ruby
|
|
500
|
-
require 'mustermann/to_pattern'
|
|
501
|
-
|
|
502
|
-
"/foo".to_pattern # => #<Mustermann::Sinatra:"/foo">
|
|
503
|
-
"/foo".to_pattern(type: :rails) # => #<Mustermann::Rails:"/foo">
|
|
504
|
-
%r{/foo}.to_pattern # => #<Mustermann::Regular:"\\/foo">
|
|
505
|
-
"/foo".to_pattern.to_pattern # => #<Mustermann::Sinatra:"/foo">
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
You can also use the `Mustermann::ToPattern` mixin to easily add `to_pattern` to your own objects:
|
|
509
|
-
|
|
510
|
-
``` ruby
|
|
511
|
-
require 'mustermann/to_pattern'
|
|
512
|
-
|
|
513
|
-
class MyObject
|
|
514
|
-
include Mustermann::ToPattern
|
|
515
|
-
|
|
516
|
-
def to_s
|
|
517
|
-
"/foo"
|
|
518
|
-
end
|
|
519
|
-
end
|
|
520
|
-
|
|
521
|
-
MyObject.new.to_pattern # => #<Mustermann::Sinatra:"/foo">
|
|
522
|
-
```
|
|
523
|
-
|
|
524
539
|
<a name="-duck-typing-respond-to"></a>
|
|
525
540
|
### `respond_to?`
|
|
526
541
|
|
|
@@ -580,6 +595,99 @@ Mustermann.new('/:id.:ext', capture: { id: /\d+/, ext: ['png', 'jpg'] })
|
|
|
580
595
|
|
|
581
596
|
Available POSIX character classes are: `:alnum`, `:alpha`, `:blank`, `:cntrl`, `:digit`, `:graph`, `:lower`, `:print`, `:punct`, `:space`, `:upper`, `:xdigit`, `:word` and `:ascii`.
|
|
582
597
|
|
|
598
|
+
#### Typed Captures
|
|
599
|
+
|
|
600
|
+
Certain Ruby classes and named symbols can be passed as a capture value. They constrain what the capture matches **and** automatically convert the captured string in `params` to the appropriate type.
|
|
601
|
+
|
|
602
|
+
``` ruby
|
|
603
|
+
require 'mustermann'
|
|
604
|
+
require 'date'
|
|
605
|
+
|
|
606
|
+
# Integer: only matches integers, converts to Integer in params
|
|
607
|
+
pattern = Mustermann.new('/:id', capture: Integer)
|
|
608
|
+
pattern.match('/42') # matches
|
|
609
|
+
pattern.match('/foo') # does not match
|
|
610
|
+
pattern.params('/42') # => { "id" => 42 }
|
|
611
|
+
|
|
612
|
+
# Float: matches integers and decimals, converts to Float in params
|
|
613
|
+
pattern = Mustermann.new('/:price', capture: Float)
|
|
614
|
+
pattern.params('/3.14') # => { "price" => 3.14 }
|
|
615
|
+
pattern.params('/5') # => { "price" => 5.0 }
|
|
616
|
+
|
|
617
|
+
# Symbol: only matches word characters (\w+), converts to Symbol in params
|
|
618
|
+
pattern = Mustermann.new('/:format', capture: Symbol)
|
|
619
|
+
pattern.params('/json') # => { "format" => :json }
|
|
620
|
+
pattern.match('/with-hyphen') # does not match
|
|
621
|
+
|
|
622
|
+
# Date: only matches YYYY-MM-DD dates, converts to Date in params
|
|
623
|
+
pattern = Mustermann.new('/:date', capture: Date)
|
|
624
|
+
pattern.params('/2026-04-23') # => { "date" => #<Date: 2026-04-23> }
|
|
625
|
+
pattern.match('/04-23-2026') # does not match
|
|
626
|
+
|
|
627
|
+
# Gem::Version: matches version strings, converts to Gem::Version in params
|
|
628
|
+
require 'rubygems/version'
|
|
629
|
+
pattern = Mustermann.new('/:version', capture: Gem::Version)
|
|
630
|
+
pattern.params('/1.2.3') # => { "version" => #<Gem::Version "1.2.3"> }
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
Lowercase symbol aliases are also available: `:integer`, `:float`, `:symbol`, `:date`, `:version`. They behave identically to their class counterparts:
|
|
634
|
+
|
|
635
|
+
``` ruby
|
|
636
|
+
pattern = Mustermann.new('/:id', capture: :integer)
|
|
637
|
+
pattern.params('/42') # => { "id" => 42 }
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
These can be mixed with other capture types in a hash:
|
|
641
|
+
|
|
642
|
+
``` ruby
|
|
643
|
+
pattern = Mustermann.new('/:id(.:format)?', capture: { id: Integer, format: :slug })
|
|
644
|
+
pattern.params('/42') # => { "id" => 42, "format" => nil }
|
|
645
|
+
pattern.params('/42.json') # => { "id" => 42, "format" => "json" }
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
Like all other capture types, these can also be used in an array:
|
|
649
|
+
|
|
650
|
+
``` ruby
|
|
651
|
+
pattern = Mustermann.new('/score/:score', capture: [Integer, Float])
|
|
652
|
+
pattern.params('/42') # => { "score" => 42 }
|
|
653
|
+
pattern.params('/3.14') # => { "score" => 3.14 }
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
#### Other Symbols
|
|
657
|
+
|
|
658
|
+
The following symbols constrain the capture with a regex but do **not** perform any type conversion — `params` still returns a string:
|
|
659
|
+
|
|
660
|
+
| Symbol | Matches |
|
|
661
|
+
|-----------|---------|
|
|
662
|
+
| `:locale` | BCP 47 language tags (`en`, `en-US`, `zh-Hans-CN`) |
|
|
663
|
+
| `:slug` | Lowercase URL slugs (`hello-world`, `foo-bar-baz`) |
|
|
664
|
+
| `:uuid` | UUIDs (`f47ac10b-58cc-4372-a567-0e02b2c3d479`, case-insensitive) |
|
|
665
|
+
|
|
666
|
+
``` ruby
|
|
667
|
+
Mustermann.new('/:lang', capture: :locale).match('/zh-Hans-CN') # matches
|
|
668
|
+
Mustermann.new('/:slug', capture: :slug).match('/Hello') # does not match
|
|
669
|
+
Mustermann.new('/:id', capture: :uuid).match('/not-a-uuid') # does not match
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
Again, these can be mixed with other capture types in a hash or array:
|
|
673
|
+
|
|
674
|
+
```ruby
|
|
675
|
+
set = Mustermann::Set.new(capture: { id: [Integer, :uuid], locale: :locale })
|
|
676
|
+
|
|
677
|
+
set.add("(/:locale)?/:id", :show)
|
|
678
|
+
set.add("/(:locale)?", :index)
|
|
679
|
+
|
|
680
|
+
# without capture constraints, this would match the :show pattern instead
|
|
681
|
+
match = set.match('/en')
|
|
682
|
+
match.value # => :index
|
|
683
|
+
|
|
684
|
+
match = set.match('/f47ac10b-58cc-4372-a567-0e02b2c3d479')
|
|
685
|
+
match.value # => :show
|
|
686
|
+
|
|
687
|
+
match = set.match('/en/12')
|
|
688
|
+
match.value # => :show
|
|
689
|
+
```
|
|
690
|
+
|
|
583
691
|
<a name="-available-options--except"></a>
|
|
584
692
|
### `except`
|
|
585
693
|
|
|
@@ -615,10 +723,10 @@ Semi-greedy behavior is not specific to dots, it works with all characters or st
|
|
|
615
723
|
|
|
616
724
|
``` ruby
|
|
617
725
|
pattern = Mustermann.new(':a.:b', greedy: true)
|
|
618
|
-
pattern.match('a.b.c.d') # => #<
|
|
726
|
+
pattern.match('a.b.c.d') # => #<Mustermann::Match>
|
|
619
727
|
|
|
620
728
|
pattern = Mustermann.new(':a.:b', greedy: false)
|
|
621
|
-
pattern.match('a.b.c.d') # => #<
|
|
729
|
+
pattern.match('a.b.c.d') # => #<Mustermann::Match>
|
|
622
730
|
```
|
|
623
731
|
|
|
624
732
|
<a name="-available-options--space_matches_plus"></a>
|
|
@@ -679,152 +787,21 @@ By setting `ignore_unknown_options` to `true`, it will happily ignore the option
|
|
|
679
787
|
<a name="-performance"></a>
|
|
680
788
|
## Performance
|
|
681
789
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
Pattern objects should be treated as immutable. Their internals have been designed for both performance and low memory usage. To reduce pattern compilation, `Mustermann.new` and `Mustermann::Pattern.new` might return the same instance when given the same arguments, if that instance has not yet been garbage collected. However, this is not guaranteed, so do not rely on object identity.
|
|
685
|
-
|
|
686
|
-
### String Matching
|
|
687
|
-
|
|
688
|
-
When using a pattern instead of a regular expression for string matching, performance will usually be comparable.
|
|
689
|
-
|
|
690
|
-
In certain cases, Mustermann might outperform naive, equivalent regular expressions. It achieves this by using look-ahead and atomic groups in ways that work well with a backtracking, NFA-based regular expression engine (such as the Oniguruma/Onigmo engine used by Ruby). It can be difficult and error prone to construct complex regular expressions using these techniques by hand. This only applies to patterns generating an AST internally (all but [identity](#-pattern-details-identity), [shell](#-pattern-details-shell), [simple](#-pattern-details-simple) and [regexp](#-pattern-details-regexp) patterns).
|
|
691
|
-
|
|
692
|
-
When using a Mustermann pattern as a direct Regexp replacement (ie, via methods like `=~`, `match` or `===`), the overhead will be a single method dispatch, which some Ruby implementations might even eliminate with method inlining. This only applies to patterns using a regular expression internally (all but [identity](#-pattern-details-identity) and [shell](#-pattern-details-shell) patterns).
|
|
693
|
-
|
|
694
|
-
### Expanding
|
|
790
|
+
Mustermann is designed so that as much work as possible happens at object-creation time, keeping matching and expansion fast at request time. Pattern objects should be treated as immutable; their internals are optimized for both speed and low memory usage.
|
|
695
791
|
|
|
696
|
-
|
|
792
|
+
Key points:
|
|
697
793
|
|
|
698
|
-
|
|
794
|
+
* **Pattern caching:** `Mustermann.new` may return the same instance for identical arguments while that instance is still alive. Do not rely on object identity.
|
|
795
|
+
* **Single-pattern matching:** AST-based patterns (sinatra, rails, hybrid, template, flask) use bounded character classes, negative look-ahead, and non-greedy splats to avoid unnecessary backtracking in Ruby's Oniguruma engine. Using a pattern as a `Regexp` replacement adds at most one method-dispatch of overhead.
|
|
796
|
+
* **Routing with `Mustermann::Set`:** Uses a trie (prefix tree) for large tables. Rather than checking every route in sequence, the trie walks the input one character at a time, sharing prefix traversal across all patterns that start with the same characters. Dispatch time grows far more slowly than a linear scan. A `use_trie:` threshold (default 50) controls when the switch happens, and an optional `ObjectSpace::WeakKeyMap` cache avoids re-matching the same string.
|
|
797
|
+
* **Expansion:** Most computation is shifted to compile time. Memory grows linearly with the number of optional-key combinations in a pattern.
|
|
699
798
|
|
|
700
|
-
|
|
701
|
-
* Memory is sacrificed in favor of performance: The size of the expander object will grow linear with the number of possible combination for expansion keys ("/:foo/:bar" has one such combination, but "/(:foo/)?:bar?" has four)
|
|
702
|
-
* Parsing a params hash from a string generated from another params hash might not result in two identical hashes, and vice versa. Specifically, expanding ignores capture constraints, type casting and greediness.
|
|
703
|
-
* Partial expansion is (currently) not supported.
|
|
799
|
+
See **[docs/performance.md](../docs/performance.md)** for a detailed explanation of each optimization, the linear vs. trie trade-off, caching, thread-safety, and benchmark guidance.
|
|
704
800
|
|
|
705
801
|
## Details on Pattern Types
|
|
706
802
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
[`
|
|
712
|
-
[`ignore_unknown_options`](#-available-options--ignore_unknown_options).
|
|
713
|
-
|
|
714
|
-
<table>
|
|
715
|
-
<thead>
|
|
716
|
-
<tr>
|
|
717
|
-
<th>Syntax Element</th>
|
|
718
|
-
<th>Description</th>
|
|
719
|
-
</tr>
|
|
720
|
-
</thead>
|
|
721
|
-
<tbody>
|
|
722
|
-
<tr>
|
|
723
|
-
<td><i>any character</i></td>
|
|
724
|
-
<td>Matches exactly that character or a URI escaped version of it.</td>
|
|
725
|
-
</tr>
|
|
726
|
-
</tbody>
|
|
727
|
-
</table>
|
|
728
|
-
|
|
729
|
-
<a name="-regexp-pattern"></a>
|
|
730
|
-
### `regexp`
|
|
731
|
-
|
|
732
|
-
**Supported options:**
|
|
733
|
-
[`uri_decode`](#-available-options--uri_decode),
|
|
734
|
-
[`ignore_unknown_options`](#-available-options--ignore_unknown_options), `check_anchors`.
|
|
735
|
-
|
|
736
|
-
The pattern string (or actual Regexp instance) should not contain anchors (`^` outside of square brackets, `$`, `\A`, `\z`, or `\Z`).
|
|
737
|
-
Anchors will be injected where necessary by Mustermann.
|
|
738
|
-
|
|
739
|
-
By default, Mustermann will raise a `Mustermann::CompileError` if an anchor is encountered.
|
|
740
|
-
If you still want it to contain anchors at your own risk, set the `check_anchors` option to `false`.
|
|
741
|
-
|
|
742
|
-
Using anchors will break [peeking](#-peeking) and [concatenation](#-concatenation).
|
|
743
|
-
|
|
744
|
-
<table>
|
|
745
|
-
<thead>
|
|
746
|
-
<tr>
|
|
747
|
-
<th>Syntax Element</th>
|
|
748
|
-
<th>Description</th>
|
|
749
|
-
</tr>
|
|
750
|
-
</thead>
|
|
751
|
-
<tbody>
|
|
752
|
-
<tr>
|
|
753
|
-
<td><i>any string</i></td>
|
|
754
|
-
<td>Interpreted as regular expression.</td>
|
|
755
|
-
</tr>
|
|
756
|
-
</tbody>
|
|
757
|
-
</table>
|
|
758
|
-
|
|
759
|
-
<a name="-sinatra-pattern"></a>
|
|
760
|
-
### `sinatra`
|
|
761
|
-
|
|
762
|
-
**Supported options:**
|
|
763
|
-
[`capture`](#-available-options--capture),
|
|
764
|
-
[`except`](#-available-options--except),
|
|
765
|
-
[`greedy`](#-available-options--greedy),
|
|
766
|
-
[`space_matches_plus`](#-available-options--space_matches_plus),
|
|
767
|
-
[`uri_decode`](#-available-options--uri_decode),
|
|
768
|
-
[`ignore_unknown_options`](#-available-options--ignore_unknown_options).
|
|
769
|
-
|
|
770
|
-
<table>
|
|
771
|
-
<thead>
|
|
772
|
-
<tr>
|
|
773
|
-
<th>Syntax Element</th>
|
|
774
|
-
<th>Description</th>
|
|
775
|
-
</tr>
|
|
776
|
-
</thead>
|
|
777
|
-
<tbody>
|
|
778
|
-
<tr>
|
|
779
|
-
<td><b>:</b><i>name</i> <i><b>or</b></i> <b>{</b><i>name</i><b>}</b></td>
|
|
780
|
-
<td>
|
|
781
|
-
Captures anything but a forward slash in a semi-greedy fashion. Capture is named <i>name</i>.
|
|
782
|
-
Capture behavior can be modified with <tt>capture</tt> and <tt>greedy</tt> option.
|
|
783
|
-
</td>
|
|
784
|
-
</tr>
|
|
785
|
-
<tr>
|
|
786
|
-
<td><b>*</b><i>name</i> <i><b>or</b></i> <b>{+</b><i>name</i><b>}</b></td>
|
|
787
|
-
<td>
|
|
788
|
-
Captures anything in a non-greedy fashion. Capture is named <i>name</i>.
|
|
789
|
-
</td>
|
|
790
|
-
</tr>
|
|
791
|
-
<tr>
|
|
792
|
-
<td><b>*</b> <i><b>or</b></i> <b>{+splat}</b></td>
|
|
793
|
-
<td>
|
|
794
|
-
Captures anything in a non-greedy fashion. Capture is named splat.
|
|
795
|
-
It is always an array of captures, as you can use it more than once in a pattern.
|
|
796
|
-
</td>
|
|
797
|
-
</tr>
|
|
798
|
-
<tr>
|
|
799
|
-
<td><b>(</b><i>expression</i><b>)</b></td>
|
|
800
|
-
<td>
|
|
801
|
-
Enclosed <i>expression</i> is a group. Useful when combined with <tt>?</tt> to make it optional,
|
|
802
|
-
or to separate two elements that would otherwise be parsed as one.
|
|
803
|
-
</td>
|
|
804
|
-
</tr>
|
|
805
|
-
<tr>
|
|
806
|
-
<td><i>expression</i><b>|</b><i>expression</i><b>|</b><i>...</i></td>
|
|
807
|
-
<td>
|
|
808
|
-
Will match anything matching the nested expressions. May contain any other syntax element, including captures.
|
|
809
|
-
</td>
|
|
810
|
-
</tr>
|
|
811
|
-
<tr>
|
|
812
|
-
<td><i>x</i><b>?</b></td>
|
|
813
|
-
<td>Makes <i>x</i> optional. For instance, <tt>(foo)?</tt> matches <tt>foo</tt> or an empty string.</td>
|
|
814
|
-
</tr>
|
|
815
|
-
<tr>
|
|
816
|
-
<td><b>/</b></td>
|
|
817
|
-
<td>
|
|
818
|
-
Matches forward slash. Does not match URI encoded version of forward slash.
|
|
819
|
-
</td>
|
|
820
|
-
</tr>
|
|
821
|
-
<tr>
|
|
822
|
-
<td><b>\</b><i>x</i></td>
|
|
823
|
-
<td>Matches <i>x</i> or URI encoded version of <i>x</i>. For instance <tt>\*</tt> matches <tt>*</tt>.</td>
|
|
824
|
-
</tr>
|
|
825
|
-
<tr>
|
|
826
|
-
<td><i>any other character</i></td>
|
|
827
|
-
<td>Matches exactly that character or a URI encoded version of it.</td>
|
|
828
|
-
</tr>
|
|
829
|
-
</tbody>
|
|
830
|
-
</table>
|
|
803
|
+
- [`identity`](../docs/patterns/identity.md)
|
|
804
|
+
- [`regexp`](../docs/patterns/regexp.md)
|
|
805
|
+
- [`sinatra`](../docs/patterns/sinatra.md)
|
|
806
|
+
- [`rails`](../docs/patterns/rails.md)
|
|
807
|
+
- [`hybrid`](../docs/patterns/hybrid.md)
|