mustermann 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+ module Mustermann
2
+ # Raised if anything goes wrong while generating a {Pattern}.
3
+ class Error < StandardError; end
4
+
5
+ # Raised if anything goes wrong while compiling a {Pattern}.
6
+ class CompileError < Error; end
7
+
8
+ # Raised if anything goes wrong while parsing a {Pattern}.
9
+ class ParseError < Error; end
10
+
11
+ #@!visibility private
12
+ class UnexpectedClosingGroup < ParseError; end
13
+ private_constant :UnexpectedClosingGroup
14
+ end
@@ -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, except: nil, capture: nil, pattern: { }, **options)
28
+ if path.respond_to? :to_str
29
+ pattern[:except] = except if except
30
+ pattern[:capture] = capture if capture
31
+
32
+ if settings.respond_to? :pattern
33
+ pattern.merge!(settings.pattern || {}) do |key, local, global|
34
+ next local unless local.is_a? Hash
35
+ next global.merge(local) if global.is_a? Hash
36
+ Hash.new(global).merge! local
37
+ end
38
+ end
39
+
40
+ path = Mustermann.new(path, **pattern)
41
+
42
+ if defined? Template and path.is_a? Template
43
+ condition { params.merge! path.params(request.path_info) }
44
+ end
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 Pattern
10
+ # @see file:README.md#identity Syntax description in the README
11
+ class Identity < Pattern
12
+ # @param (see Pattern#===)
13
+ # @return (see Pattern#===)
14
+ # @see (see Pattern#===)
15
+ def ===(string)
16
+ unescape(string) == @string
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,142 @@
1
+ require 'mustermann/error'
2
+ require 'mustermann/simple_match'
3
+ require 'uri'
4
+
5
+ module Mustermann
6
+ # Superclass for all pattern implementations.
7
+ # @abstract
8
+ class Pattern
9
+ # List of supported options.
10
+ #
11
+ # @overload supported_options
12
+ # @return [Array<Symbol>] list of supported options
13
+ # @overload supported_options(*list)
14
+ # Adds options to the list.
15
+ #
16
+ # @api private
17
+ # @param [Symbol] *list adds options to the list of supported options
18
+ # @return [Array<Symbol>] list of supported options
19
+ def self.supported_options(*list)
20
+ @supported_options ||= []
21
+ options = @supported_options.concat(list)
22
+ options += superclass.supported_options if self < Pattern
23
+ options
24
+ end
25
+
26
+ # @param [Symbol] option The option to check.
27
+ # @return [Boolean] Whether or not option is supported.
28
+ def self.supported?(option)
29
+ supported_options.include? option
30
+ end
31
+
32
+ # @overload new(string, **options)
33
+ # @param (see #initialize)
34
+ # @raise (see #initialize)
35
+ # @raise [ArgumentError] if some option is not supported
36
+ # @return [Pattern] a new instance of Pattern
37
+ # @see #initialize
38
+ def self.new(string, ignore_unknown_options: false, **options)
39
+ unless ignore_unknown_options
40
+ unsupported = options.keys.detect { |key| not supported?(key) }
41
+ raise ArgumentError, "unsupported option %p for %p" % [unsupported, self] if unsupported
42
+ end
43
+
44
+ super(string, options)
45
+ end
46
+
47
+ supported_options :uri_decode, :ignore_unknown_options
48
+
49
+ # @overload initialize(string, **options)
50
+ # @param [String] string the string repesentation of the pattern
51
+ # @param [Hash] options options for fine-tuning the pattern behavior
52
+ # @raise [Mustermann::Error] if the pattern can't be generated from the string
53
+ # @see file:README.md#Types_and_Options "Types and Options" in the README
54
+ # @see Mustermann.new
55
+ def initialize(string, uri_decode: true, **options)
56
+ @uri_decode = uri_decode
57
+ @string = string.dup
58
+ end
59
+
60
+ # @return [String] the string representation of the pattern
61
+ def to_s
62
+ @string.dup
63
+ end
64
+
65
+ # @param [String] string The string to match against
66
+ # @return [MatchData, Mustermann::SimpleMatch, nil] MatchData or similar object if the pattern matches.
67
+ # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-match Regexp#match
68
+ # @see http://ruby-doc.org/core-2.0/MatchData.html MatchData
69
+ # @see SimpleMatch
70
+ def match(string)
71
+ SimpleMatch.new(string) if self === string
72
+ end
73
+
74
+ # @param [String] string The string to match against
75
+ # @return [Integer, nil] nil if pattern does not match the string, zero if it does.
76
+ # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-3D-7E Regexp#=~
77
+ def =~(string)
78
+ 0 if self === string
79
+ end
80
+
81
+ # @param [String] string The string to match against
82
+ # @return [Boolean] Whether or not the pattern matches the given string
83
+ # @note Needs to be overridden by subclass.
84
+ # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-3D-3D-3D Regexp#===
85
+ def ===(string)
86
+ raise NotImplementedError, 'subclass responsibility'
87
+ end
88
+
89
+ # @return [Array<String>] capture names.
90
+ # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-named_captures Regexp#named_captures
91
+ def named_captures
92
+ {}
93
+ end
94
+
95
+ # @return [Hash{String}: Array<Integer>] capture names mapped to capture index.
96
+ # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-names Regexp#names
97
+ def names
98
+ []
99
+ end
100
+
101
+ # @param [String] string the string to match against
102
+ # @return [Hash{String: String, Array<String>}, nil] Sinatra style params if pattern matches.
103
+ def params(string = nil, captures: nil)
104
+ return unless captures ||= match(string)
105
+ params = named_captures.map do |name, positions|
106
+ values = positions.map { |pos| map_param(name, captures[pos]) }.flatten
107
+ values = values.first if values.size < 2 and not always_array? name
108
+ [name, values]
109
+ end
110
+
111
+ Hash[params]
112
+ end
113
+
114
+ # @!visibility private
115
+ def inspect
116
+ "#<%p:%p>" % [self.class, @string]
117
+ end
118
+
119
+ # @!visibility private
120
+ def map_param(key, value)
121
+ unescape(value, true)
122
+ end
123
+
124
+ # @!visibility private
125
+ def unescape(string, decode = @uri_decode)
126
+ return string unless decode and string
127
+ @uri ||= URI::Parser.new
128
+ @uri.unescape(string)
129
+ end
130
+
131
+ # @!visibility private
132
+ ALWAYS_ARRAY = %w[splat captures]
133
+
134
+ # @!visibility private
135
+ def always_array?(key)
136
+ ALWAYS_ARRAY.include? key
137
+ end
138
+
139
+ private :unescape, :map_param
140
+ private_constant :ALWAYS_ARRAY
141
+ end
142
+ end
@@ -0,0 +1,26 @@
1
+ require 'mustermann/ast'
2
+
3
+ module Mustermann
4
+ # Rails style pattern implementation.
5
+ #
6
+ # @example
7
+ # Mustermann.new('/:foo', type: :rails) === '/bar' # => true
8
+ #
9
+ # @see Pattern
10
+ # @see file:README.md#rails Syntax description in the README
11
+ class Rails < AST
12
+ def parse_element(buffer)
13
+ case char = buffer.getch
14
+ when nil then unexpected("end of string")
15
+ when ?) then unexpected(char, exception: UnexpectedClosingGroup)
16
+ when ?* then NamedSplat.parse { buffer.scan(/\w+/) }
17
+ when ?/ then Separator.new(char)
18
+ when ?( then Optional.new(Group.parse { parse_buffer(buffer) })
19
+ when ?: then Capture.parse { buffer.scan(/\w+/) }
20
+ else Char.new(char)
21
+ end
22
+ end
23
+
24
+ private :parse_element
25
+ end
26
+ 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 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 Pattern#initialize)
14
+ # @return (see Pattern#initialize)
15
+ # @see (see Pattern#initialize)
16
+ def initialize(string, **options)
17
+ @regexp = compile(string, **options)
18
+ super
19
+ end
20
+
21
+ extend Forwardable
22
+ def_delegators :regexp, :===, :=~, :match, :names, :named_captures
23
+
24
+ def compile(string, **options)
25
+ raise NotImplementedError, 'subclass responsibility'
26
+ end
27
+
28
+ private :compile
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ require 'mustermann/pattern'
2
+ require 'mustermann/simple_match'
3
+
4
+ module Mustermann
5
+ # Matches strings that are identical to the pattern.
6
+ #
7
+ # @example
8
+ # Mustermann.new('/*.*', type: :shell) === '/bar' # => false
9
+ #
10
+ # @see Pattern
11
+ # @see file:README.md#shell Syntax description in the README
12
+ class Shell < Pattern
13
+ FLAGS ||= File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB
14
+
15
+ # @param (see Pattern#===)
16
+ # @return (see Pattern#===)
17
+ # @see (see Pattern#===)
18
+ def ===(string)
19
+ File.fnmatch? @string, unescape(string), FLAGS
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,35 @@
1
+ require 'mustermann/regexp_based'
2
+
3
+ module Mustermann
4
+ # Sinatra 1.3 style pattern implementation.
5
+ #
6
+ # @example
7
+ # Mustermann.new('/:foo', type: :simple) === '/bar' # => true
8
+ #
9
+ # @see Pattern
10
+ # @see file:README.md#simple Syntax description in the README
11
+ class Simple < RegexpBased
12
+ supported_options :greedy, :space_matches_plus
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) }
16
+ pattern.gsub!(/((:\w+)|\*)/) do |match|
17
+ match == "*" ? "(?<splat>.*?)" : "(?<#{$2[1..-1]}>[^/?#]+#{?? unless greedy})"
18
+ end
19
+ /\A#{Regexp.new(pattern)}\Z/
20
+ rescue SyntaxError, RegexpError => error
21
+ type = error.message["invalid group name"] ? CompileError : ParseError
22
+ raise type, error.message, error.backtrace
23
+ end
24
+
25
+ def encoded(char, uri_decode, space_matches_plus)
26
+ return Regexp.escape(char) unless uri_decode
27
+ parser = URI::Parser.new
28
+ encoded = Regexp.union(parser.escape(char), parser.escape(char, /./).downcase, parser.escape(char, /./).upcase)
29
+ encoded = Regexp.union(encoded, encoded('+', true, true)) if space_matches_plus and char == " "
30
+ encoded
31
+ end
32
+
33
+ private :compile, :encoded
34
+ end
35
+ end
@@ -0,0 +1,30 @@
1
+ module Mustermann
2
+ # Fakes MatchData for patterns that do not support capturing.
3
+ # @see http://ruby-doc.org/core-2.0/MatchData.html MatchData
4
+ class SimpleMatch
5
+ # @api private
6
+ def initialize(string)
7
+ @string = string.dup
8
+ end
9
+
10
+ # @return [String] the string that was matched against
11
+ def to_s
12
+ @string.dup
13
+ end
14
+
15
+ # @return [Array<String>] empty array for immitating MatchData interface
16
+ def names
17
+ []
18
+ end
19
+
20
+ # @return [Array<String>] empty array for immitating MatchData interface
21
+ def captures
22
+ []
23
+ end
24
+
25
+ # @return [nil] imitates MatchData interface
26
+ def [](*args)
27
+ captures[*args]
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,32 @@
1
+ require 'mustermann/ast'
2
+
3
+ module Mustermann
4
+ # Sinatra 2.0 style pattern implementation.
5
+ #
6
+ # @example
7
+ # Mustermann.new('/:foo') === '/bar' # => true
8
+ #
9
+ # @see Pattern
10
+ # @see file:README.md#sinatra Syntax description in the README
11
+ class Sinatra < AST
12
+ def parse_element(buffer)
13
+ case char = buffer.getch
14
+ when nil, ?? then unexpected(char)
15
+ when ?) then unexpected(char, exception: UnexpectedClosingGroup)
16
+ when ?* then Splat.new
17
+ when ?/ then Separator.new(char)
18
+ when ?( then Group.parse { parse_buffer(buffer) }
19
+ when ?: then Capture.parse { buffer.scan(/\w+/) }
20
+ when ?\\ then Char.new expect(buffer, /./)
21
+ else Char.new(char)
22
+ end
23
+ end
24
+
25
+ def parse_suffix(element, buffer)
26
+ return element unless buffer.scan(/\?/)
27
+ Optional.new(element)
28
+ end
29
+
30
+ private :parse_element, :parse_suffix
31
+ end
32
+ end
@@ -0,0 +1,140 @@
1
+ require 'mustermann/ast'
2
+
3
+ module Mustermann
4
+ # URI template pattern implementation.
5
+ #
6
+ # @example
7
+ # Mustermann.new('/{foo}') === '/bar' # => true
8
+ #
9
+ # @see Pattern
10
+ # @see file:README.md#template Syntax description in the README
11
+ # @see http://tools.ietf.org/html/rfc6570 RFC 6570
12
+ class Template < AST
13
+ Operator ||= Struct.new(:separator, :allow_reserved, :prefix, :parametric)
14
+ OPERATORS ||= {
15
+ nil => Operator.new(?,, false, false, false), ?+ => Operator.new(?,, true, false, false),
16
+ ?# => Operator.new(?,, true, ?#, false), ?. => Operator.new(?., false, ?., false),
17
+ ?/ => Operator.new(?/, false, ?/, false), ?; => Operator.new(?;, false, ?;, true),
18
+ ?? => Operator.new(?&, false, ??, true), ?& => Operator.new(?&, false, ?&, true)
19
+ }
20
+
21
+ # AST node for template expressions.
22
+ # @!visibility private
23
+ class Expression < Group
24
+ # @!visibility private
25
+ attr_accessor :operator
26
+
27
+ # makes sure we have the proper surrounding characters for the operator
28
+ # @!visibility private
29
+ def transform
30
+ self.operator = OPERATORS.fetch(operator) { raise CompileError, "#{operator} operator not supported" }
31
+ new_payload = payload.inject { |list, element| Array(list) << separator << element }
32
+ @payload = Array(new_payload).map!(&:transform)
33
+ payload.unshift separator(operator.prefix) if operator.prefix
34
+ self
35
+ end
36
+
37
+ # @!visibility private
38
+ def compile(greedy: true, **options)
39
+ super(allow_reserved: operator.allow_reserved, greedy: greedy && !operator.allow_reserved,
40
+ parametric: operator.parametric, separator: operator.separator, **options)
41
+ end
42
+
43
+ # @!visibility private
44
+ def separator(char = operator.separator)
45
+ AST.const_get(:Separator).new(char) # uhm
46
+ end
47
+ end
48
+
49
+ # AST node for template variables.
50
+ # @!visibility private
51
+ class Variable < Capture
52
+ # @!visibility private
53
+ attr_accessor :prefix, :explode
54
+
55
+ # @!visibility private
56
+ def compile(**options)
57
+ return super(**options) if explode or not options[:parametric]
58
+ parametric super(parametric: false, **options)
59
+ end
60
+
61
+ # @!visibility private
62
+ def pattern(parametric: false, **options)
63
+ register_param(parametric: parametric, **options)
64
+ pattern = super(**options)
65
+ pattern = parametric(pattern) if parametric
66
+ pattern = "#{pattern}(?:#{Regexp.escape(options.fetch(:separator))}#{pattern})*" if explode
67
+ pattern
68
+ end
69
+
70
+ # @!visibility private
71
+ def parametric(string)
72
+ "#{Regexp.escape(name)}(?:=#{string})?"
73
+ end
74
+
75
+ # @!visibility private
76
+ def qualified(string, **options)
77
+ prefix ? "#{string}{1,#{prefix}}" : super(string, **options)
78
+ end
79
+
80
+ # @!visibility private
81
+ def default(allow_reserved: false, **options)
82
+ allow_reserved ? '[\w\-\.~%\:/\?#\[\]@\!\$\&\'\(\)\*\+,;=]' : '[\w\-\.~%]'
83
+ end
84
+
85
+ # @!visibility private
86
+ def register_param(parametric: false, split_params: nil, separator: nil, **options)
87
+ return unless explode and split_params
88
+ split_params[name] = { separator: separator, parametric: parametric }
89
+ end
90
+ end
91
+
92
+ # @!visibility private
93
+ def parse_element(buffer)
94
+ parse_expression(buffer) || parse_literal(buffer)
95
+ end
96
+
97
+ # @!visibility private
98
+ def parse_expression(buffer)
99
+ return unless buffer.scan(/\{/)
100
+ operator = buffer.scan(/[\+\#\.\/;\?\&\=\,\!\@\|]/)
101
+ expression = Expression.new(parse_variable(buffer), operator: operator)
102
+ expression.parse { parse_variable(buffer) if buffer.scan(/,/) }
103
+ expression if expect(buffer, ?})
104
+ end
105
+
106
+ # @!visibility private
107
+ def parse_variable(buffer)
108
+ match = expect(buffer, /(?<name>\w+)(?:\:(?<prefix>\d{1,4})|(?<explode>\*))?/)
109
+ Variable.new(match[:name], prefix: match[:prefix], explode: match[:explode])
110
+ end
111
+
112
+ # @!visibility private
113
+ def parse_literal(buffer)
114
+ return unless char = buffer.getch
115
+ raise unexpected(?}) if char == ?}
116
+ char == ?/ ? Separator.new('/') : Char.new(char)
117
+ end
118
+
119
+ # @!visibility private
120
+ def compile(*args, **options)
121
+ @split_params = {}
122
+ super(*args, split_params: @split_params, **options)
123
+ end
124
+
125
+ # @!visibility private
126
+ def map_param(key, value)
127
+ return super unless variable = @split_params[key]
128
+ value = value.split variable[:separator]
129
+ value.map! { |e| e.sub(/\A#{key}=/, '') } if variable[:parametric]
130
+ value.map! { |e| super(key, e) }
131
+ end
132
+
133
+ # @!visibility private
134
+ def always_array?(key)
135
+ @split_params.include? key
136
+ end
137
+
138
+ private :parse_element, :parse_expression, :parse_literal, :parse_variable, :map_param, :always_array?
139
+ end
140
+ end