mustermann 0.0.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.
@@ -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