mustermann 0.0.1 → 0.1.0
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/.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
|