mustermann 0.1.0 → 0.2.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/.travis.yml +1 -1
- data/README.md +166 -16
- data/Rakefile +1 -1
- data/bench/capturing.rb +4 -4
- data/bench/regexp.rb +21 -0
- data/lib/mustermann/ast/expander.rb +9 -3
- data/lib/mustermann/ast/node.rb +0 -1
- data/lib/mustermann/ast/parser.rb +5 -6
- data/lib/mustermann/ast/pattern.rb +17 -21
- data/lib/mustermann/ast/transformer.rb +31 -23
- data/lib/mustermann/ast/translator.rb +2 -2
- data/lib/mustermann/ast/validation.rb +8 -5
- data/lib/mustermann/caster.rb +116 -0
- data/lib/mustermann/equality_map.rb +46 -0
- data/lib/mustermann/expander.rb +169 -0
- data/lib/mustermann/pattern.rb +7 -4
- data/lib/mustermann/regexp_based.rb +2 -2
- data/lib/mustermann/router.rb +9 -0
- data/lib/mustermann/router/rack.rb +47 -0
- data/lib/mustermann/router/simple.rb +142 -0
- data/lib/mustermann/shell.rb +9 -2
- data/lib/mustermann/simple.rb +2 -2
- data/lib/mustermann/version.rb +1 -1
- data/mustermann.gemspec +1 -0
- data/spec/expander_spec.rb +77 -0
- data/spec/router/rack_spec.rb +39 -0
- data/spec/router/simple_spec.rb +30 -0
- data/spec/support/coverage.rb +12 -14
- data/spec/support/pattern.rb +10 -3
- metadata +30 -3
data/lib/mustermann/pattern.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'mustermann/error'
|
2
2
|
require 'mustermann/simple_match'
|
3
|
+
require 'mustermann/equality_map'
|
3
4
|
require 'uri'
|
4
5
|
|
5
6
|
module Mustermann
|
@@ -41,7 +42,8 @@ module Mustermann
|
|
41
42
|
raise ArgumentError, "unsupported option %p for %p" % [unsupported, self] if unsupported
|
42
43
|
end
|
43
44
|
|
44
|
-
|
45
|
+
@map ||= EqualityMap.new
|
46
|
+
@map.fetch(string, options) { super(string, options) }
|
45
47
|
end
|
46
48
|
|
47
49
|
supported_options :uri_decode, :ignore_unknown_options
|
@@ -92,7 +94,7 @@ module Mustermann
|
|
92
94
|
{}
|
93
95
|
end
|
94
96
|
|
95
|
-
# @return [Hash{String
|
97
|
+
# @return [Hash{String: Array<Integer>}] capture names mapped to capture index.
|
96
98
|
# @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-names Regexp#names
|
97
99
|
def names
|
98
100
|
[]
|
@@ -125,10 +127,11 @@ module Mustermann
|
|
125
127
|
# warn "does not support expanding"
|
126
128
|
# end
|
127
129
|
#
|
128
|
-
# @param [Hash{Symbol: #to_s, Array<#to_s>}]
|
130
|
+
# @param [Hash{Symbol: #to_s, Array<#to_s>}] values values to use for expansion
|
129
131
|
# @return [String] expanded string
|
130
132
|
# @raise [NotImplementedError] raised if expand is not supported.
|
131
|
-
# @raise [
|
133
|
+
# @raise [Mustermann::ExpandError] raised if a value is missing or unknown
|
134
|
+
# @see Mustermann::Expander
|
132
135
|
def expand(**values)
|
133
136
|
raise NotImplementedError, "expanding not supported by #{self.class}"
|
134
137
|
end
|
@@ -14,14 +14,14 @@ module Mustermann
|
|
14
14
|
# @return (see Mustermann::Pattern#initialize)
|
15
15
|
# @see (see Mustermann::Pattern#initialize)
|
16
16
|
def initialize(string, **options)
|
17
|
-
@regexp = compile(string, **options)
|
18
17
|
super
|
18
|
+
@regexp = compile(**options)
|
19
19
|
end
|
20
20
|
|
21
21
|
extend Forwardable
|
22
22
|
def_delegators :regexp, :===, :=~, :match, :names, :named_captures
|
23
23
|
|
24
|
-
def compile(
|
24
|
+
def compile(**options)
|
25
25
|
raise NotImplementedError, 'subclass responsibility'
|
26
26
|
end
|
27
27
|
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'mustermann/router/simple'
|
2
|
+
|
3
|
+
module Mustermann
|
4
|
+
module Router
|
5
|
+
# Simple pattern based router that allows matching paths to a given Rack application.
|
6
|
+
#
|
7
|
+
# @example config.ru
|
8
|
+
# router = Mustermann::Rack do
|
9
|
+
# on '/' do |env|
|
10
|
+
# [200, {'Content-Type' => 'text/plain'}, ['Hello World!']]
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# on '/:name' do |env|
|
14
|
+
# name = env['mustermann.params']['name']
|
15
|
+
# [200, {'Content-Type' => 'text/plain'}, ["Hello #{name}!"]]
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# on '/something/*', call: SomeApp
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# # in a config.ru
|
22
|
+
# run router
|
23
|
+
class Rack < Simple
|
24
|
+
def initialize(env_prefix: "mustermann", params_key: "#{env_prefix}.params", pattern_key: "#{env_prefix}.pattern", **options, &block)
|
25
|
+
@params_key, @pattern_key = params_key, pattern_key
|
26
|
+
options[:default] ||= [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass"}, ["Not Found"]]
|
27
|
+
super(**options, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def invoke(callback, env, params, pattern)
|
31
|
+
params_was, pattern_was = env[@params_key], env[@pattern_key]
|
32
|
+
env[@params_key], env[@pattern_key] = params, pattern
|
33
|
+
response = callback.call(env)
|
34
|
+
response[1].each { |k,v| throw :pass if k.downcase == 'x-cascade' and v == 'pass' }
|
35
|
+
response
|
36
|
+
ensure
|
37
|
+
env[@params_key], env[@pattern_key] = params_was, pattern_was
|
38
|
+
end
|
39
|
+
|
40
|
+
def string_for(env)
|
41
|
+
env['PATH_INFO']
|
42
|
+
end
|
43
|
+
|
44
|
+
private :invoke, :string_for
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'mustermann'
|
2
|
+
|
3
|
+
module Mustermann
|
4
|
+
module Router
|
5
|
+
# Simple pattern based router that allows matching a string to a given callback.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# require 'mustermann/router/simple'
|
9
|
+
#
|
10
|
+
# router = Mustermann::Router::Simple.new do
|
11
|
+
# on ':name/:sub' do |string, params|
|
12
|
+
# params['sub']
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# on 'foo' do
|
16
|
+
# "bar"
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# router.call("foo") # => "bar"
|
21
|
+
# router.call("a/b") # => "b"
|
22
|
+
# router.call("bar") # => nil
|
23
|
+
class Simple
|
24
|
+
# Default value for when no pattern matches
|
25
|
+
attr_accessor :default
|
26
|
+
|
27
|
+
# @example with a default value
|
28
|
+
# require 'mustermann/router/simple'
|
29
|
+
#
|
30
|
+
# router = Mustermann::Router::Simple.new(default: 42)
|
31
|
+
# router.on(':name', capture: :digit) { |string| string.to_i }
|
32
|
+
# router.call("23") # => 23
|
33
|
+
# router.call("example") # => 42
|
34
|
+
#
|
35
|
+
# @example block with implicit receiver
|
36
|
+
# require 'mustermann/router/simple'
|
37
|
+
#
|
38
|
+
# router = Mustermann::Router::Simple.new do
|
39
|
+
# on('/foo') { 'foo' }
|
40
|
+
# on('/bar') { 'bar' }
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# @example block with explicit receiver
|
44
|
+
# require 'mustermann/router/simple'
|
45
|
+
#
|
46
|
+
# router = Mustermann::Router::Simple.new(type: :rails) do |r|
|
47
|
+
# r.on('/foo') { 'foo' }
|
48
|
+
# r.on('/bar') { 'bar' }
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# @param default value to be returned if nothing matches
|
52
|
+
# @param options [Hash] pattern options
|
53
|
+
# @return [Mustermann::Router::Simple] new router instance
|
54
|
+
def initialize(default: nil, **options, &block)
|
55
|
+
@options = options
|
56
|
+
@map = []
|
57
|
+
@default = default
|
58
|
+
|
59
|
+
block.arity == 0 ? instance_eval(&block) : yield(self) if block
|
60
|
+
end
|
61
|
+
|
62
|
+
# @example
|
63
|
+
# require 'mustermann/router/simple'
|
64
|
+
#
|
65
|
+
# router = Mustermann::Router::Simple.new
|
66
|
+
# router.on(':a/:b') { 42 }
|
67
|
+
# router['foo/bar'] # => <#Proc:...>
|
68
|
+
# router['foo_bar'] # => nil
|
69
|
+
#
|
70
|
+
# @return [#call, nil] callback for given string, if a pattern matches
|
71
|
+
def [](string)
|
72
|
+
string = string_for(string) unless string.is_a? String
|
73
|
+
@map.detect { |p,v| p === string }[1]
|
74
|
+
end
|
75
|
+
|
76
|
+
# @example
|
77
|
+
# require 'mustermann/router/simple'
|
78
|
+
#
|
79
|
+
# router = Mustermann::Router::Simple.new
|
80
|
+
# router['/:name'] = proc { |string, params| params['name'] }
|
81
|
+
# router.call('/foo') # => "foo"
|
82
|
+
#
|
83
|
+
# @param pattern [String, Mustermann::Pattern] matcher
|
84
|
+
# @param callback [#call] callback to call on match
|
85
|
+
# @see #on
|
86
|
+
def []=(pattern, callback)
|
87
|
+
on(pattern, call: callback)
|
88
|
+
end
|
89
|
+
|
90
|
+
# @example with block
|
91
|
+
# require 'mustermann/router/simple'
|
92
|
+
#
|
93
|
+
# router = Mustermann::Router::Simple.new
|
94
|
+
#
|
95
|
+
# router.on(':a/:b') { 42 }
|
96
|
+
# router.call('foo/bar') # => 42
|
97
|
+
# router.call('foo_bar') # => nil
|
98
|
+
#
|
99
|
+
# @example with callback option
|
100
|
+
# require 'mustermann/router/simple'
|
101
|
+
#
|
102
|
+
# callback = proc { 42 }
|
103
|
+
# router = Mustermann::Router::Simple.new
|
104
|
+
#
|
105
|
+
# router.on(':a/:b', call: callback)
|
106
|
+
# router.call('foo/bar') # => 42
|
107
|
+
# router.call('foo_bar') # => nil
|
108
|
+
#
|
109
|
+
# @param patterns [Array<String, Pattern>]
|
110
|
+
# @param call [#call] callback object, need to hand in block if missing
|
111
|
+
# @param options [Hash] pattern options
|
112
|
+
def on(*patterns, call: Proc.new, **options)
|
113
|
+
patterns.each do |pattern|
|
114
|
+
pattern = Mustermann.new(pattern.to_str, **options, **@options) if pattern.respond_to? :to_str
|
115
|
+
@map << [pattern, call]
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Finds the matching callback and calls `call` on it with the given input and the params.
|
120
|
+
# @return the callback's return value
|
121
|
+
def call(input, &fallback)
|
122
|
+
@map.each do |pattern, callback|
|
123
|
+
catch(:pass) do
|
124
|
+
next unless params = pattern.params(string_for(input))
|
125
|
+
return invoke(callback, input, params, pattern)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
@default
|
129
|
+
end
|
130
|
+
|
131
|
+
def invoke(callback, input, params, pattern)
|
132
|
+
callback.call(input, params)
|
133
|
+
end
|
134
|
+
|
135
|
+
def string_for(input)
|
136
|
+
input.to_str
|
137
|
+
end
|
138
|
+
|
139
|
+
private :invoke, :string_for
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
data/lib/mustermann/shell.rb
CHANGED
@@ -10,13 +10,20 @@ module Mustermann
|
|
10
10
|
# @see Mustermann::Pattern
|
11
11
|
# @see file:README.md#shell Syntax description in the README
|
12
12
|
class Shell < Pattern
|
13
|
-
|
13
|
+
# @param (see Mustermann::Pattern#initialize)
|
14
|
+
# @return (see Mustermann::Pattern#initialize)
|
15
|
+
# @see (see Mustermann::Pattern#initialize)
|
16
|
+
def initialize(string, **options)
|
17
|
+
@flags = File::FNM_PATHNAME | File::FNM_DOTMATCH
|
18
|
+
@flags |= File::FNM_EXTGLOB if defined? File::FNM_EXTGLOB
|
19
|
+
super(string, **options)
|
20
|
+
end
|
14
21
|
|
15
22
|
# @param (see Mustermann::Pattern#===)
|
16
23
|
# @return (see Mustermann::Pattern#===)
|
17
24
|
# @see (see Mustermann::Pattern#===)
|
18
25
|
def ===(string)
|
19
|
-
File.fnmatch? @string, unescape(string),
|
26
|
+
File.fnmatch? @string, unescape(string), @flags
|
20
27
|
end
|
21
28
|
end
|
22
29
|
end
|
data/lib/mustermann/simple.rb
CHANGED
@@ -11,8 +11,8 @@ module Mustermann
|
|
11
11
|
class Simple < RegexpBased
|
12
12
|
supported_options :greedy, :space_matches_plus
|
13
13
|
|
14
|
-
def compile(
|
15
|
-
pattern = string.gsub(/[^\?\%\\\/\:\*\w]/) { |c| encoded(c, uri_decode, space_matches_plus) }
|
14
|
+
def compile(greedy: true, uri_decode: true, space_matches_plus: true, **options)
|
15
|
+
pattern = @string.gsub(/[^\?\%\\\/\:\*\w]/) { |c| encoded(c, uri_decode, space_matches_plus) }
|
16
16
|
pattern.gsub!(/((:\w+)|\*)/) do |match|
|
17
17
|
match == "*" ? "(?<splat>.*?)" : "(?<#{$2[1..-1]}>[^/?#]+#{?? unless greedy})"
|
18
18
|
end
|
data/lib/mustermann/version.rb
CHANGED
data/mustermann.gemspec
CHANGED
@@ -18,6 +18,7 @@ Gem::Specification.new do |s|
|
|
18
18
|
s.required_ruby_version = '>= 2.0.0'
|
19
19
|
|
20
20
|
s.add_development_dependency 'rspec'
|
21
|
+
s.add_development_dependency 'addressable'
|
21
22
|
s.add_development_dependency 'sinatra', '~> 1.4'
|
22
23
|
s.add_development_dependency 'rack-test'
|
23
24
|
s.add_development_dependency 'rake'
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'support'
|
2
|
+
require 'mustermann/expander'
|
3
|
+
|
4
|
+
describe Mustermann::Expander do
|
5
|
+
it 'expands a pattern' do
|
6
|
+
expander = Mustermann::Expander.new("/:foo.jpg")
|
7
|
+
expander.expand(foo: 42).should be == "/42.jpg"
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'expands multiple patterns' do
|
11
|
+
expander = Mustermann::Expander.new << "/:foo.:ext" << "/:foo"
|
12
|
+
expander.expand(foo: 42, ext: 'jpg').should be == "/42.jpg"
|
13
|
+
expander.expand(foo: 23).should be == "/23"
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'supports setting pattern options' do
|
17
|
+
expander = Mustermann::Expander.new(type: :rails) << "/:foo(.:ext)" << "/:bar"
|
18
|
+
expander.expand(foo: 42, ext: 'jpg').should be == "/42.jpg"
|
19
|
+
expander.expand(foo: 42).should be == "/42"
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'supports combining different pattern styles' do
|
23
|
+
expander = Mustermann::Expander.new << Mustermann.new("/:foo(.:ext)", type: :rails) << Mustermann.new("/:bar", type: :sinatra)
|
24
|
+
expander.expand(foo: 'pony', ext: 'jpg').should be == '/pony.jpg'
|
25
|
+
expander.expand(bar: 23).should be == "/23"
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'ignores nil values' do
|
29
|
+
expander = Mustermann::Expander.new << Mustermann.new("/:foo(.:ext)?")
|
30
|
+
expander.expand(foo: 'pony', ext: nil).should be == '/pony'
|
31
|
+
end
|
32
|
+
|
33
|
+
describe :additional_values do
|
34
|
+
context "illegal value" do
|
35
|
+
example { expect { Mustermann::Expander.new(additional_values: :foo) }.to raise_error(ArgumentError) }
|
36
|
+
example { expect { Mustermann::Expander.new('/').expand(:foo, a: 10) }.to raise_error(ArgumentError) }
|
37
|
+
end
|
38
|
+
|
39
|
+
context :raise do
|
40
|
+
subject(:expander) { Mustermann::Expander.new('/:a', additional_values: :raise) }
|
41
|
+
example { expander.expand(a: ?a).should be == '/a' }
|
42
|
+
example { expect { expander.expand(a: ?a, b: ?b) }.to raise_error(Mustermann::ExpandError) }
|
43
|
+
example { expect { expander.expand(b: ?b) }.to raise_error(Mustermann::ExpandError) }
|
44
|
+
end
|
45
|
+
|
46
|
+
context :ignore do
|
47
|
+
subject(:expander) { Mustermann::Expander.new('/:a', additional_values: :ignore) }
|
48
|
+
example { expander.expand(a: ?a).should be == '/a' }
|
49
|
+
example { expander.expand(a: ?a, b: ?b).should be == '/a' }
|
50
|
+
example { expect { expander.expand(b: ?b) }.to raise_error(Mustermann::ExpandError) }
|
51
|
+
end
|
52
|
+
|
53
|
+
context :append do
|
54
|
+
subject(:expander) { Mustermann::Expander.new('/:a', additional_values: :append) }
|
55
|
+
example { expander.expand(a: ?a).should be == '/a' }
|
56
|
+
example { expander.expand(a: ?a, b: ?b).should be == '/a?b=b' }
|
57
|
+
example { expect { expander.expand(b: ?b) }.to raise_error(Mustermann::ExpandError) }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe :cast do
|
62
|
+
subject(:expander) { Mustermann::Expander.new('/:a(/:b)?') }
|
63
|
+
|
64
|
+
example { expander.cast { "FOOBAR" }.expand(a: "foo") .should be == "/FOOBAR" }
|
65
|
+
example { expander.cast { |v| v.upcase }.expand(a: "foo") .should be == "/FOO" }
|
66
|
+
example { expander.cast { |v| v.upcase }.expand(a: "foo", b: "bar") .should be == "/FOO/BAR" }
|
67
|
+
example { expander.cast(:a) { |v| v.upcase }.expand(a: "foo", b: "bar") .should be == "/FOO/bar" }
|
68
|
+
example { expander.cast(:a, :b) { |v| v.upcase }.expand(a: "foo", b: "bar") .should be == "/FOO/BAR" }
|
69
|
+
example { expander.cast(Integer) { |k,v| "#{k}_#{v}" }.expand(a: "foo", b: 42) .should be == "/foo/b_42" }
|
70
|
+
|
71
|
+
example do
|
72
|
+
expander.cast(:a) { |v| v.upcase }
|
73
|
+
expander.cast(:b) { |v| v.downcase }
|
74
|
+
expander.expand(a: "fOo", b: "bAr").should be == "/FOO/bar"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'mustermann/router/rack'
|
2
|
+
|
3
|
+
describe Mustermann::Router::Rack do
|
4
|
+
include Rack::Test::Methods
|
5
|
+
subject(:app) { described_class.new }
|
6
|
+
|
7
|
+
context 'matching' do
|
8
|
+
before { app.on('/foo') { [418, {'Content-Type' => 'text/plain'}, 'bar'] } }
|
9
|
+
example { get('/foo').status.should be == 418 }
|
10
|
+
example { get('/bar').status.should be == 404 }
|
11
|
+
end
|
12
|
+
|
13
|
+
context "params" do
|
14
|
+
before { app.on('/:name') { |e| [200, {'Content-Type' => 'text/plain'}, e['mustermann.params']['name']] } }
|
15
|
+
example { get('/foo').body.should be == 'foo' }
|
16
|
+
example { get('/bar').body.should be == 'bar' }
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'X-Cascade: pass' do
|
20
|
+
before do
|
21
|
+
app.on('/') { [200, { 'X-Cascade' => 'pass' }, ['a']] }
|
22
|
+
app.on('/') { [200, { 'x-cascade' => 'pass' }, ['b']] }
|
23
|
+
app.on('/') { [200, { 'Content-Type' => 'text/plain' }, ['c']] }
|
24
|
+
app.on('/') { [200, { 'Content-Type' => 'text/plain' }, ['d']] }
|
25
|
+
end
|
26
|
+
|
27
|
+
example { get('/').body.should be == 'c' }
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'throw :pass' do
|
31
|
+
before do
|
32
|
+
app.on('/') { throw :pass }
|
33
|
+
app.on('/') { [200, { 'Content-Type' => 'text/plain' }, ['b']] }
|
34
|
+
app.on('/') { [200, { 'Content-Type' => 'text/plain' }, ['c']] }
|
35
|
+
end
|
36
|
+
|
37
|
+
example { get('/').body.should be == 'b' }
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'mustermann/router/simple'
|
2
|
+
|
3
|
+
describe Mustermann::Router::Simple do
|
4
|
+
describe :initialize do
|
5
|
+
context "with implicit receiver" do
|
6
|
+
subject(:router) { described_class.new { on('/foo') { 'bar' } } }
|
7
|
+
example { router.call('/foo').should be == 'bar' }
|
8
|
+
end
|
9
|
+
|
10
|
+
context "with explicit receiver" do
|
11
|
+
subject(:router) { described_class.new { |r| r.on('/foo') { 'bar' } } }
|
12
|
+
example { router.call('/foo').should be == 'bar' }
|
13
|
+
end
|
14
|
+
|
15
|
+
context "with default" do
|
16
|
+
subject(:router) { described_class.new(default: 'bar') }
|
17
|
+
example { router.call('/foo').should be == 'bar' }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe :[]= do
|
22
|
+
before { subject['/:name'] = proc { |*a| a } }
|
23
|
+
example { subject.call('/foo').should be == ['/foo', "name" => 'foo'] }
|
24
|
+
end
|
25
|
+
|
26
|
+
describe :[] do
|
27
|
+
before { subject.on('/x') { 42 } }
|
28
|
+
example { subject['/x'].call.should be == 42 }
|
29
|
+
end
|
30
|
+
end
|