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.
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