mustermann 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -1
  3. data/.travis.yml +4 -3
  4. data/.yardopts +1 -0
  5. data/README.md +53 -10
  6. data/Rakefile +4 -1
  7. data/bench/capturing.rb +42 -9
  8. data/bench/template_vs_addressable.rb +3 -0
  9. data/internals.md +64 -0
  10. data/lib/mustermann.rb +14 -5
  11. data/lib/mustermann/ast/compiler.rb +150 -0
  12. data/lib/mustermann/ast/expander.rb +112 -0
  13. data/lib/mustermann/ast/node.rb +155 -0
  14. data/lib/mustermann/ast/parser.rb +136 -0
  15. data/lib/mustermann/ast/pattern.rb +89 -0
  16. data/lib/mustermann/ast/transformer.rb +121 -0
  17. data/lib/mustermann/ast/translator.rb +111 -0
  18. data/lib/mustermann/ast/tree_renderer.rb +29 -0
  19. data/lib/mustermann/ast/validation.rb +40 -0
  20. data/lib/mustermann/error.rb +4 -12
  21. data/lib/mustermann/extension.rb +3 -6
  22. data/lib/mustermann/identity.rb +4 -4
  23. data/lib/mustermann/pattern.rb +34 -5
  24. data/lib/mustermann/rails.rb +7 -16
  25. data/lib/mustermann/regexp_based.rb +4 -4
  26. data/lib/mustermann/shell.rb +4 -4
  27. data/lib/mustermann/simple.rb +1 -1
  28. data/lib/mustermann/simple_match.rb +2 -2
  29. data/lib/mustermann/sinatra.rb +10 -20
  30. data/lib/mustermann/template.rb +11 -104
  31. data/lib/mustermann/version.rb +1 -1
  32. data/mustermann.gemspec +1 -1
  33. data/spec/extension_spec.rb +143 -0
  34. data/spec/mustermann_spec.rb +41 -0
  35. data/spec/pattern_spec.rb +16 -6
  36. data/spec/rails_spec.rb +77 -9
  37. data/spec/sinatra_spec.rb +6 -0
  38. data/spec/support.rb +5 -78
  39. data/spec/support/coverage.rb +18 -0
  40. data/spec/support/env.rb +6 -0
  41. data/spec/support/expand_matcher.rb +27 -0
  42. data/spec/support/match_matcher.rb +39 -0
  43. data/spec/support/pattern.rb +28 -0
  44. metadata +43 -43
  45. data/.test_queue_stats +0 -0
  46. data/lib/mustermann/ast.rb +0 -403
  47. 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