mustermann19 0.3.1 → 0.3.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.travis.yml +4 -3
- data/README.md +680 -376
- data/lib/mustermann/ast/compiler.rb +13 -7
- data/lib/mustermann/ast/expander.rb +11 -5
- data/lib/mustermann/ast/node.rb +27 -1
- data/lib/mustermann/ast/param_scanner.rb +20 -0
- data/lib/mustermann/ast/parser.rb +131 -12
- data/lib/mustermann/ast/pattern.rb +45 -6
- data/lib/mustermann/ast/template_generator.rb +28 -0
- data/lib/mustermann/ast/validation.rb +5 -3
- data/lib/mustermann/composite.rb +103 -0
- data/lib/mustermann/expander.rb +1 -1
- data/lib/mustermann/express.rb +34 -0
- data/lib/mustermann/flask.rb +204 -0
- data/lib/mustermann/identity.rb +54 -0
- data/lib/mustermann/pattern.rb +186 -12
- data/lib/mustermann/pattern_cache.rb +49 -0
- data/lib/mustermann/pyramid.rb +25 -0
- data/lib/mustermann/regexp_based.rb +18 -1
- data/lib/mustermann/regular.rb +1 -1
- data/lib/mustermann/shell.rb +8 -0
- data/lib/mustermann/simple.rb +1 -1
- data/lib/mustermann/simple_match.rb +5 -0
- data/lib/mustermann/sinatra.rb +19 -5
- data/lib/mustermann/string_scanner.rb +314 -0
- data/lib/mustermann/template.rb +10 -0
- data/lib/mustermann/to_pattern.rb +11 -6
- data/lib/mustermann/version.rb +1 -1
- data/lib/mustermann.rb +52 -3
- data/mustermann.gemspec +1 -1
- data/spec/composite_spec.rb +147 -0
- data/spec/expander_spec.rb +15 -0
- data/spec/express_spec.rb +209 -0
- data/spec/flask_spec.rb +361 -0
- data/spec/flask_subclass_spec.rb +368 -0
- data/spec/identity_spec.rb +44 -0
- data/spec/mustermann_spec.rb +14 -0
- data/spec/pattern_spec.rb +7 -3
- data/spec/pyramid_spec.rb +101 -0
- data/spec/rails_spec.rb +76 -2
- data/spec/regular_spec.rb +25 -0
- data/spec/shell_spec.rb +33 -0
- data/spec/simple_spec.rb +25 -0
- data/spec/sinatra_spec.rb +184 -9
- data/spec/string_scanner_spec.rb +271 -0
- data/spec/support/expand_matcher.rb +7 -5
- data/spec/support/generate_template_matcher.rb +27 -0
- data/spec/support/pattern.rb +3 -0
- data/spec/support/scan_matcher.rb +63 -0
- data/spec/support.rb +2 -1
- data/spec/template_spec.rb +22 -0
- data/spec/to_pattern_spec.rb +49 -0
- metadata +47 -61
- data/internals.md +0 -64
@@ -0,0 +1,204 @@
|
|
1
|
+
require 'mustermann/ast/pattern'
|
2
|
+
|
3
|
+
module Mustermann
|
4
|
+
# Flask style pattern implementation.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# Mustermann.new('/<foo>', type: :flask) === '/bar' # => true
|
8
|
+
#
|
9
|
+
# @see Mustermann::Pattern
|
10
|
+
# @see file:README.md#flask Syntax description in the README
|
11
|
+
class Flask < AST::Pattern
|
12
|
+
on(nil, ?>, ?:) { |c| unexpected(c) }
|
13
|
+
|
14
|
+
on(?<) do |char|
|
15
|
+
converter_name = expect(/\w+/, char: char)
|
16
|
+
args, opts = scan(?() ? read_args(?=, ?)) : [[], {}]
|
17
|
+
|
18
|
+
if scan(?:)
|
19
|
+
name = read_escaped(?>)
|
20
|
+
else
|
21
|
+
converter_name, name = 'default', converter_name
|
22
|
+
expect(?>)
|
23
|
+
end
|
24
|
+
|
25
|
+
converter = pattern.converters.fetch(converter_name) { unexpected("converter %p" % converter_name) }
|
26
|
+
converter = converter.new(*args, opts) if converter.respond_to? :new
|
27
|
+
constraint = converter.constraint if converter.respond_to? :constraint
|
28
|
+
convert = converter.convert if converter.respond_to? :convert
|
29
|
+
qualifier = converter.qualifier if converter.respond_to? :qualifier
|
30
|
+
node_type = converter.node_type if converter.respond_to? :node_type
|
31
|
+
node_type ||= :capture
|
32
|
+
|
33
|
+
node(node_type, name, convert: convert, constraint: constraint, qualifier: qualifier)
|
34
|
+
end
|
35
|
+
|
36
|
+
# A class for easy creating of converters.
|
37
|
+
# @see Mustermann::Flask#register_converter
|
38
|
+
class Converter
|
39
|
+
# Constraint on the format used for the capture.
|
40
|
+
# Should be a regexp (or a string corresponding to a regexp)
|
41
|
+
# @see Mustermann::Flask#register_converter
|
42
|
+
attr_accessor :constraint
|
43
|
+
|
44
|
+
# Callback
|
45
|
+
# Should be a Proc.
|
46
|
+
# @see Mustermann::Flask#register_converter
|
47
|
+
attr_accessor :convert
|
48
|
+
|
49
|
+
# Constraint on the format used for the capture.
|
50
|
+
# Should be a regexp (or a string corresponding to a regexp)
|
51
|
+
# @see Mustermann::Flask#register_converter
|
52
|
+
# @!visibility private
|
53
|
+
attr_accessor :node_type
|
54
|
+
|
55
|
+
# Constraint on the format used for the capture.
|
56
|
+
# Should be a regexp (or a string corresponding to a regexp)
|
57
|
+
# @see Mustermann::Flask#register_converter
|
58
|
+
# @!visibility private
|
59
|
+
attr_accessor :qualifier
|
60
|
+
|
61
|
+
# @!visibility private
|
62
|
+
def self.create(&block)
|
63
|
+
Class.new(self) do
|
64
|
+
define_method(:initialize) { |*a| o = a.last.kind_of?(Hash) ? a.pop : {}; block[self, *a, o] }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Makes sure a given value falls inbetween a min and a max.
|
69
|
+
# Uses the passed block to convert the value from a string to whatever
|
70
|
+
# format you'd expect.
|
71
|
+
#
|
72
|
+
# @example
|
73
|
+
# require 'mustermann/flask'
|
74
|
+
#
|
75
|
+
# class MyPattern < Mustermann::Flask
|
76
|
+
# register_converter(:x) { between(5, 15, &:to_i) }
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# pattern = MyPattern.new('<x:id>')
|
80
|
+
# pattern.params('/12') # => { 'id' => 12 }
|
81
|
+
# pattern.params('/16') # => { 'id' => 15 }
|
82
|
+
#
|
83
|
+
# @see Mustermann::Flask#register_converter
|
84
|
+
def between(min, max)
|
85
|
+
self.convert = proc do |input|
|
86
|
+
value = yield(input)
|
87
|
+
value = yield(min) if min and value < yield(min)
|
88
|
+
value = yield(max) if max and value > yield(max)
|
89
|
+
value
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Generally available converters.
|
95
|
+
# @!visibility private
|
96
|
+
def self.converters(inherited = true)
|
97
|
+
return @converters ||= {} unless inherited
|
98
|
+
defaults = superclass.respond_to?(:converters) ? superclass.converters : {}
|
99
|
+
defaults.merge(converters(false))
|
100
|
+
end
|
101
|
+
|
102
|
+
# Allows you to register your own converters.
|
103
|
+
#
|
104
|
+
# It is reommended to use this on a subclass, so to not influence other subsystems
|
105
|
+
# using flask templates.
|
106
|
+
#
|
107
|
+
# The object passed in as converter can implement #convert and/or #constraint.
|
108
|
+
#
|
109
|
+
# It can also instead implement #new, which will then return an object responding
|
110
|
+
# to some of these methods. Arguments from the flask pattern will be passed to #new.
|
111
|
+
#
|
112
|
+
# If passed a block, it will be yielded to with a {Mustermann::Flask::Converter}
|
113
|
+
# instance and any arguments in the flask pattern.
|
114
|
+
#
|
115
|
+
# @example with simple object
|
116
|
+
# require 'mustermann/flask'
|
117
|
+
#
|
118
|
+
# MyPattern = Class.new(Mustermann::Flask)
|
119
|
+
# up_converter = Struct.new(:convert).new(:upcase.to_proc)
|
120
|
+
# MyPattern.register_converter(:upper, up_converter)
|
121
|
+
#
|
122
|
+
# MyPattern.new("/<up:name>").params('/foo') # => { "name" => "FOO" }
|
123
|
+
#
|
124
|
+
# @example with block
|
125
|
+
# require 'mustermann/flask'
|
126
|
+
#
|
127
|
+
# MyPattern = Class.new(Mustermann::Flask)
|
128
|
+
# MyPattern.register_converter(:upper) { |c| c.convert = :upcase.to_proc }
|
129
|
+
#
|
130
|
+
# MyPattern.new("/<up:name>").params('/foo') # => { "name" => "FOO" }
|
131
|
+
#
|
132
|
+
# @example with converter class
|
133
|
+
# require 'mustermann/flasl'
|
134
|
+
#
|
135
|
+
# class MyPattern < Mustermann::Flask
|
136
|
+
# class Converter
|
137
|
+
# attr_reader :convert
|
138
|
+
# def initialize(send: :to_s)
|
139
|
+
# @convert = send.to_sym.to_proc
|
140
|
+
# end
|
141
|
+
# end
|
142
|
+
#
|
143
|
+
# register_converter(:t, Converter)
|
144
|
+
# end
|
145
|
+
#
|
146
|
+
# MyPattern.new("/<t(send=upcase):name>").params('/Foo') # => { "name" => "FOO" }
|
147
|
+
# MyPattern.new("/<t(send=downcase):name>").params('/Foo') # => { "name" => "foo" }
|
148
|
+
#
|
149
|
+
# @param [#to_s] name converter name
|
150
|
+
# @param [#new, #convert, #constraint, nil] converter
|
151
|
+
def self.register_converter(name, converter = nil, &block)
|
152
|
+
converter ||= Converter.create(&block)
|
153
|
+
converters(false)[name.to_s] = converter
|
154
|
+
end
|
155
|
+
|
156
|
+
register_converter(:string) do |converter, options = {}|
|
157
|
+
minlength = options.delete(:minlength)
|
158
|
+
maxlength = options.delete(:maxlength)
|
159
|
+
length = options.delete(:length)
|
160
|
+
converter.qualifier = "{%s,%s}" % [minlength || 1, maxlength] if minlength or maxlength
|
161
|
+
converter.qualifier = "{%s}" % length if length
|
162
|
+
end
|
163
|
+
|
164
|
+
register_converter(:int) do |converter, options = {}|
|
165
|
+
min = options.delete(:min)
|
166
|
+
max = options.delete(:max)
|
167
|
+
fixed_digits = options.fetch(:fixed_digits, false)
|
168
|
+
options.delete(:fixed_digits)
|
169
|
+
converter.constraint = /\d/
|
170
|
+
converter.qualifier = "{#{fixed_digits}}" if fixed_digits
|
171
|
+
converter.between(min, max) { |string| Integer(string) }
|
172
|
+
end
|
173
|
+
|
174
|
+
register_converter(:float) do |converter, options = {}|
|
175
|
+
min = options.delete(:min)
|
176
|
+
max = options.delete(:max)
|
177
|
+
converter.constraint = /\d*\.?\d+/
|
178
|
+
converter.qualifier = ""
|
179
|
+
converter.between(min, max) { |string| Float(string) }
|
180
|
+
end
|
181
|
+
|
182
|
+
register_converter(:path) do |converter|
|
183
|
+
converter.node_type = :named_splat
|
184
|
+
end
|
185
|
+
|
186
|
+
register_converter(:any) do |converter, *strings|
|
187
|
+
strings = strings.map { |s| Regexp.escape(s) unless s == {} }.compact
|
188
|
+
converter.qualifier = ""
|
189
|
+
converter.constraint = Regexp.union(*strings)
|
190
|
+
end
|
191
|
+
|
192
|
+
register_converter(:default, converters['string'])
|
193
|
+
|
194
|
+
supported_options :converters
|
195
|
+
attr_reader :converters
|
196
|
+
|
197
|
+
def initialize(input, options = {})
|
198
|
+
converters = options[:converters] || {}
|
199
|
+
@converters = self.class.converters.dup
|
200
|
+
converters.each { |k,v| @converters[k.to_s] = v } if converters
|
201
|
+
super(input, options)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
data/lib/mustermann/identity.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'mustermann/pattern'
|
2
|
+
require 'mustermann/ast/node'
|
2
3
|
|
3
4
|
module Mustermann
|
4
5
|
# Matches strings that are identical to the pattern.
|
@@ -15,5 +16,58 @@ module Mustermann
|
|
15
16
|
def ===(string)
|
16
17
|
unescape(string) == @string
|
17
18
|
end
|
19
|
+
|
20
|
+
# @param (see Mustermann::Pattern#peek_size)
|
21
|
+
# @return (see Mustermann::Pattern#peek_size)
|
22
|
+
# @see (see Mustermann::Pattern#peek_size)
|
23
|
+
def peek_size(string)
|
24
|
+
return unless unescape(string).start_with? @string
|
25
|
+
return @string.size if string.start_with? @string # optimization
|
26
|
+
@string.each_char.with_index.inject(0) do |count, (char, index)|
|
27
|
+
char_size = 1
|
28
|
+
escaped = @@uri.escape(char, /./)
|
29
|
+
char_size = escaped.size if string[index, escaped.size].downcase == escaped.downcase
|
30
|
+
count + char_size
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# URI templates support generating templates (the logic is quite complex, though).
|
35
|
+
#
|
36
|
+
# @example (see Mustermann::Pattern#to_templates)
|
37
|
+
# @param (see Mustermann::Pattern#to_templates)
|
38
|
+
# @return (see Mustermann::Pattern#to_templates)
|
39
|
+
# @see Mustermann::Pattern#to_templates
|
40
|
+
def to_templates
|
41
|
+
[@@uri.escape(to_s)]
|
42
|
+
end
|
43
|
+
|
44
|
+
# Generates an AST so it's compatible with {Mustermann::AST::Pattern}.
|
45
|
+
# Not used internally by {Mustermann::Identity}.
|
46
|
+
# @!visibility private
|
47
|
+
def to_ast
|
48
|
+
payload = @string.each_char.map { |c| AST::Node[c == ?/ ? :separator : :char].new(c) }
|
49
|
+
AST::Node[:root].new(payload, pattern: @string)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Identity patterns support expanding.
|
53
|
+
#
|
54
|
+
# This implementation does not use {Mustermann::Expander} internally to save memory and
|
55
|
+
# compilation time.
|
56
|
+
#
|
57
|
+
# @example (see Mustermann::Pattern#expand)
|
58
|
+
# @param (see Mustermann::Pattern#expand)
|
59
|
+
# @return (see Mustermann::Pattern#expand)
|
60
|
+
# @raise (see Mustermann::Pattern#expand)
|
61
|
+
# @see Mustermann::Pattern#expand
|
62
|
+
# @see Mustermann::Expander
|
63
|
+
def expand(behavior = nil, values = {})
|
64
|
+
values, behavior = behavior, nil if behavior.kind_of?(Hash)
|
65
|
+
return to_s if values.empty? or behavior == :ignore
|
66
|
+
raise ExpandError, "cannot expand with keys %p" % values.keys.sort if behavior == :raise
|
67
|
+
raise ArgumentError, "unknown behavior %p" % behavior if behavior != :append
|
68
|
+
params = values.map { |key, value| @@uri.escape(key.to_s) + "=" + @@uri.escape(value.to_s, /[^\w\d]/) }
|
69
|
+
separator = @string.include?(??) ? ?& : ??
|
70
|
+
@string + separator + params.join(?&)
|
71
|
+
end
|
18
72
|
end
|
19
73
|
end
|
data/lib/mustermann/pattern.rb
CHANGED
@@ -8,7 +8,9 @@ module Mustermann
|
|
8
8
|
# @abstract
|
9
9
|
class Pattern
|
10
10
|
include Mustermann
|
11
|
+
@@uri ||= URI::Parser.new
|
11
12
|
|
13
|
+
PATTERN_METHODS = %w[expand to_templates].map(&:to_sym)
|
12
14
|
# List of supported options.
|
13
15
|
#
|
14
16
|
# @overload supported_options
|
@@ -28,7 +30,7 @@ module Mustermann
|
|
28
30
|
|
29
31
|
# @param [Symbol] option The option to check.
|
30
32
|
# @return [Boolean] Whether or not option is supported.
|
31
|
-
def self.supported?(option)
|
33
|
+
def self.supported?(option, options = {})
|
32
34
|
supported_options.include? option
|
33
35
|
end
|
34
36
|
|
@@ -42,7 +44,7 @@ module Mustermann
|
|
42
44
|
ignore_unknown_options = options.fetch(:ignore_unknown_options, false)
|
43
45
|
options.delete(:ignore_unknown_options)
|
44
46
|
unless ignore_unknown_options
|
45
|
-
unsupported = options.keys.detect { |key| not supported?(key) }
|
47
|
+
unsupported = options.keys.detect { |key| not supported?(key, options) }
|
46
48
|
raise ArgumentError, "unsupported option %p for %p" % [unsupported, self] if unsupported
|
47
49
|
end
|
48
50
|
|
@@ -93,13 +95,73 @@ module Mustermann
|
|
93
95
|
raise NotImplementedError, 'subclass responsibility'
|
94
96
|
end
|
95
97
|
|
96
|
-
#
|
98
|
+
# Tries to match the pattern against the beginning of the string (as opposed to the full string).
|
99
|
+
# Will return the count of the matching characters if it matches.
|
100
|
+
#
|
101
|
+
# @example
|
102
|
+
# pattern = Mustermann.new('/:name')
|
103
|
+
# pattern.size("/Frank/Sinatra") # => 6
|
104
|
+
#
|
105
|
+
# @param [String] string The string to match against
|
106
|
+
# @return [Integer, nil] the number of characters that match
|
107
|
+
def peek_size(string)
|
108
|
+
# this is a very naive, unperformant implementation
|
109
|
+
string.size.downto(0).detect { |s| self === string[0, s] }
|
110
|
+
end
|
111
|
+
|
112
|
+
# Tries to match the pattern against the beginning of the string (as opposed to the full string).
|
113
|
+
# Will return the substring if it matches.
|
114
|
+
#
|
115
|
+
# @example
|
116
|
+
# pattern = Mustermann.new('/:name')
|
117
|
+
# pattern.peek("/Frank/Sinatra") # => "/Frank"
|
118
|
+
#
|
119
|
+
# @param [String] string The string to match against
|
120
|
+
# @return [String, nil] matched subsctring
|
121
|
+
def peek(string)
|
122
|
+
size = peek_size(string)
|
123
|
+
string[0, size] if size
|
124
|
+
end
|
125
|
+
|
126
|
+
# Tries to match the pattern against the beginning of the string (as opposed to the full string).
|
127
|
+
# Will return a MatchData or similar instance for the matched substring.
|
128
|
+
#
|
129
|
+
# @example
|
130
|
+
# pattern = Mustermann.new('/:name')
|
131
|
+
# pattern.peek("/Frank/Sinatra") # => #<MatchData "/Frank" name:"Frank">
|
132
|
+
#
|
133
|
+
# @param [String] string The string to match against
|
134
|
+
# @return [MatchData, Mustermann::SimpleMatch, nil] MatchData or similar object if the pattern matches.
|
135
|
+
# @see #peek_params
|
136
|
+
def peek_match(string)
|
137
|
+
matched = peek(string)
|
138
|
+
match(matched) if matched
|
139
|
+
end
|
140
|
+
|
141
|
+
# Tries to match the pattern against the beginning of the string (as opposed to the full string).
|
142
|
+
# Will return a two element Array with the params parsed from the substring as first entry and the length of
|
143
|
+
# the substring as second.
|
144
|
+
#
|
145
|
+
# @example
|
146
|
+
# pattern = Mustermann.new('/:name')
|
147
|
+
# params, _ = pattern.peek_params("/Frank/Sinatra")
|
148
|
+
#
|
149
|
+
# puts "Hello, #{params['name']}!" # Hello, Frank!
|
150
|
+
#
|
151
|
+
# @param [String] string The string to match against
|
152
|
+
# @return [Array<Hash, Integer>, nil] Array with params hash and length of substing if matched, nil otherwise
|
153
|
+
def peek_params(string)
|
154
|
+
match = peek_match(string)
|
155
|
+
[params(nil, :captures => match), match.to_s.size] if match
|
156
|
+
end
|
157
|
+
|
158
|
+
# @return [Hash{String: Array<Integer>}] capture names mapped to capture index.
|
97
159
|
# @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-named_captures Regexp#named_captures
|
98
160
|
def named_captures
|
99
161
|
{}
|
100
162
|
end
|
101
163
|
|
102
|
-
# @return [
|
164
|
+
# @return [Array<String>] capture names.
|
103
165
|
# @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-names Regexp#names
|
104
166
|
def names
|
105
167
|
[]
|
@@ -109,8 +171,8 @@ module Mustermann
|
|
109
171
|
# @return [Hash{String: String, Array<String>}, nil] Sinatra style params if pattern matches.
|
110
172
|
def params(string = nil, options = {})
|
111
173
|
options, string = string, nil if string.is_a?(Hash)
|
112
|
-
captures = options
|
113
|
-
offset = options
|
174
|
+
captures = options.fetch(:captures, nil)
|
175
|
+
offset = options.fetch(:offset, 0)
|
114
176
|
return unless captures ||= match(string)
|
115
177
|
params = named_captures.map do |name, positions|
|
116
178
|
values = positions.map { |pos| map_param(name, captures[pos + offset]) }.flatten
|
@@ -135,20 +197,127 @@ module Mustermann
|
|
135
197
|
# warn "does not support expanding"
|
136
198
|
# end
|
137
199
|
#
|
138
|
-
#
|
200
|
+
# Expanding is supported by almost all patterns (notable execptions are {Mustermann::Shell},
|
201
|
+
# {Mustermann::Regular} and {Mustermann::Simple}).
|
202
|
+
#
|
203
|
+
# Union {Mustermann::Composite} patterns (with the | operator) support expanding if all
|
204
|
+
# patterns they are composed of also support it.
|
205
|
+
#
|
206
|
+
# @param (see Mustermann::Expander#expand)
|
139
207
|
# @return [String] expanded string
|
140
208
|
# @raise [NotImplementedError] raised if expand is not supported.
|
141
209
|
# @raise [Mustermann::ExpandError] raised if a value is missing or unknown
|
142
210
|
# @see Mustermann::Expander
|
143
|
-
def expand(values = {})
|
211
|
+
def expand(behavior = nil, values = {})
|
144
212
|
raise NotImplementedError, "expanding not supported by #{self.class}"
|
145
213
|
end
|
146
214
|
|
215
|
+
# @note This method is only implemented by certain subclasses.
|
216
|
+
#
|
217
|
+
# Generates a list of URI template strings representing the pattern.
|
218
|
+
#
|
219
|
+
# Note that this transformation is lossy and the strings matching these
|
220
|
+
# templates might not match the pattern (and vice versa).
|
221
|
+
#
|
222
|
+
# This comes in quite handy since URI templates are not made for pattern matching.
|
223
|
+
# That way you can easily use a more precise template syntax and have it automatically
|
224
|
+
# generate hypermedia links for you.
|
225
|
+
#
|
226
|
+
# @example generating templates
|
227
|
+
# Mustermann.new("/:name").to_templates # => ["/{name}"]
|
228
|
+
# Mustermann.new("/:foo(@:bar)?/*baz").to_templates # => ["/{foo}@{bar}/{+baz}", "/{foo}/{+baz}"]
|
229
|
+
# Mustermann.new("/{name}", type: :template).to_templates # => ["/{name}"]
|
230
|
+
#
|
231
|
+
# @example generating templates from composite patterns
|
232
|
+
# pattern = Mustermann.new('/:name')
|
233
|
+
# pattern |= Mustermann.new('/{name}', type: :template)
|
234
|
+
# pattern |= Mustermann.new('/example/*nested')
|
235
|
+
# pattern.to_templates # => ["/{name}", "/example/{+nested}"]
|
236
|
+
#
|
237
|
+
# Template generation is supported by {Mustermann::Sinatra}, {Mustermann::Rails},
|
238
|
+
# {Mustermann::Template} and {Mustermann::Identity} patterns. Union {Mustermann::Composite}
|
239
|
+
# patterns (with the | operator) support template generation if all patterns they are composed
|
240
|
+
# of also support it.
|
241
|
+
#
|
242
|
+
# @example Checking if a pattern supports expanding
|
243
|
+
# if pattern.respond_to? :to_templates
|
244
|
+
# pattern.to_templates
|
245
|
+
# else
|
246
|
+
# warn "does not support template generation"
|
247
|
+
# end
|
248
|
+
#
|
249
|
+
# @return [Array<String>] list of URI templates
|
250
|
+
def to_templates
|
251
|
+
raise NotImplementedError, "template generation not supported by #{self.class}"
|
252
|
+
end
|
253
|
+
|
254
|
+
# @overload |(other)
|
255
|
+
# Creates a pattern that matches any string matching either one of the patterns.
|
256
|
+
# If a string is supplied, it is treated as an identity pattern.
|
257
|
+
#
|
258
|
+
# @example
|
259
|
+
# pattern = Mustermann.new('/foo/:name') | Mustermann.new('/:first/:second')
|
260
|
+
# pattern === '/foo/bar' # => true
|
261
|
+
# pattern === '/fox/bar' # => true
|
262
|
+
# pattern === '/foo' # => false
|
263
|
+
#
|
264
|
+
# @overload &(other)
|
265
|
+
# Creates a pattern that matches any string matching both of the patterns.
|
266
|
+
# If a string is supplied, it is treated as an identity pattern.
|
267
|
+
#
|
268
|
+
# @example
|
269
|
+
# pattern = Mustermann.new('/foo/:name') & Mustermann.new('/:first/:second')
|
270
|
+
# pattern === '/foo/bar' # => true
|
271
|
+
# pattern === '/fox/bar' # => false
|
272
|
+
# pattern === '/foo' # => false
|
273
|
+
#
|
274
|
+
# @overload ^(other)
|
275
|
+
# Creates a pattern that matches any string matching exactly one of the patterns.
|
276
|
+
# If a string is supplied, it is treated as an identity pattern.
|
277
|
+
#
|
278
|
+
# @example
|
279
|
+
# pattern = Mustermann.new('/foo/:name') ^ Mustermann.new('/:first/:second')
|
280
|
+
# pattern === '/foo/bar' # => false
|
281
|
+
# pattern === '/fox/bar' # => true
|
282
|
+
# pattern === '/foo' # => false
|
283
|
+
#
|
284
|
+
# @param [Mustermann::Pattern, String] other the other pattern
|
285
|
+
# @return [Mustermann::Pattern] a composite pattern
|
286
|
+
def |(other)
|
287
|
+
Mustermann.new(self, other, :operator => :|, :type => :identity)
|
288
|
+
end
|
289
|
+
|
290
|
+
def &(other)
|
291
|
+
Mustermann.new(self, other, :operator => :&, :type => :identity)
|
292
|
+
end
|
293
|
+
|
294
|
+
def ^(other)
|
295
|
+
Mustermann.new(self, other, :operator => :^, :type => :identity)
|
296
|
+
end
|
297
|
+
|
298
|
+
# @example
|
299
|
+
# pattern = Mustermann.new('/:a/:b')
|
300
|
+
# strings = ["foo/bar", "/foo/bar", "/foo/bar/"]
|
301
|
+
# strings.detect(&pattern) # => "/foo/bar"
|
302
|
+
#
|
303
|
+
# @return [Proc] proc wrapping {#===}
|
304
|
+
def to_proc
|
305
|
+
@to_proc ||= method(:===).to_proc
|
306
|
+
end
|
307
|
+
|
147
308
|
# @!visibility private
|
148
309
|
# @return [Boolean]
|
149
310
|
# @see Object#respond_to?
|
150
311
|
def respond_to?(method, *args)
|
151
|
-
|
312
|
+
return super unless PATTERN_METHODS.include? method
|
313
|
+
respond_to_special?(method)
|
314
|
+
end
|
315
|
+
|
316
|
+
# @!visibility private
|
317
|
+
# @return [Boolean]
|
318
|
+
# @see #respond_to?
|
319
|
+
def respond_to_special?(method)
|
320
|
+
method(method).owner != Mustermann::Pattern
|
152
321
|
end
|
153
322
|
|
154
323
|
# @!visibility private
|
@@ -156,6 +325,12 @@ module Mustermann
|
|
156
325
|
"#<%p:%p>" % [self.class, @string]
|
157
326
|
end
|
158
327
|
|
328
|
+
# @!visibility private
|
329
|
+
def simple_inspect
|
330
|
+
type = self.class.name[/[^:]+$/].downcase
|
331
|
+
"%s:%p" % [type, @string]
|
332
|
+
end
|
333
|
+
|
159
334
|
# @!visibility private
|
160
335
|
def map_param(key, value)
|
161
336
|
unescape(value, true)
|
@@ -164,8 +339,7 @@ module Mustermann
|
|
164
339
|
# @!visibility private
|
165
340
|
def unescape(string, decode = @uri_decode)
|
166
341
|
return string unless decode and string
|
167
|
-
|
168
|
-
@uri.unescape(string)
|
342
|
+
@@uri.unescape(string)
|
169
343
|
end
|
170
344
|
|
171
345
|
# @!visibility private
|
@@ -176,7 +350,7 @@ module Mustermann
|
|
176
350
|
ALWAYS_ARRAY.include? key
|
177
351
|
end
|
178
352
|
|
179
|
-
private :unescape, :map_param
|
353
|
+
private :unescape, :map_param, :respond_to_special?
|
180
354
|
#private_constant :ALWAYS_ARRAY
|
181
355
|
end
|
182
356
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'thread'
|
3
|
+
require 'mustermann'
|
4
|
+
|
5
|
+
module Mustermann
|
6
|
+
# A simple, persistent cache for creating repositories.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# require 'mustermann/pattern_cache'
|
10
|
+
# cache = Mustermann::PatternCache.new
|
11
|
+
#
|
12
|
+
# # use this instead of Mustermann.new
|
13
|
+
# pattern = cache.create_pattern("/:name", type: :rails)
|
14
|
+
#
|
15
|
+
# @note
|
16
|
+
# {Mustermann::Pattern.new} (which is used by {Mustermann.new}) will reuse instances that have
|
17
|
+
# not yet been garbage collected. You only need an extra cache if you do not keep a reference to
|
18
|
+
# the patterns around.
|
19
|
+
#
|
20
|
+
# @api private
|
21
|
+
class PatternCache
|
22
|
+
# @param [Hash] pattern_options default options used for {#create_pattern}
|
23
|
+
def initialize(pattern_options = {})
|
24
|
+
@cached = Set.new
|
25
|
+
@mutex = Mutex.new
|
26
|
+
@pattern_options = pattern_options
|
27
|
+
end
|
28
|
+
|
29
|
+
# @param (see Mustermann.new)
|
30
|
+
# @return (see Mustermann.new)
|
31
|
+
# @raise (see Mustermann.new)
|
32
|
+
# @see Mustermann.new
|
33
|
+
def create_pattern(string, pattern_options = {})
|
34
|
+
pattern = Mustermann.new(string, @pattern_options.merge(pattern_options))
|
35
|
+
@mutex.synchronize { @cached.add(pattern) } unless @cached.include? pattern
|
36
|
+
pattern
|
37
|
+
end
|
38
|
+
|
39
|
+
# Removes all pattern instances from the cache.
|
40
|
+
def clear
|
41
|
+
@mutex.synchronize { @cached.clear }
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Integer] number of currently cached patterns
|
45
|
+
def size
|
46
|
+
@mutex.synchronize { @cached.size }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'mustermann/ast/pattern'
|
2
|
+
|
3
|
+
module Mustermann
|
4
|
+
# Pyramid style pattern implementation.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# Mustermann.new('/<foo>', type: :pryamid) === '/bar' # => true
|
8
|
+
#
|
9
|
+
# @see Mustermann::Pattern
|
10
|
+
# @see file:README.md#pryamid Syntax description in the README
|
11
|
+
class Pyramid < AST::Pattern
|
12
|
+
on(nil, ?}) { |c| unexpected(c) }
|
13
|
+
|
14
|
+
on(?{) do |char|
|
15
|
+
name = expect(/\w+/, char: char)
|
16
|
+
constraint = read_brackets(?{, ?}) if scan(?:)
|
17
|
+
expect(?}) unless constraint
|
18
|
+
node(:capture, name, constraint: constraint)
|
19
|
+
end
|
20
|
+
|
21
|
+
on(?*) do |char|
|
22
|
+
node(:named_splat, expect(/\w+$/, char: char), convert: -> e { e.split(?/) })
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -15,7 +15,24 @@ module Mustermann
|
|
15
15
|
# @see (see Mustermann::Pattern#initialize)
|
16
16
|
def initialize(string, options = {})
|
17
17
|
super
|
18
|
-
|
18
|
+
regexp = compile(options)
|
19
|
+
@peek_regexp = /\A(#{regexp})/
|
20
|
+
@regexp = /\A#{regexp}\Z/
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param (see Mustermann::Pattern#peek_size)
|
24
|
+
# @return (see Mustermann::Pattern#peek_size)
|
25
|
+
# @see (see Mustermann::Pattern#peek_size)
|
26
|
+
def peek_size(string)
|
27
|
+
return unless match = peek_match(string)
|
28
|
+
match.to_s.size
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param (see Mustermann::Pattern#peek_match)
|
32
|
+
# @return (see Mustermann::Pattern#peek_match)
|
33
|
+
# @see (see Mustermann::Pattern#peek_match)
|
34
|
+
def peek_match(string)
|
35
|
+
@peek_regexp.match(string)
|
19
36
|
end
|
20
37
|
|
21
38
|
extend Forwardable
|
data/lib/mustermann/regular.rb
CHANGED
data/lib/mustermann/shell.rb
CHANGED
@@ -25,5 +25,13 @@ module Mustermann
|
|
25
25
|
def ===(string)
|
26
26
|
File.fnmatch? @string, unescape(string), @flags
|
27
27
|
end
|
28
|
+
|
29
|
+
# @param (see Mustermann::Pattern#peek_size)
|
30
|
+
# @return (see Mustermann::Pattern#peek_size)
|
31
|
+
# @see (see Mustermann::Pattern#peek_size)
|
32
|
+
def peek_size(string)
|
33
|
+
@peek_string ||= @string + "{**,/**,/**/*}"
|
34
|
+
super if File.fnmatch? @peek_string, unescape(string), @flags
|
35
|
+
end
|
28
36
|
end
|
29
37
|
end
|
data/lib/mustermann/simple.rb
CHANGED
@@ -19,7 +19,7 @@ module Mustermann
|
|
19
19
|
pattern.gsub!(/((:\w+)|\*)/) do |match|
|
20
20
|
match == "*" ? "(?<splat>.*?)" : "(?<#{$2[1..-1]}>[^/?#]+#{?? unless greedy})"
|
21
21
|
end
|
22
|
-
|
22
|
+
Regexp.new(pattern)
|
23
23
|
rescue SyntaxError, RegexpError => error
|
24
24
|
type = error.message["invalid group name"] ? CompileError : ParseError
|
25
25
|
raise type, error.message, error.backtrace
|