mustermann 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|