mustermann 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,10 +2,9 @@ require 'mustermann/ast/parser'
2
2
  require 'mustermann/ast/compiler'
3
3
  require 'mustermann/ast/transformer'
4
4
  require 'mustermann/ast/validation'
5
-
6
5
  require 'mustermann/regexp_based'
7
- require 'mustermann/equality_map'
8
6
  require 'mustermann/expander'
7
+ require 'tool/equality_map'
9
8
 
10
9
  module Mustermann
11
10
  # @see Mustermann::AST::Pattern
@@ -62,7 +61,7 @@ module Mustermann
62
61
  # Internal AST representation of pattern.
63
62
  # @!visibility private
64
63
  def to_ast
65
- @ast_cache ||= EqualityMap.new
64
+ @ast_cache ||= Tool::EqualityMap.new
66
65
  @ast_cache.fetch(@string) { validate(transform(parse(@string))) }
67
66
  end
68
67
 
@@ -93,7 +93,7 @@ module Mustermann
93
93
  #
94
94
  # @param [Array<Symbol, Regexp, #===>] type_matchers
95
95
  # To identify key/value pairs to match against.
96
- # Regexps and Symbols match againg key, everything else matches against value.
96
+ # Regexps and Symbols match against key, everything else matches against value.
97
97
  #
98
98
  # @yield every key/value pair
99
99
  # @yieldparam key [Symbol] omitted if block takes less than 2
@@ -138,7 +138,8 @@ module Mustermann
138
138
  # @raise [NotImplementedError] raised if expand is not supported.
139
139
  # @raise [Mustermann::ExpandError] raised if a value is missing or unknown
140
140
  def expand(behavior = nil, **values)
141
- values = caster.cast(values)
141
+ behavior, values = nil, behavior if behavior.is_a? Hash
142
+ values = map_values(values)
142
143
 
143
144
  case behavior || additional_values
144
145
  when :raise then @api_expander.expand(values)
@@ -148,10 +149,38 @@ module Mustermann
148
149
  end
149
150
  end
150
151
 
152
+ # @see Object#==
153
+ def ==(other)
154
+ return false unless other.class == self.class
155
+ other.patterns == patterns and other.additional_values == additional_values
156
+ end
157
+
158
+ # @see Object#eql?
159
+ def eql?(other)
160
+ return false unless other.class == self.class
161
+ other.patterns.eql? patterns and other.additional_values.eql? additional_values
162
+ end
163
+
164
+ # @see Object#hash
165
+ def hash
166
+ patterns.hash + additional_values.hash
167
+ end
168
+
169
+ def expandable?(values)
170
+ return false unless values
171
+ expandable, _ = split_values(map_values(values))
172
+ @api_expander.expandable? expandable
173
+ end
174
+
151
175
  def with_rest(values)
176
+ expandable, non_expandable = split_values(values)
177
+ yield expand(:raise, slice(values, expandable)), slice(values, non_expandable)
178
+ end
179
+
180
+ def split_values(values)
152
181
  expandable = @api_expander.expandable_keys(values.keys)
153
182
  non_expandable = values.keys - expandable
154
- yield expand(:raise, slice(values, expandable)), slice(values, non_expandable)
183
+ [expandable, non_expandable]
155
184
  end
156
185
 
157
186
  def slice(hash, keys)
@@ -164,6 +193,12 @@ module Mustermann
164
193
  "#{ uri }#{ uri[??]??&:?? }#{ entries.join(?&) }"
165
194
  end
166
195
 
167
- private :with_rest, :slice, :append, :caster
196
+ def map_values(values)
197
+ values = values.dup
198
+ @api_expander.keys.each { |key| values[key] ||= values.delete(key.to_s) if values.include? key.to_s }
199
+ caster.cast(values)
200
+ end
201
+
202
+ private :with_rest, :slice, :append, :caster, :map_values, :split_values
168
203
  end
169
204
  end
@@ -0,0 +1,94 @@
1
+ require 'mustermann'
2
+ require 'mustermann/expander'
3
+
4
+ module Mustermann
5
+ # A mapper allows mapping one string to another based on pattern parsing and expanding.
6
+ #
7
+ # @example
8
+ # require 'mustermann/mapper'
9
+ # mapper = Mustermann::Mapper.new("/:foo" => "/:foo.html")
10
+ # mapper['/example'] # => "/example.html"
11
+ class Mapper
12
+ # Creates a new mapper.
13
+ #
14
+ # @overload initialize(**options)
15
+ # @param options [Hash] options The options hash
16
+ # @yield block for generating mappings as a hash
17
+ # @yieldreturn [Hash] see {#update}
18
+ #
19
+ # @example
20
+ # require 'mustermann/mapper'
21
+ # Mustermann::Mapper.new(type: :rails) {{
22
+ # "/:foo" => ["/:foo.html", "/:foo.:format"]
23
+ # }}
24
+ #
25
+ # @overload initialize(**options)
26
+ # @param options [Hash] options The options hash
27
+ # @yield block for generating mappings as a hash
28
+ # @yieldparam mapper [Mustermann::Mapper] the mapper instance
29
+ #
30
+ # @example
31
+ # require 'mustermann/mapper'
32
+ # Mustermann::Mapper.new(type: :rails) do |mapper|
33
+ # mapper["/:foo"] = ["/:foo.html", "/:foo.:format"]
34
+ # end
35
+ #
36
+ # @overload initialize(map = {}, **options)
37
+ # @param map [Hash] see {#update}
38
+ # @param [Hash] options The options hash
39
+ #
40
+ # @example map before options
41
+ # require 'mustermann/mapper'
42
+ # Mustermann::Mapper.new("/:foo" => "/:foo.html", type: :rails)
43
+ #
44
+ # @example map after options
45
+ # require 'mustermann/mapper'
46
+ # Mustermann::Mapper.new(type: :rails, "/:foo" => "/:foo.html")
47
+ def initialize(map = {}, additional_values: :ignore, **options, &block)
48
+ @map = []
49
+ @options = options
50
+ @additional_values = additional_values
51
+ block.arity == 0 ? update(yield) : yield(self) if block
52
+ update(map) if map
53
+ end
54
+
55
+ # Add multiple mappings.
56
+ #
57
+ # @param map [Hash{String, Pattern: String, Pattern, Arry<String, Pattern>, Expander}] the mapping
58
+ def update(map)
59
+ map.to_h.each_pair do |input, output|
60
+ input = Mustermann.new(input, **@options)
61
+ output = Expander.new(*output, additional_values: @additional_values, **@options) unless output.is_a? Expander
62
+ @map << [input, output]
63
+ end
64
+ end
65
+
66
+ # @return [Hash{Patttern: Expander}] Hash version of the mapper.
67
+ def to_h
68
+ Hash[@map]
69
+ end
70
+
71
+ # Convert a string according to mappings. You can pass in additional params.
72
+ #
73
+ # @example mapping with and without additional parameters
74
+ # mapper = Mustermann::Mapper.new("/:example" => "(/:prefix)?/:example.html")
75
+ #
76
+ def convert(input, values = {})
77
+ @map.inject(input) do |current, (pattern, expander)|
78
+ params = pattern.params(current)
79
+ params &&= Hash[values.merge(params).map { |k,v| [k.to_s, v] }]
80
+ expander.expandable?(params) ? expander.expand(params) : current
81
+ end
82
+ end
83
+
84
+ # Add a single mapping.
85
+ #
86
+ # @param key [String, Pattern] format of the input string
87
+ # @param value [String, Pattern, Arry<String, Pattern>, Expander] format of the output string
88
+ def []=(key, value)
89
+ update key => value
90
+ end
91
+
92
+ alias_method :[], :convert
93
+ end
94
+ end
@@ -1,6 +1,6 @@
1
1
  require 'mustermann/error'
2
2
  require 'mustermann/simple_match'
3
- require 'mustermann/equality_map'
3
+ require 'tool/equality_map'
4
4
  require 'uri'
5
5
 
6
6
  module Mustermann
@@ -42,7 +42,7 @@ module Mustermann
42
42
  raise ArgumentError, "unsupported option %p for %p" % [unsupported, self] if unsupported
43
43
  end
44
44
 
45
- @map ||= EqualityMap.new
45
+ @map ||= Tool::EqualityMap.new
46
46
  @map.fetch(string, options) { super(string, options) }
47
47
  end
48
48
 
@@ -56,7 +56,7 @@ module Mustermann
56
56
  # @see Mustermann.new
57
57
  def initialize(string, uri_decode: true, **options)
58
58
  @uri_decode = uri_decode
59
- @string = string.dup
59
+ @string = string.to_s.dup
60
60
  end
61
61
 
62
62
  # @return [String] the string representation of the pattern
@@ -0,0 +1,26 @@
1
+ require 'mustermann/regexp_based'
2
+
3
+ module Mustermann
4
+ # Regexp pattern implementation.
5
+ #
6
+ # @example
7
+ # Mustermann.new('/.*', type: :regexp) === '/bar' # => true
8
+ #
9
+ # @see Mustermann::Pattern
10
+ # @see file:README.md#simple Syntax description in the README
11
+ class Regular < RegexpBased
12
+ # @param (see Mustermann::Pattern#initialize)
13
+ # @return (see Mustermann::Pattern#initialize)
14
+ # @see (see Mustermann::Pattern#initialize)
15
+ def initialize(string, **options)
16
+ string = $1 if string.to_s =~ /\A\(\?\-mix\:(.*)\)\Z/ && string.inspect == "/#$1/"
17
+ super(string, **options)
18
+ end
19
+
20
+ def compile(**options)
21
+ /\A#{@string}\Z/
22
+ end
23
+
24
+ private :compile
25
+ end
26
+ end
@@ -5,7 +5,7 @@ module Mustermann
5
5
  # Simple pattern based router that allows matching paths to a given Rack application.
6
6
  #
7
7
  # @example config.ru
8
- # router = Mustermann::Rack do
8
+ # router = Mustermann::Rack.new do
9
9
  # on '/' do |env|
10
10
  # [200, {'Content-Type' => 'text/plain'}, ['Hello World!']]
11
11
  # end
@@ -23,7 +23,7 @@ module Mustermann
23
23
  class Rack < Simple
24
24
  def initialize(env_prefix: "mustermann", params_key: "#{env_prefix}.params", pattern_key: "#{env_prefix}.pattern", **options, &block)
25
25
  @params_key, @pattern_key = params_key, pattern_key
26
- options[:default] ||= [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass"}, ["Not Found"]]
26
+ options[:default] = [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass"}, ["Not Found"]] unless options.include? :default
27
27
  super(**options, &block)
28
28
  end
29
29
 
@@ -118,7 +118,7 @@ module Mustermann
118
118
 
119
119
  # Finds the matching callback and calls `call` on it with the given input and the params.
120
120
  # @return the callback's return value
121
- def call(input, &fallback)
121
+ def call(input)
122
122
  @map.each do |pattern, callback|
123
123
  catch(:pass) do
124
124
  next unless params = pattern.params(string_for(input))
@@ -10,7 +10,7 @@ module Mustermann
10
10
  # @see file:README.md#sinatra Syntax description in the README
11
11
  class Sinatra < AST::Pattern
12
12
  on(nil, ??, ?)) { |c| unexpected(c) }
13
- on(?*) { |c| node(:splat) }
13
+ on(?*) { |c| scan(/\w+/) ? node(:named_splat, buffer.matched) : node(:splat) }
14
14
  on(?() { |c| node(:group) { read unless scan(?)) } }
15
15
  on(?:) { |c| node(:capture) { scan(/\w+/) } }
16
16
  on(?\\) { |c| node(:char, expect(/./)) }
@@ -0,0 +1,45 @@
1
+ require 'mustermann'
2
+
3
+ module Mustermann
4
+ # Mixin for adding {#to_pattern} ducktyping to objects.
5
+ #
6
+ # @example
7
+ # require 'mustermann/to_pattern'
8
+ #
9
+ # class Foo
10
+ # include Mustermann::ToPattern
11
+ #
12
+ # def to_s
13
+ # ":foo/:bar"
14
+ # end
15
+ # end
16
+ #
17
+ # Foo.new.to_pattern # => #<Mustermann::Sinatra:":foo/:bar">
18
+ #
19
+ # By default included into {String}, {Symbol}, {Regexp} and {Mustermann::Pattern}.
20
+ module ToPattern
21
+ # Converts the object into a {Mustermann::Pattern}.
22
+ #
23
+ # @example converting a string
24
+ # ":name.png".to_pattern # => #<Mustermann::Sinatra:":name.png">
25
+ #
26
+ # @example converting a string with options
27
+ # "/*path".to_pattern(type: :rails) # => #<Mustermann::Rails:"/*path">
28
+ #
29
+ # @example converting a regexp
30
+ # /.*/.to_pattern # => #<Mustermann::Regular:".*">
31
+ #
32
+ # @example converting a pattern
33
+ # Mustermann.new("foo").to_pattern # => #<Mustermann::Sinatra:"foo">
34
+ #
35
+ # @param [Hash] options The options hash.
36
+ # @return [Mustermann::Pattern] pattern corresponding to object.
37
+ def to_pattern(**options)
38
+ Mustermann.new(self, **options)
39
+ end
40
+
41
+ append_features String
42
+ append_features Regexp
43
+ append_features Mustermann::Pattern
44
+ end
45
+ end
@@ -1,3 +1,3 @@
1
1
  module Mustermann
2
- VERSION ||= '0.2.0'
2
+ VERSION ||= '0.3.0'
3
3
  end
@@ -17,7 +17,9 @@ Gem::Specification.new do |s|
17
17
  s.require_path = 'lib'
18
18
  s.required_ruby_version = '>= 2.0.0'
19
19
 
20
- s.add_development_dependency 'rspec'
20
+ s.add_dependency 'tool', '~> 0.2'
21
+ s.add_development_dependency 'rspec' #, '~> 2.14'
22
+ s.add_development_dependency 'rspec-its'
21
23
  s.add_development_dependency 'addressable'
22
24
  s.add_development_dependency 'sinatra', '~> 1.4'
23
25
  s.add_development_dependency 'rack-test'
@@ -74,4 +74,32 @@ describe Mustermann::Expander do
74
74
  expander.expand(a: "fOo", b: "bAr").should be == "/FOO/bar"
75
75
  end
76
76
  end
77
+
78
+ describe :== do
79
+ example { Mustermann::Expander.new('/foo') .should be == Mustermann::Expander.new('/foo') }
80
+ example { Mustermann::Expander.new('/foo') .should_not be == Mustermann::Expander.new('/bar') }
81
+ example { Mustermann::Expander.new('/foo', type: :rails) .should be == Mustermann::Expander.new('/foo', type: :rails) }
82
+ example { Mustermann::Expander.new('/foo', type: :rails) .should_not be == Mustermann::Expander.new('/foo', type: :sinatra) }
83
+ end
84
+
85
+ describe :hash do
86
+ example { Mustermann::Expander.new('/foo') .hash.should be == Mustermann::Expander.new('/foo').hash }
87
+ example { Mustermann::Expander.new('/foo') .hash.should_not be == Mustermann::Expander.new('/bar').hash }
88
+ example { Mustermann::Expander.new('/foo', type: :rails) .hash.should be == Mustermann::Expander.new('/foo', type: :rails).hash }
89
+ example { Mustermann::Expander.new('/foo', type: :rails) .hash.should_not be == Mustermann::Expander.new('/foo', type: :sinatra).hash }
90
+ end
91
+
92
+ describe :eql? do
93
+ example { Mustermann::Expander.new('/foo') .should be_eql Mustermann::Expander.new('/foo') }
94
+ example { Mustermann::Expander.new('/foo') .should_not be_eql Mustermann::Expander.new('/bar') }
95
+ example { Mustermann::Expander.new('/foo', type: :rails) .should be_eql Mustermann::Expander.new('/foo', type: :rails) }
96
+ example { Mustermann::Expander.new('/foo', type: :rails) .should_not be_eql Mustermann::Expander.new('/foo', type: :sinatra) }
97
+ end
98
+
99
+ describe :equal? do
100
+ example { Mustermann::Expander.new('/foo') .should_not be_equal Mustermann::Expander.new('/foo') }
101
+ example { Mustermann::Expander.new('/foo') .should_not be_equal Mustermann::Expander.new('/bar') }
102
+ example { Mustermann::Expander.new('/foo', type: :rails) .should_not be_equal Mustermann::Expander.new('/foo', type: :rails) }
103
+ example { Mustermann::Expander.new('/foo', type: :rails) .should_not be_equal Mustermann::Expander.new('/foo', type: :sinatra) }
104
+ end
77
105
  end
@@ -0,0 +1,83 @@
1
+ require 'support'
2
+ require 'mustermann/mapper'
3
+
4
+ describe Mustermann::Mapper do
5
+ describe :initialize do
6
+ context 'accepts a block with no arguments, using the return value' do
7
+ subject(:mapper) { Mustermann::Mapper.new(additional_values: :raise) {{ "/foo" => "/bar" }}}
8
+ its(:to_h) { should be == { Mustermann.new("/foo") => Mustermann::Expander.new("/bar") } }
9
+ example { mapper['/foo'].should be == '/bar' }
10
+ example { mapper['/fox'].should be == '/fox' }
11
+ end
12
+
13
+ context 'accepts a block with argument, passes instance to it' do
14
+ subject(:mapper) { Mustermann::Mapper.new(additional_values: :raise) { |m| m["/foo"] = "/bar" }}
15
+ its(:to_h) { should be == { Mustermann.new("/foo") => Mustermann::Expander.new("/bar") } }
16
+ example { mapper['/foo'].should be == '/bar' }
17
+ example { mapper['/fox'].should be == '/fox' }
18
+ end
19
+
20
+ context 'accepts mappings followed by options' do
21
+ subject(:mapper) { Mustermann::Mapper.new("/foo" => "/bar", additional_values: :raise) }
22
+ its(:to_h) { should be == { Mustermann.new("/foo") => Mustermann::Expander.new("/bar") } }
23
+ example { mapper['/foo'].should be == '/bar' }
24
+ example { mapper['/fox'].should be == '/fox' }
25
+ end
26
+
27
+ context 'accepts options followed by mappings' do
28
+ subject(:mapper) { Mustermann::Mapper.new(additional_values: :raise, "/foo" => "/bar") }
29
+ its(:to_h) { should be == { Mustermann.new("/foo") => Mustermann::Expander.new("/bar") } }
30
+ example { mapper['/foo'].should be == '/bar' }
31
+ example { mapper['/fox'].should be == '/fox' }
32
+ end
33
+
34
+ context 'allows specifying type' do
35
+ subject(:mapper) { Mustermann::Mapper.new(additional_values: :raise, type: :rails, "/foo" => "/bar") }
36
+ its(:to_h) { should be == { Mustermann.new("/foo", type: :rails) => Mustermann::Expander.new("/bar", type: :rails) } }
37
+ example { mapper['/foo'].should be == '/bar' }
38
+ example { mapper['/fox'].should be == '/fox' }
39
+ end
40
+ end
41
+
42
+ describe :convert do
43
+ subject(:mapper) { Mustermann::Mapper.new }
44
+
45
+ context 'it maps params' do
46
+ before { mapper["/:a"] = "/:a.html" }
47
+ example { mapper["/foo"] .should be == "/foo.html" }
48
+ example { mapper["/foo/bar"] .should be == "/foo/bar" }
49
+ end
50
+
51
+ context 'it supports named splats' do
52
+ before { mapper["/*a"] = "/*a.html" }
53
+ example { mapper["/foo"] .should be == "/foo.html" }
54
+ example { mapper["/foo/bar"] .should be == "/foo/bar.html" }
55
+ end
56
+
57
+ context 'can map from patterns' do
58
+ before { mapper[Mustermann.new("/:a")] = "/:a.html" }
59
+ example { mapper["/foo"] .should be == "/foo.html" }
60
+ example { mapper["/foo/bar"] .should be == "/foo/bar" }
61
+ end
62
+
63
+ context 'can map to patterns' do
64
+ before { mapper[Mustermann.new("/:a")] = Mustermann.new("/:a.html") }
65
+ example { mapper["/foo"] .should be == "/foo.html" }
66
+ example { mapper["/foo/bar"] .should be == "/foo/bar" }
67
+ end
68
+
69
+ context 'can map to expanders' do
70
+ before { mapper[Mustermann.new("/:a")] = Mustermann::Expander.new("/:a.html") }
71
+ example { mapper["/foo"] .should be == "/foo.html" }
72
+ example { mapper["/foo/bar"] .should be == "/foo/bar" }
73
+ end
74
+
75
+ context 'can map to array' do
76
+ before { mapper["/:a"] = ["/:a.html", "/:a.:f"] }
77
+ example { mapper["/foo"] .should be == "/foo.html" }
78
+ example { mapper["/foo", "f" => 'x'] .should be == "/foo.x" }
79
+ example { mapper["/foo", f: 'x'] .should be == "/foo.x" }
80
+ example { mapper["/foo/bar"] .should be == "/foo/bar" }
81
+ end
82
+ end
83
+ end