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.
@@ -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