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,121 @@
1
+ require 'mustermann/ast/translator'
2
+
3
+ module Mustermann
4
+ module AST
5
+ # Takes a tree, turns it into an even better tree.
6
+ # @!visibility private
7
+ class Transformer < Translator
8
+ # @!visibility private
9
+ Operator ||= Struct.new(:separator, :allow_reserved, :prefix, :parametric)
10
+
11
+ # Operators available for expressions.
12
+ # @!visibility private
13
+ OPERATORS ||= {
14
+ nil => Operator.new(?,, false, false, false), ?+ => Operator.new(?,, true, false, false),
15
+ ?# => Operator.new(?,, true, ?#, false), ?. => Operator.new(?., false, ?., false),
16
+ ?/ => Operator.new(?/, false, ?/, false), ?; => Operator.new(?;, false, ?;, true),
17
+ ?? => Operator.new(?&, false, ??, true), ?& => Operator.new(?&, false, ?&, true)
18
+ }
19
+
20
+ # Transforms a tree.
21
+ # @note might mutate handed in tree instead of creating a new one
22
+ # @param [Mustermann::AST::Node] tree to be transformed
23
+ # @return [Mustermann::AST::Node] transformed tree
24
+ # @!visibility private
25
+ def self.transform(ast)
26
+ new.translate(ast)
27
+ end
28
+
29
+ translate(:node) { self }
30
+
31
+ translate(:expression) do
32
+ self.operator = OPERATORS.fetch(operator) { raise CompileError, "#{operator} operator not supported" }
33
+ separator = Node[:separator].new(operator.separator)
34
+ prefix = Node[:separator].new(operator.prefix)
35
+ self.payload = Array(payload.inject { |list, element| Array(list) << t(separator) << t(element) })
36
+ payload.unshift(prefix) if operator.prefix
37
+ self
38
+ end
39
+
40
+ translate(:group, :root) do
41
+ self.payload = t(payload)
42
+ self
43
+ end
44
+
45
+ # Inserts with_look_ahead nodes wherever appropriate
46
+ # @!visibility private
47
+ class ArrayTransform < NodeTranslator
48
+ register Array
49
+
50
+ # the new array
51
+ # @!visibility private
52
+ def payload
53
+ @payload ||= []
54
+ end
55
+
56
+ # buffer for potential look ahead
57
+ # @!visibility private
58
+ def lookahead_buffer
59
+ @lookahead_buffer ||= []
60
+ end
61
+
62
+ # transform the array
63
+ # @!visibility private
64
+ def translate
65
+ each { |e| track t(e) }
66
+ payload.concat create_lookahead(lookahead_buffer, true)
67
+ end
68
+
69
+ # handle a single element from the array
70
+ # @!visibility private
71
+ def track(element)
72
+ return list_for(element) << element if lookahead_buffer.empty?
73
+ return lookahead_buffer << element if lookahead? element
74
+
75
+ lookahead = lookahead_buffer.dup
76
+ lookahead = create_lookahead(lookahead, false) if element.is_a? Node[:separator]
77
+ lookahead_buffer.clear
78
+
79
+ payload.concat(lookahead) << element
80
+ end
81
+
82
+ # turn look ahead buffer into look ahead node
83
+ # @!visibility private
84
+ def create_lookahead(elements, *args)
85
+ return elements unless elements.size > 1
86
+ [Node[:with_look_ahead].new(elements, *args)]
87
+ end
88
+
89
+ # can the given element be used in a look-ahead?
90
+ # @!visibility private
91
+ def lookahead?(element, in_lookahead = false)
92
+ case element
93
+ when Node[:char] then in_lookahead
94
+ when Node[:group] then lookahead_payload?(element.payload, in_lookahead)
95
+ when Node[:optional] then lookahead?(element.payload, true) or expect_lookahead?(element.payload)
96
+ end
97
+ end
98
+
99
+ # does the list of elements look look-ahead-ish to you?
100
+ # @!visibility private
101
+ def lookahead_payload?(payload, in_lookahead)
102
+ return unless payload[0..-2].all? { |e| lookahead?(e, in_lookahead) }
103
+ expect_lookahead?(payload.last) or lookahead?(payload.last, in_lookahead)
104
+ end
105
+
106
+ # can the current element deal with a look-ahead?
107
+ # @!visibility private
108
+ def expect_lookahead?(element)
109
+ return element.class == Node[:capture] unless element.is_a? Node[:group]
110
+ element.payload.all? { |e| expect_lookahead?(e) }
111
+ end
112
+
113
+ # helper method for deciding where to put an element for now
114
+ # @!visibility private
115
+ def list_for(element)
116
+ expect_lookahead?(element) ? lookahead_buffer : payload
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,111 @@
1
+ require 'mustermann/ast/node'
2
+ require 'mustermann/error'
3
+ require 'delegate'
4
+
5
+ module Mustermann
6
+ module AST
7
+ # Implements translator pattern
8
+ #
9
+ # @abstract
10
+ # @!visibility private
11
+ class Translator
12
+ # Encapsulates a single node translation
13
+ # @!visibility private
14
+ class NodeTranslator < DelegateClass(Node)
15
+ # @param [Array<Symbol, Class>] list of types to register for.
16
+ # @!visibility private
17
+ def self.register(*types)
18
+ types.each do |type|
19
+ type = Node.constant_name(type) if type.is_a? Symbol
20
+ translator.dispatch_table[type.to_s] = self
21
+ end
22
+ end
23
+
24
+ # @param node [Mustermann::AST::Node, Object]
25
+ # @param translator [Mustermann::AST::Translator]
26
+ #
27
+ # @!visibility private
28
+ def initialize(node, translator)
29
+ @translator = translator
30
+ super(node)
31
+ end
32
+
33
+ # @!visibility private
34
+ attr_reader :translator
35
+
36
+ # shorthand for translating a nested object
37
+ # @!visibility private
38
+ def t(*args, &block)
39
+ return translator unless args.any?
40
+ translator.translate(*args, &block)
41
+ end
42
+
43
+ # @!visibility private
44
+ alias_method :node, :__getobj__
45
+ end
46
+
47
+ # maps types to translations
48
+ # @!visibility private
49
+ def self.dispatch_table
50
+ @dispatch_table ||= {}
51
+ end
52
+
53
+ # some magic sauce so {NodeTranslator}s know whom to talk to for {#register}
54
+ # @!visibility private
55
+ def self.inherited(subclass)
56
+ node_translator = Class.new(NodeTranslator)
57
+ node_translator.define_singleton_method(:translator) { subclass }
58
+ subclass.const_set(:NodeTranslator, node_translator)
59
+ super
60
+ end
61
+
62
+ # DSL-ish method for specifying the exception class to use.
63
+ # @!visibility private
64
+ def self.raises(error)
65
+ define_method(:error_class) { error }
66
+ end
67
+
68
+ # DSL method for defining single method translations.
69
+ # @!visibility private
70
+ def self.translate(*types, &block)
71
+ Class.new(const_get(:NodeTranslator)) do
72
+ register(*types)
73
+ define_method(:translate, &block)
74
+ end
75
+ end
76
+
77
+ raises Mustermann::Error
78
+
79
+ # @param [Mustermann::AST::Node, Object] object to translate
80
+ # @return decorator encapsulating translation
81
+ #
82
+ # @!visibility private
83
+ def decorator_for(node)
84
+ factory = node.class.ancestors.inject(nil) { |d,a| d || self.class.dispatch_table[a.name] }
85
+ raise error_class, "#{self.class}: Cannot translate #{node.class}" unless factory
86
+ factory.new(node, self)
87
+ end
88
+
89
+ # Start the translation dance for a (sub)tree.
90
+ # @!visibility private
91
+ def translate(node, *args, &block)
92
+ result = decorator_for(node).translate(*args, &block)
93
+ result = result.node while result.is_a? NodeTranslator
94
+ result
95
+ end
96
+
97
+ # Reusing URI::Parser instance gives > 50% performance boost for some operations, like expanding.
98
+ # @!visibility private
99
+ def uri_parser
100
+ @uri_parser ||= URI::Parser.new
101
+ end
102
+
103
+ # @return [String] escaped character
104
+ # @!visibility private
105
+ def escape(char, parser: uri_parser, escape: parser.regexp[:UNSAFE], also_escape: nil)
106
+ escape = Regexp.union(also_escape, escape) if also_escape
107
+ char =~ escape ? parser.escape(char, Regexp.union(*escape)) : char
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,29 @@
1
+ require 'mustermann/ast/translator'
2
+
3
+ module Mustermann
4
+ module AST
5
+ # Turns an AST into a human readable string.
6
+ # @!visibility private
7
+ class TreeRenderer < Translator
8
+ # @example
9
+ # Mustermann::AST::TreeRenderer.render Mustermann::Sinatra::Parser.parse('/foo')
10
+ #
11
+ # @!visibility private
12
+ def self.render(ast)
13
+ new.translate(ast)
14
+ end
15
+
16
+ translate(Object) { inspect }
17
+ translate(Array) { map { |e| "\n" << t(e) }.join.gsub("\n", "\n ") }
18
+ translate(:node) { "#{t.type(node)} #{t(payload)}" }
19
+ translate(:with_look_ahead) { "#{t.type(node)} #{t(head)} #{t(payload)}" }
20
+
21
+ # Turns a class name into a node identifier.
22
+ #
23
+ # @!visibility private
24
+ def type(node)
25
+ node.class.name[/[^:]+$/].split(/(?<=.)(?=[A-Z])/).map(&:downcase).join(?_)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,40 @@
1
+ require 'mustermann/ast/translator'
2
+
3
+ module Mustermann
4
+ module AST
5
+ # Checks the AST for certain validations, like correct capture names.
6
+ #
7
+ # Internally a poor man's visitor (abusing translator to not have to impelment a visitor).
8
+ # @!visibility private
9
+ class Validation < Translator
10
+ # Runs validations.
11
+ #
12
+ # @param [Mustermann::AST::Node] ast to be validated
13
+ # @return [Mustermann::AST::Node] the validated ast
14
+ # @raises [Mustermann::AST::CompileError] if validation fails
15
+ # @!visibility private
16
+ def self.validate(ast)
17
+ new.translate(ast)
18
+ ast
19
+ end
20
+
21
+ translate(Object, :splat) {}
22
+ translate(:node) { t(payload) }
23
+ translate(Array) { each { |p| t(p)} }
24
+
25
+ translate(:capture, :variable, :named_splat) do
26
+ raise CompileError, "capture name can't be empty" if name.nil? or name.empty?
27
+ raise CompileError, "capture name must start with underscore or lower case letter" unless name =~ /^[a-z_]/
28
+ raise CompileError, "capture name can't be #{name}" if name == "splat" or name == "captures"
29
+ raise CompileError, "can't use the same capture name twice" if t.names.include? name
30
+ t.names << name
31
+ end
32
+
33
+ # @return [Array<String>] list of capture names in tree
34
+ # @!visibility private
35
+ def names
36
+ @names ||= []
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,14 +1,6 @@
1
1
  module Mustermann
2
- # Raised if anything goes wrong while generating a {Pattern}.
3
- class Error < StandardError; end
4
-
5
- # Raised if anything goes wrong while compiling a {Pattern}.
6
- class CompileError < Error; end
7
-
8
- # Raised if anything goes wrong while parsing a {Pattern}.
9
- class ParseError < Error; end
10
-
11
- #@!visibility private
12
- class UnexpectedClosingGroup < ParseError; end
13
- private_constant :UnexpectedClosingGroup
2
+ Error ||= Class.new(StandardError) # Raised if anything goes wrong while generating a {Pattern}.
3
+ CompileError ||= Class.new(Error) # Raised if anything goes wrong while compiling a {Pattern}.
4
+ ParseError ||= Class.new(Error) # Raised if anything goes wrong while parsing a {Pattern}.
5
+ ExpandError ||= Class.new(Error) # Raised if anything goes wrong while expanding a {Pattern}.
14
6
  end
@@ -29,8 +29,8 @@ module Mustermann
29
29
  pattern[:except] = except if except
30
30
  pattern[:capture] = capture if capture
31
31
 
32
- if settings.respond_to? :pattern
33
- pattern.merge!(settings.pattern || {}) do |key, local, global|
32
+ if settings.respond_to? :pattern and settings.pattern?
33
+ pattern.merge! settings.pattern do |key, local, global|
34
34
  next local unless local.is_a? Hash
35
35
  next global.merge(local) if global.is_a? Hash
36
36
  Hash.new(global).merge! local
@@ -38,10 +38,7 @@ module Mustermann
38
38
  end
39
39
 
40
40
  path = Mustermann.new(path, **pattern)
41
-
42
- if defined? Template and path.is_a? Template
43
- condition { params.merge! path.params(request.path_info) }
44
- end
41
+ condition { params.merge! path.params(captures: Array(params[:captures]), offset: -1) }
45
42
  end
46
43
 
47
44
  super(verb, path, block, options)
@@ -6,12 +6,12 @@ module Mustermann
6
6
  # @example
7
7
  # Mustermann.new('/:foo', type: :identity) === '/bar' # => false
8
8
  #
9
- # @see Pattern
9
+ # @see Mustermann::Pattern
10
10
  # @see file:README.md#identity Syntax description in the README
11
11
  class Identity < Pattern
12
- # @param (see Pattern#===)
13
- # @return (see Pattern#===)
14
- # @see (see Pattern#===)
12
+ # @param (see Mustermann::Pattern#===)
13
+ # @return (see Mustermann::Pattern#===)
14
+ # @see (see Mustermann::Pattern#===)
15
15
  def ===(string)
16
16
  unescape(string) == @string
17
17
  end
@@ -33,7 +33,7 @@ module Mustermann
33
33
  # @param (see #initialize)
34
34
  # @raise (see #initialize)
35
35
  # @raise [ArgumentError] if some option is not supported
36
- # @return [Pattern] a new instance of Pattern
36
+ # @return [Mustermann::Pattern] a new instance of Mustermann::Pattern
37
37
  # @see #initialize
38
38
  def self.new(string, ignore_unknown_options: false, **options)
39
39
  unless ignore_unknown_options
@@ -47,7 +47,7 @@ module Mustermann
47
47
  supported_options :uri_decode, :ignore_unknown_options
48
48
 
49
49
  # @overload initialize(string, **options)
50
- # @param [String] string the string repesentation of the pattern
50
+ # @param [String] string the string representation of the pattern
51
51
  # @param [Hash] options options for fine-tuning the pattern behavior
52
52
  # @raise [Mustermann::Error] if the pattern can't be generated from the string
53
53
  # @see file:README.md#Types_and_Options "Types and Options" in the README
@@ -66,7 +66,7 @@ module Mustermann
66
66
  # @return [MatchData, Mustermann::SimpleMatch, nil] MatchData or similar object if the pattern matches.
67
67
  # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-match Regexp#match
68
68
  # @see http://ruby-doc.org/core-2.0/MatchData.html MatchData
69
- # @see SimpleMatch
69
+ # @see Mustermann::SimpleMatch
70
70
  def match(string)
71
71
  SimpleMatch.new(string) if self === string
72
72
  end
@@ -100,10 +100,10 @@ module Mustermann
100
100
 
101
101
  # @param [String] string the string to match against
102
102
  # @return [Hash{String: String, Array<String>}, nil] Sinatra style params if pattern matches.
103
- def params(string = nil, captures: nil)
103
+ def params(string = nil, captures: nil, offset: 0)
104
104
  return unless captures ||= match(string)
105
105
  params = named_captures.map do |name, positions|
106
- values = positions.map { |pos| map_param(name, captures[pos]) }.flatten
106
+ values = positions.map { |pos| map_param(name, captures[pos + offset]) }.flatten
107
107
  values = values.first if values.size < 2 and not always_array? name
108
108
  [name, values]
109
109
  end
@@ -111,6 +111,35 @@ module Mustermann
111
111
  Hash[params]
112
112
  end
113
113
 
114
+ # @note This method is only implemented by certain subclasses.
115
+ #
116
+ # @example Expanding a pattern
117
+ # pattern = Mustermann.new('/:name(.:ext)?')
118
+ # pattern.expand(name: 'hello') # => "/hello"
119
+ # pattern.expand(name: 'hello', ext: 'png') # => "/hello.png"
120
+ #
121
+ # @example Checking if a pattern supports expanding
122
+ # if pattern.respond_to? :expand
123
+ # pattern.expand(name: "foo")
124
+ # else
125
+ # warn "does not support expanding"
126
+ # end
127
+ #
128
+ # @param [Hash{Symbol: #to_s, Array<#to_s>}] **values values to use for expansion
129
+ # @return [String] expanded string
130
+ # @raise [NotImplementedError] raised if expand is not supported.
131
+ # @raise [ArgumentError] raised if a value is missing or unknown
132
+ def expand(**values)
133
+ raise NotImplementedError, "expanding not supported by #{self.class}"
134
+ end
135
+
136
+ # @!visibility private
137
+ # @return [Boolean]
138
+ # @see Object#respond_to?
139
+ def respond_to?(method, *args)
140
+ method.to_s == 'expand' ? false : super
141
+ end
142
+
114
143
  # @!visibility private
115
144
  def inspect
116
145
  "#<%p:%p>" % [self.class, @string]
@@ -1,4 +1,4 @@
1
- require 'mustermann/ast'
1
+ require 'mustermann/ast/pattern'
2
2
 
3
3
  module Mustermann
4
4
  # Rails style pattern implementation.
@@ -6,21 +6,12 @@ module Mustermann
6
6
  # @example
7
7
  # Mustermann.new('/:foo', type: :rails) === '/bar' # => true
8
8
  #
9
- # @see Pattern
9
+ # @see Mustermann::Pattern
10
10
  # @see file:README.md#rails Syntax description in the README
11
- class Rails < AST
12
- def parse_element(buffer)
13
- case char = buffer.getch
14
- when nil then unexpected("end of string")
15
- when ?) then unexpected(char, exception: UnexpectedClosingGroup)
16
- when ?* then NamedSplat.parse { buffer.scan(/\w+/) }
17
- when ?/ then Separator.new(char)
18
- when ?( then Optional.new(Group.parse { parse_buffer(buffer) })
19
- when ?: then Capture.parse { buffer.scan(/\w+/) }
20
- else Char.new(char)
21
- end
22
- end
23
-
24
- private :parse_element
11
+ class Rails < AST::Pattern
12
+ on(nil, ?)) { |c| unexpected(c) }
13
+ on(?*) { |c| node(:named_splat) { scan(/\w+/) } }
14
+ on(?() { |c| node(:optional, node(:group) { read unless scan(?)) }) }
15
+ on(?:) { |c| node(:capture) { scan(/\w+/) } }
25
16
  end
26
17
  end