mustermann19 0.3.1

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.
Files changed (69) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +10 -0
  4. data/.yardopts +1 -0
  5. data/Gemfile +2 -0
  6. data/LICENSE +22 -0
  7. data/README.md +1081 -0
  8. data/Rakefile +6 -0
  9. data/bench/capturing.rb +57 -0
  10. data/bench/regexp.rb +21 -0
  11. data/bench/simple_vs_sinatra.rb +23 -0
  12. data/bench/template_vs_addressable.rb +26 -0
  13. data/internals.md +64 -0
  14. data/lib/mustermann.rb +61 -0
  15. data/lib/mustermann/ast/compiler.rb +168 -0
  16. data/lib/mustermann/ast/expander.rb +134 -0
  17. data/lib/mustermann/ast/node.rb +160 -0
  18. data/lib/mustermann/ast/parser.rb +137 -0
  19. data/lib/mustermann/ast/pattern.rb +84 -0
  20. data/lib/mustermann/ast/transformer.rb +129 -0
  21. data/lib/mustermann/ast/translator.rb +108 -0
  22. data/lib/mustermann/ast/tree_renderer.rb +29 -0
  23. data/lib/mustermann/ast/validation.rb +43 -0
  24. data/lib/mustermann/caster.rb +117 -0
  25. data/lib/mustermann/equality_map.rb +48 -0
  26. data/lib/mustermann/error.rb +6 -0
  27. data/lib/mustermann/expander.rb +206 -0
  28. data/lib/mustermann/extension.rb +52 -0
  29. data/lib/mustermann/identity.rb +19 -0
  30. data/lib/mustermann/mapper.rb +98 -0
  31. data/lib/mustermann/pattern.rb +182 -0
  32. data/lib/mustermann/rails.rb +17 -0
  33. data/lib/mustermann/regexp_based.rb +30 -0
  34. data/lib/mustermann/regular.rb +26 -0
  35. data/lib/mustermann/router.rb +9 -0
  36. data/lib/mustermann/router/rack.rb +50 -0
  37. data/lib/mustermann/router/simple.rb +144 -0
  38. data/lib/mustermann/shell.rb +29 -0
  39. data/lib/mustermann/simple.rb +38 -0
  40. data/lib/mustermann/simple_match.rb +30 -0
  41. data/lib/mustermann/sinatra.rb +22 -0
  42. data/lib/mustermann/template.rb +48 -0
  43. data/lib/mustermann/to_pattern.rb +45 -0
  44. data/lib/mustermann/version.rb +3 -0
  45. data/mustermann.gemspec +31 -0
  46. data/spec/expander_spec.rb +105 -0
  47. data/spec/extension_spec.rb +296 -0
  48. data/spec/identity_spec.rb +83 -0
  49. data/spec/mapper_spec.rb +83 -0
  50. data/spec/mustermann_spec.rb +65 -0
  51. data/spec/pattern_spec.rb +49 -0
  52. data/spec/rails_spec.rb +522 -0
  53. data/spec/regexp_based_spec.rb +8 -0
  54. data/spec/regular_spec.rb +36 -0
  55. data/spec/router/rack_spec.rb +39 -0
  56. data/spec/router/simple_spec.rb +32 -0
  57. data/spec/shell_spec.rb +109 -0
  58. data/spec/simple_match_spec.rb +10 -0
  59. data/spec/simple_spec.rb +237 -0
  60. data/spec/sinatra_spec.rb +574 -0
  61. data/spec/support.rb +5 -0
  62. data/spec/support/coverage.rb +16 -0
  63. data/spec/support/env.rb +15 -0
  64. data/spec/support/expand_matcher.rb +27 -0
  65. data/spec/support/match_matcher.rb +39 -0
  66. data/spec/support/pattern.rb +39 -0
  67. data/spec/template_spec.rb +815 -0
  68. data/spec/to_pattern_spec.rb +20 -0
  69. metadata +301 -0
@@ -0,0 +1,52 @@
1
+ require 'sinatra/version'
2
+ fail "no need to load the Mustermann extension for #{::Sinatra::VERSION}" if ::Sinatra::VERSION >= '2.0.0'
3
+
4
+ require 'mustermann'
5
+
6
+ module Mustermann
7
+ # Sinatra 1.x extension switching default pattern parsing over to Mustermann.
8
+ #
9
+ # @example With classic Sinatra application
10
+ # require 'sinatra'
11
+ # require 'mustermann'
12
+ #
13
+ # register Mustermann
14
+ # get('/:id', capture: /\d+/) { ... }
15
+ #
16
+ # @example With modular Sinatra application
17
+ # require 'sinatra/base'
18
+ # require 'mustermann'
19
+ #
20
+ # class MyApp < Sinatra::Base
21
+ # register Mustermann
22
+ # get('/:id', capture: /\d+/) { ... }
23
+ # end
24
+ #
25
+ # @see file:README.md#Sinatra_Integration "Sinatra Integration" in the README
26
+ module Extension
27
+ def compile!(verb, path, block, options = {})
28
+ except = options.delete(:except)
29
+ capture = options.delete(:capture)
30
+ pattern = options.delete(:pattern) || {}
31
+ if path.respond_to? :to_str
32
+ pattern[:except] = except if except
33
+ pattern[:capture] = capture if capture
34
+
35
+ if settings.respond_to? :pattern and settings.pattern?
36
+ pattern.merge! settings.pattern do |key, local, global|
37
+ next local unless local.is_a? Hash
38
+ next global.merge(local) if global.is_a? Hash
39
+ Hash.new(global).merge! local
40
+ end
41
+ end
42
+
43
+ path = Mustermann.new(path, pattern)
44
+ condition { params.merge! path.params(captures: Array(params[:captures]), offset: -1) }
45
+ end
46
+
47
+ super(verb, path, block, options)
48
+ end
49
+
50
+ private :compile!
51
+ end
52
+ end
@@ -0,0 +1,19 @@
1
+ require 'mustermann/pattern'
2
+
3
+ module Mustermann
4
+ # Matches strings that are identical to the pattern.
5
+ #
6
+ # @example
7
+ # Mustermann.new('/:foo', type: :identity) === '/bar' # => false
8
+ #
9
+ # @see Mustermann::Pattern
10
+ # @see file:README.md#identity Syntax description in the README
11
+ class Identity < Pattern
12
+ # @param (see Mustermann::Pattern#===)
13
+ # @return (see Mustermann::Pattern#===)
14
+ # @see (see Mustermann::Pattern#===)
15
+ def ===(string)
16
+ unescape(string) == @string
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,98 @@
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(options = {}, &block)
48
+ @map = []
49
+ @additional_values = options.delete(:additional_values) || :ignore
50
+ @options = options
51
+ map = @options.inject({}) do |result, entry|
52
+ result[entry[0]] = @options.delete(entry[0]) if entry[0].is_a?(String)
53
+ result
54
+ end
55
+ block.arity == 0 ? update(yield) : yield(self) if block
56
+ update(map) if map
57
+ end
58
+
59
+ # Add multiple mappings.
60
+ #
61
+ # @param map [Hash{String, Pattern: String, Pattern, Arry<String, Pattern>, Expander}] the mapping
62
+ def update(map)
63
+ map.to_hash.each_pair do |input, output|
64
+ input = Mustermann.new(input, @options.dup)
65
+ output = Expander.new(*output, @options.merge(additional_values: @additional_values)) unless output.is_a? Expander
66
+ @map << [input, output]
67
+ end
68
+ end
69
+
70
+ # @return [Hash{Patttern: Expander}] Hash version of the mapper.
71
+ def to_h
72
+ Hash[@map]
73
+ end
74
+
75
+ # Convert a string according to mappings. You can pass in additional params.
76
+ #
77
+ # @example mapping with and without additional parameters
78
+ # mapper = Mustermann::Mapper.new("/:example" => "(/:prefix)?/:example.html")
79
+ #
80
+ def convert(input, values = {})
81
+ @map.inject(input) do |current, (pattern, expander)|
82
+ params = pattern.params(current)
83
+ params &&= Hash[values.merge(params).map { |k,v| [k.to_s, v] }]
84
+ expander.expandable?(params) ? expander.expand(params) : current
85
+ end
86
+ end
87
+
88
+ # Add a single mapping.
89
+ #
90
+ # @param key [String, Pattern] format of the input string
91
+ # @param value [String, Pattern, Arry<String, Pattern>, Expander] format of the output string
92
+ def []=(key, value)
93
+ update key => value
94
+ end
95
+
96
+ alias_method :[], :convert
97
+ end
98
+ end
@@ -0,0 +1,182 @@
1
+ require 'mustermann/error'
2
+ require 'mustermann/simple_match'
3
+ require 'mustermann/equality_map'
4
+ require 'uri'
5
+
6
+ module Mustermann
7
+ # Superclass for all pattern implementations.
8
+ # @abstract
9
+ class Pattern
10
+ include Mustermann
11
+
12
+ # List of supported options.
13
+ #
14
+ # @overload supported_options
15
+ # @return [Array<Symbol>] list of supported options
16
+ # @overload supported_options(*list)
17
+ # Adds options to the list.
18
+ #
19
+ # @api private
20
+ # @param [Symbol] *list adds options to the list of supported options
21
+ # @return [Array<Symbol>] list of supported options
22
+ def self.supported_options(*list)
23
+ @supported_options ||= []
24
+ options = @supported_options.concat(list)
25
+ options += superclass.supported_options if self < Pattern
26
+ options
27
+ end
28
+
29
+ # @param [Symbol] option The option to check.
30
+ # @return [Boolean] Whether or not option is supported.
31
+ def self.supported?(option)
32
+ supported_options.include? option
33
+ end
34
+
35
+ # @overload new(string, **options)
36
+ # @param (see #initialize)
37
+ # @raise (see #initialize)
38
+ # @raise [ArgumentError] if some option is not supported
39
+ # @return [Mustermann::Pattern] a new instance of Mustermann::Pattern
40
+ # @see #initialize
41
+ def self.new(string, options = {})
42
+ ignore_unknown_options = options.fetch(:ignore_unknown_options, false)
43
+ options.delete(:ignore_unknown_options)
44
+ unless ignore_unknown_options
45
+ unsupported = options.keys.detect { |key| not supported?(key) }
46
+ raise ArgumentError, "unsupported option %p for %p" % [unsupported, self] if unsupported
47
+ end
48
+
49
+ @map ||= EqualityMap.new
50
+ @map.fetch(string, options) { super(string, options) }
51
+ end
52
+
53
+ supported_options :uri_decode, :ignore_unknown_options
54
+
55
+ # @overload initialize(string, **options)
56
+ # @param [String] string the string representation of the pattern
57
+ # @param [Hash] options options for fine-tuning the pattern behavior
58
+ # @raise [Mustermann::Error] if the pattern can't be generated from the string
59
+ # @see file:README.md#Types_and_Options "Types and Options" in the README
60
+ # @see Mustermann.new
61
+ def initialize(string, options = {})
62
+ uri_decode = options.fetch(:uri_decode, true)
63
+ @uri_decode = uri_decode
64
+ @string = string.to_s.dup
65
+ end
66
+
67
+ # @return [String] the string representation of the pattern
68
+ def to_s
69
+ @string.dup
70
+ end
71
+
72
+ # @param [String] string The string to match against
73
+ # @return [MatchData, Mustermann::SimpleMatch, nil] MatchData or similar object if the pattern matches.
74
+ # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-match Regexp#match
75
+ # @see http://ruby-doc.org/core-2.0/MatchData.html MatchData
76
+ # @see Mustermann::SimpleMatch
77
+ def match(string)
78
+ SimpleMatch.new(string) if self === string
79
+ end
80
+
81
+ # @param [String] string The string to match against
82
+ # @return [Integer, nil] nil if pattern does not match the string, zero if it does.
83
+ # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-3D-7E Regexp#=~
84
+ def =~(string)
85
+ 0 if self === string
86
+ end
87
+
88
+ # @param [String] string The string to match against
89
+ # @return [Boolean] Whether or not the pattern matches the given string
90
+ # @note Needs to be overridden by subclass.
91
+ # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-3D-3D-3D Regexp#===
92
+ def ===(string)
93
+ raise NotImplementedError, 'subclass responsibility'
94
+ end
95
+
96
+ # @return [Array<String>] capture names.
97
+ # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-named_captures Regexp#named_captures
98
+ def named_captures
99
+ {}
100
+ end
101
+
102
+ # @return [Hash{String: Array<Integer>}] capture names mapped to capture index.
103
+ # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-names Regexp#names
104
+ def names
105
+ []
106
+ end
107
+
108
+ # @param [String] string the string to match against
109
+ # @return [Hash{String: String, Array<String>}, nil] Sinatra style params if pattern matches.
110
+ def params(string = nil, options = {})
111
+ options, string = string, nil if string.is_a?(Hash)
112
+ captures = options[:captures]
113
+ offset = options[:offset] || 0
114
+ return unless captures ||= match(string)
115
+ params = named_captures.map do |name, positions|
116
+ values = positions.map { |pos| map_param(name, captures[pos + offset]) }.flatten
117
+ values = values.first if values.size < 2 and not always_array? name
118
+ [name, values]
119
+ end
120
+
121
+ Hash[params]
122
+ end
123
+
124
+ # @note This method is only implemented by certain subclasses.
125
+ #
126
+ # @example Expanding a pattern
127
+ # pattern = Mustermann.new('/:name(.:ext)?')
128
+ # pattern.expand(name: 'hello') # => "/hello"
129
+ # pattern.expand(name: 'hello', ext: 'png') # => "/hello.png"
130
+ #
131
+ # @example Checking if a pattern supports expanding
132
+ # if pattern.respond_to? :expand
133
+ # pattern.expand(name: "foo")
134
+ # else
135
+ # warn "does not support expanding"
136
+ # end
137
+ #
138
+ # @param [Hash{Symbol: #to_s, Array<#to_s>}] values values to use for expansion
139
+ # @return [String] expanded string
140
+ # @raise [NotImplementedError] raised if expand is not supported.
141
+ # @raise [Mustermann::ExpandError] raised if a value is missing or unknown
142
+ # @see Mustermann::Expander
143
+ def expand(values = {})
144
+ raise NotImplementedError, "expanding not supported by #{self.class}"
145
+ end
146
+
147
+ # @!visibility private
148
+ # @return [Boolean]
149
+ # @see Object#respond_to?
150
+ def respond_to?(method, *args)
151
+ method.to_s == 'expand' ? false : super
152
+ end
153
+
154
+ # @!visibility private
155
+ def inspect
156
+ "#<%p:%p>" % [self.class, @string]
157
+ end
158
+
159
+ # @!visibility private
160
+ def map_param(key, value)
161
+ unescape(value, true)
162
+ end
163
+
164
+ # @!visibility private
165
+ def unescape(string, decode = @uri_decode)
166
+ return string unless decode and string
167
+ @uri ||= URI::Parser.new
168
+ @uri.unescape(string)
169
+ end
170
+
171
+ # @!visibility private
172
+ ALWAYS_ARRAY = %w[splat captures]
173
+
174
+ # @!visibility private
175
+ def always_array?(key)
176
+ ALWAYS_ARRAY.include? key
177
+ end
178
+
179
+ private :unescape, :map_param
180
+ #private_constant :ALWAYS_ARRAY
181
+ end
182
+ end
@@ -0,0 +1,17 @@
1
+ require 'mustermann/ast/pattern'
2
+
3
+ module Mustermann
4
+ # Rails style pattern implementation.
5
+ #
6
+ # @example
7
+ # Mustermann.new('/:foo', type: :rails) === '/bar' # => true
8
+ #
9
+ # @see Mustermann::Pattern
10
+ # @see file:README.md#rails Syntax description in the README
11
+ class Rails < AST::Pattern
12
+ on(nil, ?)) { |c| unexpected(c) }
13
+ on(?*) { |c| node(:named_splat) { scan(/\w+/) } }
14
+ on(?() { |c| node(:optional, node(:group) { read unless scan(?)) }) }
15
+ on(?:) { |c| node(:capture) { scan(/\w+/) } }
16
+ end
17
+ end
@@ -0,0 +1,30 @@
1
+ require 'mustermann/pattern'
2
+ require 'forwardable'
3
+
4
+ module Mustermann
5
+ # Superclass for patterns that internally compile to a regular expression.
6
+ # @see Mustermann::Pattern
7
+ # @abstract
8
+ class RegexpBased < Pattern
9
+ # @return [Regexp] regular expression equivalent to the pattern.
10
+ attr_reader :regexp
11
+ alias_method :to_regexp, :regexp
12
+
13
+ # @param (see Mustermann::Pattern#initialize)
14
+ # @return (see Mustermann::Pattern#initialize)
15
+ # @see (see Mustermann::Pattern#initialize)
16
+ def initialize(string, options = {})
17
+ super
18
+ @regexp = compile(options)
19
+ end
20
+
21
+ extend Forwardable
22
+ def_delegators :regexp, :===, :=~, :match, :names, :named_captures
23
+
24
+ def compile(options = {})
25
+ raise NotImplementedError, 'subclass responsibility'
26
+ end
27
+
28
+ private :compile
29
+ end
30
+ end
@@ -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
@@ -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,50 @@
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.new 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(options = {}, &block)
25
+ env_prefix = options.delete(:env_prefix) || "mustermann"
26
+ params_key = options.delete(:params_key) || "#{env_prefix}.params"
27
+ pattern_key = options.delete(:pattern_key) || "#{env_prefix}.pattern"
28
+ @params_key, @pattern_key = params_key, pattern_key
29
+ options[:default] = [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass"}, ["Not Found"]] unless options.include? :default
30
+ super(options, &block)
31
+ end
32
+
33
+ def invoke(callback, env, params, pattern)
34
+ params_was, pattern_was = env[@params_key], env[@pattern_key]
35
+ env[@params_key], env[@pattern_key] = params, pattern
36
+ response = callback.call(env)
37
+ response[1].each { |k,v| throw :pass if k.downcase == 'x-cascade' and v == 'pass' }
38
+ response
39
+ ensure
40
+ env[@params_key], env[@pattern_key] = params_was, pattern_was
41
+ end
42
+
43
+ def string_for(env)
44
+ env['PATH_INFO']
45
+ end
46
+
47
+ private :invoke, :string_for
48
+ end
49
+ end
50
+ end