mustermann 0.2.0 → 0.3.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.
@@ -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