mustermann19 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
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