mustermann 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +2 -1
- data/.travis.yml +4 -3
- data/.yardopts +1 -0
- data/README.md +53 -10
- data/Rakefile +4 -1
- data/bench/capturing.rb +42 -9
- data/bench/template_vs_addressable.rb +3 -0
- data/internals.md +64 -0
- data/lib/mustermann.rb +14 -5
- data/lib/mustermann/ast/compiler.rb +150 -0
- data/lib/mustermann/ast/expander.rb +112 -0
- data/lib/mustermann/ast/node.rb +155 -0
- data/lib/mustermann/ast/parser.rb +136 -0
- data/lib/mustermann/ast/pattern.rb +89 -0
- data/lib/mustermann/ast/transformer.rb +121 -0
- data/lib/mustermann/ast/translator.rb +111 -0
- data/lib/mustermann/ast/tree_renderer.rb +29 -0
- data/lib/mustermann/ast/validation.rb +40 -0
- data/lib/mustermann/error.rb +4 -12
- data/lib/mustermann/extension.rb +3 -6
- data/lib/mustermann/identity.rb +4 -4
- data/lib/mustermann/pattern.rb +34 -5
- data/lib/mustermann/rails.rb +7 -16
- data/lib/mustermann/regexp_based.rb +4 -4
- data/lib/mustermann/shell.rb +4 -4
- data/lib/mustermann/simple.rb +1 -1
- data/lib/mustermann/simple_match.rb +2 -2
- data/lib/mustermann/sinatra.rb +10 -20
- data/lib/mustermann/template.rb +11 -104
- data/lib/mustermann/version.rb +1 -1
- data/mustermann.gemspec +1 -1
- data/spec/extension_spec.rb +143 -0
- data/spec/mustermann_spec.rb +41 -0
- data/spec/pattern_spec.rb +16 -6
- data/spec/rails_spec.rb +77 -9
- data/spec/sinatra_spec.rb +6 -0
- data/spec/support.rb +5 -78
- data/spec/support/coverage.rb +18 -0
- data/spec/support/env.rb +6 -0
- data/spec/support/expand_matcher.rb +27 -0
- data/spec/support/match_matcher.rb +39 -0
- data/spec/support/pattern.rb +28 -0
- metadata +43 -43
- data/.test_queue_stats +0 -0
- data/lib/mustermann/ast.rb +0 -403
- data/spec/ast_spec.rb +0 -8
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'mustermann/ast/translator'
|
2
|
+
require 'mustermann/ast/compiler'
|
3
|
+
|
4
|
+
module Mustermann
|
5
|
+
module AST
|
6
|
+
# Looks at an AST, remembers the important bits of information to do an
|
7
|
+
# ultra fast expansion.
|
8
|
+
#
|
9
|
+
# @!visibility private
|
10
|
+
class Expander < Translator
|
11
|
+
raises ExpandError
|
12
|
+
|
13
|
+
translate Array do
|
14
|
+
inject(t.pattern) do |pattern, element|
|
15
|
+
t.add_to(pattern, t(element))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
translate :capture do
|
20
|
+
t.for_capture(node)
|
21
|
+
end
|
22
|
+
|
23
|
+
translate :named_splat, :splat do
|
24
|
+
t.pattern + t.for_capture(node)
|
25
|
+
end
|
26
|
+
|
27
|
+
translate :root, :group, :expression do
|
28
|
+
t(payload)
|
29
|
+
end
|
30
|
+
|
31
|
+
translate :char do
|
32
|
+
t.pattern(t.escape(payload, also_escape: /[\/\?#\&\=%]/).gsub(?%, "%%"))
|
33
|
+
end
|
34
|
+
|
35
|
+
translate :separator do
|
36
|
+
t.pattern(payload.gsub(?%, "%%"))
|
37
|
+
end
|
38
|
+
|
39
|
+
translate :with_look_ahead do
|
40
|
+
t.add_to(t(head), t(payload))
|
41
|
+
end
|
42
|
+
|
43
|
+
translate :optional do
|
44
|
+
nested = t(payload)
|
45
|
+
nested += t.pattern unless nested.any? { |n| n.first.empty? }
|
46
|
+
nested
|
47
|
+
end
|
48
|
+
|
49
|
+
# helper method for captures
|
50
|
+
# @!visibility private
|
51
|
+
def for_capture(node)
|
52
|
+
name = node.name.to_sym
|
53
|
+
pattern('%s', name, name => /(?!#{pattern_for(node)})./)
|
54
|
+
end
|
55
|
+
|
56
|
+
# maps sorted key list to sprintf patterns and filters
|
57
|
+
# @!visibility private
|
58
|
+
def mappings
|
59
|
+
@mappings ||= {}
|
60
|
+
end
|
61
|
+
|
62
|
+
# add a tree for expansion
|
63
|
+
# @!visibility private
|
64
|
+
def add(ast)
|
65
|
+
translate(ast).each do |keys, pattern, filter|
|
66
|
+
mappings[keys.uniq.sort] ||= [keys, pattern, filter]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# helper method for getting a capture's pattern.
|
71
|
+
# @!visibility private
|
72
|
+
def pattern_for(node, **options)
|
73
|
+
Compiler.new.decorator_for(node).pattern(**options)
|
74
|
+
end
|
75
|
+
|
76
|
+
# @see Mustermann::Pattern#expand
|
77
|
+
# @!visibility private
|
78
|
+
def expand(**values)
|
79
|
+
keys, pattern, filters = mappings.fetch(values.keys.sort) { error_for(values) }
|
80
|
+
filters.each { |key, filter| values[key] &&= escape(values[key], also_escape: filter) }
|
81
|
+
pattern % values.values_at(*keys)
|
82
|
+
end
|
83
|
+
|
84
|
+
# helper method for raising an error for unexpandable values
|
85
|
+
# @!visibility private
|
86
|
+
def error_for(values)
|
87
|
+
expansions = mappings.keys.map(&:inspect).join(" or ")
|
88
|
+
raise error_class, "cannot expand with keys %p, possible expansions: %s" % [values.keys.sort, expansions]
|
89
|
+
end
|
90
|
+
|
91
|
+
# @see Mustermann::AST::Translator#expand
|
92
|
+
# @!visibility private
|
93
|
+
def escape(string, *args)
|
94
|
+
# URI::Parser is pretty slow, let's not had every string to it, even if it's uneccessary
|
95
|
+
string =~ /\A\w*\Z/ ? string : super
|
96
|
+
end
|
97
|
+
|
98
|
+
# Turns a sprintf pattern into our secret internal data strucutre.
|
99
|
+
# @!visibility private
|
100
|
+
def pattern(string = "", *keys, **filters)
|
101
|
+
[[keys, string, filters]]
|
102
|
+
end
|
103
|
+
|
104
|
+
# Creates the product of two of our secret internal data strucutres.
|
105
|
+
# @!visibility private
|
106
|
+
def add_to(list, result)
|
107
|
+
list << [[], ""] if list.empty?
|
108
|
+
list.inject([]) { |l, (k1, p1, f1)| l + result.map { |k2, p2, f2| [k1+k2, p1+p2, **f1, **f2] } }
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
module Mustermann
|
2
|
+
# @see Mustermann::AST::Pattern
|
3
|
+
module AST
|
4
|
+
# @!visibility private
|
5
|
+
class Node
|
6
|
+
# @!visibility private
|
7
|
+
attr_accessor :payload
|
8
|
+
|
9
|
+
# @!visibility private
|
10
|
+
# @param [Symbol] name of the node
|
11
|
+
# @return [Class] factory for the node
|
12
|
+
def self.[](name)
|
13
|
+
@names ||= {}
|
14
|
+
@names.fetch(name) { Object.const_get(constant_name(name)) }
|
15
|
+
end
|
16
|
+
|
17
|
+
# @!visibility private
|
18
|
+
# @param [Symbol] name of the node
|
19
|
+
# @return [String] qualified name of factory for the node
|
20
|
+
def self.constant_name(name)
|
21
|
+
return self.name if name.to_sym == :node
|
22
|
+
name = name.to_s.split(?_).map(&:capitalize).join
|
23
|
+
"#{self.name}::#{name}"
|
24
|
+
end
|
25
|
+
|
26
|
+
# Helper for creating a new instance and calling #parse on it.
|
27
|
+
# @return [Mustermann::AST::Node]
|
28
|
+
# @!visibility private
|
29
|
+
def self.parse(*args, &block)
|
30
|
+
new(*args).tap { |n| n.parse(&block) }
|
31
|
+
end
|
32
|
+
|
33
|
+
# @!visibility private
|
34
|
+
def initialize(payload = nil, **options)
|
35
|
+
options.each { |key, value| public_send("#{key}=", value) }
|
36
|
+
self.payload = payload
|
37
|
+
end
|
38
|
+
|
39
|
+
# Double dispatch helper for reading from the buffer into the payload.
|
40
|
+
# @!visibility private
|
41
|
+
def parse
|
42
|
+
self.payload ||= []
|
43
|
+
while element = yield
|
44
|
+
payload << element
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Loop through all nodes that don't have child nodes.
|
49
|
+
# @!visibility private
|
50
|
+
def each_leaf(&block)
|
51
|
+
return enum_for(__method__) unless block_given?
|
52
|
+
called = false
|
53
|
+
Array(payload).each do |entry|
|
54
|
+
next unless entry.respond_to? :each_leaf
|
55
|
+
entry.each_leaf(&block)
|
56
|
+
called = true
|
57
|
+
end
|
58
|
+
yield(self) unless called
|
59
|
+
end
|
60
|
+
|
61
|
+
# @!visibility private
|
62
|
+
class Capture < Node
|
63
|
+
# @see Mustermann::AST::Node#parse
|
64
|
+
# @!visibility private
|
65
|
+
def parse
|
66
|
+
self.payload ||= ""
|
67
|
+
super
|
68
|
+
end
|
69
|
+
|
70
|
+
# @!visibility private
|
71
|
+
alias_method :name, :payload
|
72
|
+
end
|
73
|
+
|
74
|
+
# @!visibility private
|
75
|
+
class Char < Node
|
76
|
+
end
|
77
|
+
|
78
|
+
# AST node for template expressions.
|
79
|
+
# @!visibility private
|
80
|
+
class Expression < Node
|
81
|
+
# @!visibility private
|
82
|
+
attr_accessor :operator
|
83
|
+
end
|
84
|
+
|
85
|
+
# @!visibility private
|
86
|
+
class Group < Node
|
87
|
+
# @!visibility private
|
88
|
+
def initialize(payload = nil, **options)
|
89
|
+
super(Array(payload), **options)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# @!visibility private
|
94
|
+
class Optional < Node
|
95
|
+
end
|
96
|
+
|
97
|
+
# @!visibility private
|
98
|
+
class Root < Node
|
99
|
+
# @!visibility private
|
100
|
+
attr_accessor :pattern
|
101
|
+
|
102
|
+
# Will trigger transform.
|
103
|
+
#
|
104
|
+
# @see Mustermann::AST::Node.parse
|
105
|
+
# @!visibility private
|
106
|
+
def self.parse(string, &block)
|
107
|
+
root = new
|
108
|
+
root.pattern = string
|
109
|
+
root.parse(&block)
|
110
|
+
#root.transform
|
111
|
+
root
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# @!visibility private
|
116
|
+
class Separator < Node
|
117
|
+
end
|
118
|
+
|
119
|
+
# @!visibility private
|
120
|
+
class Splat < Capture
|
121
|
+
# @see Mustermann::AST::Node::Capture#name
|
122
|
+
# @!visibility private
|
123
|
+
def name
|
124
|
+
"splat"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# @!visibility private
|
129
|
+
class NamedSplat < Splat
|
130
|
+
# @see Mustermann::AST::Node::Capture#name
|
131
|
+
# @!visibility private
|
132
|
+
alias_method :name, :payload
|
133
|
+
end
|
134
|
+
|
135
|
+
# AST node for template variables.
|
136
|
+
# @!visibility private
|
137
|
+
class Variable < Capture
|
138
|
+
# @!visibility private
|
139
|
+
attr_accessor :prefix, :explode
|
140
|
+
end
|
141
|
+
|
142
|
+
# @!visibility private
|
143
|
+
class WithLookAhead < Node
|
144
|
+
# @!visibility private
|
145
|
+
attr_accessor :head, :at_end
|
146
|
+
|
147
|
+
# @!visibility private
|
148
|
+
def initialize(payload, at_end)
|
149
|
+
self.head, *self.payload = Array(payload)
|
150
|
+
self.at_end = at_end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'mustermann/ast/node'
|
2
|
+
require 'forwardable'
|
3
|
+
require 'strscan'
|
4
|
+
|
5
|
+
module Mustermann
|
6
|
+
# @see Mustermann::AST::Pattern
|
7
|
+
module AST
|
8
|
+
# Simple, StringScanner based parser.
|
9
|
+
# @!visibility private
|
10
|
+
class Parser
|
11
|
+
# @param [String] string to be parsed
|
12
|
+
# @param [Hash] **options parse options
|
13
|
+
# @return [Mustermann::AST::Node] parse tree for string
|
14
|
+
# @!visibility private
|
15
|
+
def self.parse(string)
|
16
|
+
new.parse(string)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Defines another grammar rule for first character.
|
20
|
+
#
|
21
|
+
# @see Mustermann::Rails
|
22
|
+
# @see Mustermann::Sinatra
|
23
|
+
# @see Mustermann::Template
|
24
|
+
# @!visibility private
|
25
|
+
def self.on(*chars, &block)
|
26
|
+
chars.each do |char|
|
27
|
+
define_method("read %p" % char, &block)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Defines another grammar rule for a suffix.
|
32
|
+
#
|
33
|
+
# @see Mustermann::Sinatra
|
34
|
+
# @!visibility private
|
35
|
+
def self.suffix(pattern = /./, &block)
|
36
|
+
@suffix ||= []
|
37
|
+
@suffix << [pattern, block] if block
|
38
|
+
@suffix
|
39
|
+
end
|
40
|
+
|
41
|
+
# @!visibility private
|
42
|
+
attr_reader :buffer, :string
|
43
|
+
|
44
|
+
extend Forwardable
|
45
|
+
def_delegators :buffer, :eos?, :getch
|
46
|
+
|
47
|
+
# @param [String] string to be parsed
|
48
|
+
# @return [Mustermann::AST::Node] parse tree for string
|
49
|
+
# @!visibility private
|
50
|
+
def parse(string)
|
51
|
+
@string = string
|
52
|
+
@buffer = StringScanner.new(string)
|
53
|
+
node(:root, string) { read unless eos? }
|
54
|
+
end
|
55
|
+
|
56
|
+
# @example
|
57
|
+
# node(:char, 'x').compile =~ 'x' # => true
|
58
|
+
#
|
59
|
+
# @param [Symbol] type node type
|
60
|
+
# @return [Mustermann::AST::Node]
|
61
|
+
# @!visibility private
|
62
|
+
def node(type, *args, &block)
|
63
|
+
type = Node[type] unless type.respond_to? :new
|
64
|
+
block ? type.parse(*args, &block) : type.new(*args)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Create a node for a character we don't have an explicite rule for.
|
68
|
+
#
|
69
|
+
# @param [String] char the character
|
70
|
+
# @return [Mustermann::AST::Node] the node
|
71
|
+
# @!visibility private
|
72
|
+
def default_node(char)
|
73
|
+
char == ?/ ? node(:separator, char) : node(:char, char)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Reads the next element from the buffer.
|
77
|
+
# @return [Mustermann::AST::Node] next element
|
78
|
+
# @!visibility private
|
79
|
+
def read
|
80
|
+
char = getch
|
81
|
+
method = "read %p" % char
|
82
|
+
element = respond_to?(method) ? send(method, char) : default_node(char)
|
83
|
+
read_suffix(element)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Checks for a potential suffix on the buffer.
|
87
|
+
# @param [Mustermann::AST::Node] element node without suffix
|
88
|
+
# @return [Mustermann::AST::Node] node with suffix
|
89
|
+
# @!visibility private
|
90
|
+
def read_suffix(element)
|
91
|
+
self.class.suffix.inject(element) do |element, (regexp, callback)|
|
92
|
+
next element unless payload = scan(regexp)
|
93
|
+
instance_exec(payload, element, &callback)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Wrapper around {StringScanner#scan} that turns strings into escaped
|
98
|
+
# regular expressions and returns a MatchData if the regexp has any
|
99
|
+
# named captures.
|
100
|
+
#
|
101
|
+
# @param [Regexp, String] regexp
|
102
|
+
# @see StringScanner#scan
|
103
|
+
# @return [String, MatchData, nil]
|
104
|
+
# @!visibility private
|
105
|
+
def scan(regexp)
|
106
|
+
regexp = Regexp.new(Regexp.escape(regexp)) unless regexp.is_a? Regexp
|
107
|
+
string = buffer.scan(regexp)
|
108
|
+
regexp.names.any? ? regexp.match(string) : string
|
109
|
+
end
|
110
|
+
|
111
|
+
# Asserts a regular expression matches what's next on the buffer.
|
112
|
+
# Will return corresponding MatchData if regexp includes named captures.
|
113
|
+
#
|
114
|
+
# @param [Regexp] regexp expected to match
|
115
|
+
# @return [String, MatchData] the match
|
116
|
+
# @raise [Mustermann::ParseError] if expectation wasn't met
|
117
|
+
# @!visibility private
|
118
|
+
def expect(regexp, **options)
|
119
|
+
scan(regexp)|| unexpected(**options)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Helper for raising an exception for an unexpected character.
|
123
|
+
# Will read character from buffer if buffer is passed in.
|
124
|
+
#
|
125
|
+
# @param [String, nil] char the unexcpected character
|
126
|
+
# @raise [Mustermann::ParseError, Exception]
|
127
|
+
# @!visibility private
|
128
|
+
def unexpected(char = getch, exception: ParseError)
|
129
|
+
char = "space" if char == " "
|
130
|
+
raise exception, "unexpected #{char || "end of string"} while parsing #{string.inspect}"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
private_constant :Parser
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'mustermann/ast/parser'
|
2
|
+
require 'mustermann/ast/compiler'
|
3
|
+
require 'mustermann/ast/transformer'
|
4
|
+
require 'mustermann/ast/validation'
|
5
|
+
require 'mustermann/ast/expander'
|
6
|
+
require 'mustermann/regexp_based'
|
7
|
+
|
8
|
+
module Mustermann
|
9
|
+
# @see Mustermann::AST::Pattern
|
10
|
+
module AST
|
11
|
+
# Superclass for pattern styles that parse an AST from the string pattern.
|
12
|
+
# @abstract
|
13
|
+
class Pattern < Mustermann::RegexpBased
|
14
|
+
supported_options :capture, :except, :greedy, :space_matches_plus
|
15
|
+
|
16
|
+
extend Forwardable, SingleForwardable
|
17
|
+
single_delegate on: :parser, suffix: :parser
|
18
|
+
instance_delegate %i[parser compiler transformer validation expander_class] => 'self.class'
|
19
|
+
instance_delegate parse: :parser, transform: :transformer, validate: :validation
|
20
|
+
|
21
|
+
# @api private
|
22
|
+
# @return [#expand] expander object for pattern
|
23
|
+
# @!visibility private
|
24
|
+
attr_accessor :expander
|
25
|
+
|
26
|
+
# @api private
|
27
|
+
# @return [#parse] parser object for pattern
|
28
|
+
# @!visibility private
|
29
|
+
def self.parser
|
30
|
+
return Parser if self == AST::Pattern
|
31
|
+
const_set :Parser, Class.new(superclass.parser) unless const_defined? :Parser, false
|
32
|
+
const_get :Parser
|
33
|
+
end
|
34
|
+
|
35
|
+
# @api private
|
36
|
+
# @return [#compile] compiler object for pattern
|
37
|
+
# @!visibility private
|
38
|
+
def self.compiler
|
39
|
+
Compiler
|
40
|
+
end
|
41
|
+
|
42
|
+
# @api private
|
43
|
+
# @return [#transform] compiler object for pattern
|
44
|
+
# @!visibility private
|
45
|
+
def self.transformer
|
46
|
+
Transformer
|
47
|
+
end
|
48
|
+
|
49
|
+
# @api private
|
50
|
+
# @return [#validate] validation object for pattern
|
51
|
+
# @!visibility private
|
52
|
+
def self.validation
|
53
|
+
Validation
|
54
|
+
end
|
55
|
+
|
56
|
+
# @api private
|
57
|
+
# @return [#new] expander factory for pattern
|
58
|
+
# @!visibility private
|
59
|
+
def self.expander_class
|
60
|
+
Expander
|
61
|
+
end
|
62
|
+
|
63
|
+
# @!visibility private
|
64
|
+
def compile(string, **options)
|
65
|
+
self.expander = expander_class.new
|
66
|
+
options[:except] &&= parse options[:except]
|
67
|
+
ast = validate(transform(parse(string)))
|
68
|
+
expander.add(ast)
|
69
|
+
compiler.compile(ast, **options)
|
70
|
+
rescue CompileError => error
|
71
|
+
error.message << ": %p" % string
|
72
|
+
raise error
|
73
|
+
end
|
74
|
+
|
75
|
+
# All AST-based pattern implementations support expanding.
|
76
|
+
#
|
77
|
+
# @example (see Mustermann::Pattern#expand)
|
78
|
+
# @param (see Mustermann::Pattern#expand)
|
79
|
+
# @return (see Mustermann::Pattern#expand)
|
80
|
+
# @raise (see Mustermann::Pattern#expand)
|
81
|
+
# @see Mustermann::Pattern#expand
|
82
|
+
def expand(**values)
|
83
|
+
expander.expand(**values)
|
84
|
+
end
|
85
|
+
|
86
|
+
private :compile
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|