mustermann 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- super(string, options)
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}: Array<Integer>] capture names mapped to capture index.
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>}] **values values to use for expansion
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 [ArgumentError] raised if a value is missing or unknown
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(string, **options)
24
+ def compile(**options)
25
25
  raise NotImplementedError, 'subclass responsibility'
26
26
  end
27
27
 
@@ -0,0 +1,9 @@
1
+ require 'mustermann/router/simple'
2
+ require 'mustermann/router/rack'
3
+
4
+ module Mustermann
5
+ # @see Mustermann::Router::Simple
6
+ # @see Mustermann::Router::Rack
7
+ module Router
8
+ end
9
+ end
@@ -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
@@ -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
- FLAGS ||= File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB
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), FLAGS
26
+ File.fnmatch? @string, unescape(string), @flags
20
27
  end
21
28
  end
22
29
  end
@@ -11,8 +11,8 @@ module Mustermann
11
11
  class Simple < RegexpBased
12
12
  supported_options :greedy, :space_matches_plus
13
13
 
14
- def compile(string, greedy: true, uri_decode: true, space_matches_plus: true, **options)
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
@@ -1,3 +1,3 @@
1
1
  module Mustermann
2
- VERSION ||= '0.1.0'
2
+ VERSION ||= '0.2.0'
3
3
  end
@@ -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