mustermann19 0.3.1 → 0.3.1.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.
- 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
|