mustermann 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +429 -672
  3. data/lib/mustermann.rb +95 -20
  4. data/lib/mustermann/ast/boundaries.rb +44 -0
  5. data/lib/mustermann/ast/compiler.rb +13 -7
  6. data/lib/mustermann/ast/expander.rb +22 -12
  7. data/lib/mustermann/ast/node.rb +69 -5
  8. data/lib/mustermann/ast/param_scanner.rb +20 -0
  9. data/lib/mustermann/ast/parser.rb +138 -19
  10. data/lib/mustermann/ast/pattern.rb +59 -7
  11. data/lib/mustermann/ast/template_generator.rb +28 -0
  12. data/lib/mustermann/ast/transformer.rb +2 -2
  13. data/lib/mustermann/ast/translator.rb +20 -0
  14. data/lib/mustermann/ast/validation.rb +4 -3
  15. data/lib/mustermann/composite.rb +101 -0
  16. data/lib/mustermann/expander.rb +2 -2
  17. data/lib/mustermann/identity.rb +56 -0
  18. data/lib/mustermann/pattern.rb +185 -10
  19. data/lib/mustermann/pattern_cache.rb +49 -0
  20. data/lib/mustermann/regexp.rb +1 -0
  21. data/lib/mustermann/regexp_based.rb +18 -1
  22. data/lib/mustermann/regular.rb +4 -1
  23. data/lib/mustermann/simple_match.rb +5 -0
  24. data/lib/mustermann/sinatra.rb +22 -5
  25. data/lib/mustermann/to_pattern.rb +11 -6
  26. data/lib/mustermann/version.rb +1 -1
  27. data/mustermann.gemspec +1 -14
  28. data/spec/ast_spec.rb +14 -0
  29. data/spec/composite_spec.rb +147 -0
  30. data/spec/expander_spec.rb +15 -0
  31. data/spec/identity_spec.rb +44 -0
  32. data/spec/mustermann_spec.rb +17 -2
  33. data/spec/pattern_spec.rb +7 -3
  34. data/spec/regular_spec.rb +25 -0
  35. data/spec/sinatra_spec.rb +184 -9
  36. data/spec/to_pattern_spec.rb +49 -0
  37. metadata +15 -180
  38. data/.gitignore +0 -18
  39. data/.rspec +0 -2
  40. data/.travis.yml +0 -4
  41. data/.yardopts +0 -1
  42. data/Gemfile +0 -2
  43. data/LICENSE +0 -22
  44. data/Rakefile +0 -6
  45. data/internals.md +0 -64
  46. data/lib/mustermann/ast/tree_renderer.rb +0 -29
  47. data/lib/mustermann/rails.rb +0 -17
  48. data/lib/mustermann/shell.rb +0 -29
  49. data/lib/mustermann/simple.rb +0 -35
  50. data/lib/mustermann/template.rb +0 -47
  51. data/spec/rails_spec.rb +0 -521
  52. data/spec/shell_spec.rb +0 -108
  53. data/spec/simple_spec.rb +0 -236
  54. data/spec/support.rb +0 -5
  55. data/spec/support/coverage.rb +0 -16
  56. data/spec/support/env.rb +0 -16
  57. data/spec/support/expand_matcher.rb +0 -27
  58. data/spec/support/match_matcher.rb +0 -39
  59. data/spec/support/pattern.rb +0 -39
  60. data/spec/template_spec.rb +0 -814
@@ -1,43 +1,117 @@
1
1
  require 'mustermann/pattern'
2
+ require 'mustermann/composite'
3
+ require 'thread'
2
4
 
3
5
  # Namespace and main entry point for the Mustermann library.
4
6
  #
5
7
  # Under normal circumstances the only external API entry point you should be using is {Mustermann.new}.
6
8
  module Mustermann
7
- # @param [String, Pattern, Regexp, #to_pattern] input The representation of the new pattern
9
+ # Type to use if no type is given.
10
+ # @api private
11
+ DEFAULT_TYPE = :sinatra
12
+
13
+ # Creates a new pattern based on input.
14
+ #
15
+ # * From {Mustermann::Pattern}: returns given pattern.
16
+ # * From String: creates a pattern from the string, depending on type option (defaults to {Mustermann::Sinatra})
17
+ # * From Regexp: creates a {Mustermann::Regular} pattern.
18
+ # * From Symbol: creates a {Mustermann::Sinatra} pattern with a single named capture named after the input.
19
+ # * From an Array or multiple inputs: creates a new pattern from each element, combines them to a {Mustermann::Composite}.
20
+ # * From anything else: Will try to call to_pattern on it or raise a TypeError.
21
+ #
22
+ # Note that if the input is a {Mustermann::Pattern}, Regexp or Symbol, the type option is ignored and if to_pattern is
23
+ # called on the object, the type will be handed on but might be ignored by the input object.
24
+ #
25
+ # If you want to enforce the pattern type, you should create them via their expected class.
26
+ #
27
+ # @example creating patterns
28
+ # require 'mustermann'
29
+ #
30
+ # Mustermann.new("/:name") # => #<Mustermann::Sinatra:"/example">
31
+ # Mustermann.new("/{name}", type: :template) # => #<Mustermann::Template:"/{name}">
32
+ # Mustermann.new(/.*/) # => #<Mustermann::Regular:".*">
33
+ # Mustermann.new(:name, capture: :word) # => #<Mustermann::Sinatra:":name">
34
+ # Mustermann.new("/", "/*.jpg", type: :shell) # => #<Mustermann::Composite:(shell:"/" | shell:"/*.jpg")>
35
+ #
36
+ # @example using custom #to_pattern
37
+ # require 'mustermann'
38
+ #
39
+ # class MyObject
40
+ # def to_pattern(**options)
41
+ # Mustermann.new("/:name", **options)
42
+ # end
43
+ # end
44
+ #
45
+ # Mustermann.new(MyObject.new, type: :rails) # => #<Mustermann::Rails:"/:name">
46
+ #
47
+ # @example enforcing type
48
+ # require 'mustermann/sinatra'
49
+ #
50
+ # Mustermann::Sinatra.new("/:name")
51
+ #
52
+ # @param [String, Pattern, Regexp, Symbol, #to_pattern, Array<String, Pattern, Regexp, Symbol, #to_pattern>]
53
+ # input The representation of the pattern
8
54
  # @param [Hash] options The options hash
9
55
  # @return [Mustermann::Pattern] pattern corresponding to string.
10
56
  # @raise (see [])
11
57
  # @raise (see Mustermann::Pattern.new)
58
+ # @raise [TypeError] if the passed object cannot be converted to a pattern
12
59
  # @see file:README.md#Types_and_Options "Types and Options" in the README
13
- def self.new(input, type: :sinatra, **options)
60
+ def self.new(*input, type: DEFAULT_TYPE, **options)
61
+ type ||= DEFAULT_TYPE
62
+ input = input.first if input.size < 2
14
63
  case input
15
64
  when Pattern then input
16
65
  when Regexp then self[:regexp].new(input, **options)
17
66
  when String then self[type].new(input, **options)
18
- else input.to_pattern(type: type, **options)
67
+ when Symbol then self[:sinatra].new(input.inspect, **options)
68
+ when Array then Composite.new(input, type: type, **options)
69
+ else
70
+ pattern = input.to_pattern(type: type, **options) if input.respond_to? :to_pattern
71
+ raise TypeError, "#{input.class} can't be coerced into Mustermann::Pattern" if pattern.nil?
72
+ pattern
19
73
  end
20
74
  end
21
75
 
76
+ @mutex ||= Mutex.new
77
+ @types ||= {}
78
+
22
79
  # Maps a type to its factory.
23
80
  #
24
81
  # @example
25
82
  # Mustermann[:sinatra] # => Mustermann::Sinatra
26
83
  #
27
- # @param [Symbol] key a pattern type identifier
84
+ # @param [Symbol] name a pattern type identifier
28
85
  # @raise [ArgumentError] if the type is not supported
29
86
  # @return [Class, #new] pattern factory
30
- def self.[](key)
31
- constant, library = register.fetch(key) { raise ArgumentError, "unsupported type %p" % key }
32
- require library if library
33
- constant.respond_to?(:new) ? constant : register[key] = const_get(constant)
87
+ def self.[](name)
88
+ return name if name.respond_to? :new
89
+ @types.fetch(normalized = normalized_type(name)) do
90
+ @mutex.synchronize do
91
+ error = try_require "mustermann/#{normalized}"
92
+ @types.fetch(normalized) { raise ArgumentError, "unsupported type %p#{" (#{error.message})" if error}" % name }
93
+ end
94
+ end
95
+ end
96
+
97
+ # @return [LoadError, nil]
98
+ # @!visibility private
99
+ def self.try_require(path)
100
+ require(path)
101
+ nil
102
+ rescue LoadError => error
103
+ raise(error) unless error.path == path
104
+ error
105
+ end
106
+
107
+ # @!visibility private
108
+ def self.register(name, type)
109
+ @types[normalized_type(name)] = type
34
110
  end
35
111
 
36
112
  # @!visibility private
37
- def self.register(*identifiers, constant: identifiers.first.to_s.capitalize, load: "mustermann/#{identifiers.first}")
38
- @register ||= {}
39
- identifiers.each { |i| @register[i] = [constant, load] }
40
- @register
113
+ def self.normalized_type(type)
114
+ type.to_s.gsub('-', '_').downcase
41
115
  end
42
116
 
43
117
  # @!visibility private
@@ -46,12 +120,13 @@ module Mustermann
46
120
  require 'mustermann/extension'
47
121
  object.register Extension
48
122
  end
123
+ end
49
124
 
50
- register :identity
51
- register :rails
52
- register :regular, :regexp
53
- register :shell
54
- register :simple
55
- register :sinatra
56
- register :template
57
- end
125
+ # :nocov:
126
+ begin
127
+ require 'mustermann/visualizer' if defined?(Pry) or defined?(IRB)
128
+ rescue LoadError => error
129
+ raise error unless error.path == 'mustermann/visualizer'
130
+ $stderr.puts(error.message) if caller_locations[1].absolute_path =~ %r{/lib/pry/|/irb/|^\((?:irb|pry)\)$}
131
+ end
132
+ # :nocov:
@@ -0,0 +1,44 @@
1
+ require 'mustermann/ast/translator'
2
+
3
+ module Mustermann
4
+ module AST
5
+ # Make sure #start and #stop is set on every node and within its parents #start and #stop.
6
+ # @!visibility private
7
+ class Boundaries < Translator
8
+ # @return [Mustermann::AST::Node] the ast passed as first argument
9
+ # @!visibility private
10
+ def self.set_boundaries(ast, string: nil, start: 0, stop: string.length)
11
+ new.translate(ast, start, stop)
12
+ ast
13
+ end
14
+
15
+ translate(:node) do |start, stop|
16
+ t.set_boundaries(node, start, stop)
17
+ t(payload, node.start, node.stop)
18
+ end
19
+
20
+ translate(:with_look_ahead) do |start, stop|
21
+ t.set_boundaries(node, start, stop)
22
+ t(head, node.start, node.stop)
23
+ t(payload, node.start, node.stop)
24
+ end
25
+
26
+ translate(Array) do |start, stop|
27
+ each do |subnode|
28
+ t(subnode, start, stop)
29
+ start = subnode.stop
30
+ end
31
+ end
32
+
33
+ translate(Object) { |*| node }
34
+
35
+ # Checks that a node is within the given boundaries.
36
+ # @!visibility private
37
+ def set_boundaries(node, start, stop)
38
+ node.start = start if node.start.nil? or node.start < start
39
+ node.stop = node.start + node.min_size if node.stop.nil? or node.stop < node.start
40
+ node.stop = stop if node.stop > stop
41
+ end
42
+ end
43
+ end
44
+ end
@@ -15,6 +15,10 @@ module Mustermann
15
15
  translate(:optional) { |**o| "(?:%s)?" % t(payload, **o) }
16
16
  translate(:char) { |**o| t.encoded(payload, **o) }
17
17
 
18
+ translate :union do |**options|
19
+ "(?:%s)" % payload.map { |e| "(?:%s)" % t(e, **options) }.join(?|)
20
+ end
21
+
18
22
  translate :expression do |greedy: true, **options|
19
23
  t(payload, allow_reserved: operator.allow_reserved, greedy: greedy && !operator.allow_reserved,
20
24
  parametric: operator.parametric, separator: operator.separator, **options)
@@ -53,14 +57,14 @@ module Mustermann
53
57
  end
54
58
 
55
59
  private
56
- def qualified(string, greedy: true, **options) "#{string}+#{?? unless greedy}" end
60
+ def qualified(string, greedy: true, **options) "#{string}#{qualifier || "+#{?? unless greedy}"}" end
57
61
  def with_lookahead(string, lookahead: nil, **options) lookahead ? "(?:(?!#{lookahead})#{string})" : string end
58
62
  def from_hash(hash, **options) pattern(capture: hash[name.to_sym], **options) end
59
63
  def from_array(array, **options) Regexp.union(*array.map { |e| pattern(capture: e, **options) }) end
60
64
  def from_symbol(symbol, **options) qualified(with_lookahead("[[:#{symbol}:]]", **options), **options) end
61
65
  def from_string(string, **options) Regexp.new(string.chars.map { |c| t.encoded(c, **options) }.join) end
62
66
  def from_nil(**options) qualified(with_lookahead(default(**options), **options), **options) end
63
- def default(**options) "[^/\\?#]" end
67
+ def default(**options) constraint || "[^/\\?#]" end
64
68
  end
65
69
 
66
70
  # @!visibility private
@@ -69,7 +73,7 @@ module Mustermann
69
73
  # splats are always non-greedy
70
74
  # @!visibility private
71
75
  def pattern(**options)
72
- ".*?"
76
+ constraint || ".*?"
73
77
  end
74
78
  end
75
79
 
@@ -120,7 +124,10 @@ module Mustermann
120
124
  return Regexp.escape(char) unless uri_decode
121
125
  encoded = escape(char, escape: /./)
122
126
  list = [escape(char), encoded.downcase, encoded.upcase].uniq.map { |c| Regexp.escape(c) }
123
- list << encoded('+') if space_matches_plus and char == " "
127
+ if char == " "
128
+ list << encoded('+') if space_matches_plus
129
+ list << " "
130
+ end
124
131
  "(?:%s)" % list.join("|")
125
132
  end
126
133
 
@@ -139,9 +146,8 @@ module Mustermann
139
146
  #
140
147
  # @!visibility private
141
148
  def compile(ast, except: nil, **options)
142
- except &&= "(?!#{translate(except, no_captures: true, **options)}\\Z)"
143
- expression = "\\A#{except}#{translate(ast, **options)}\\Z"
144
- Regexp.new(expression)
149
+ except &&= "(?!#{translate(except, no_captures: true, **options)}\\Z)"
150
+ Regexp.new("#{except}#{translate(ast, **options)}")
145
151
  end
146
152
  end
147
153
 
@@ -10,21 +10,25 @@ module Mustermann
10
10
  class Expander < Translator
11
11
  raises ExpandError
12
12
 
13
- translate Array do
13
+ translate Array do |*args|
14
14
  inject(t.pattern) do |pattern, element|
15
- t.add_to(pattern, t(element))
15
+ t.add_to(pattern, t(element, *args))
16
16
  end
17
17
  end
18
18
 
19
- translate :capture do
20
- t.for_capture(node)
19
+ translate :capture do |**options|
20
+ t.for_capture(node, **options)
21
21
  end
22
22
 
23
23
  translate :named_splat, :splat do
24
24
  t.pattern + t.for_capture(node)
25
25
  end
26
26
 
27
- translate :root, :group, :expression do
27
+ translate :expression do
28
+ t(payload, allow_reserved: operator.allow_reserved)
29
+ end
30
+
31
+ translate :root, :group do
28
32
  t(payload)
29
33
  end
30
34
 
@@ -46,11 +50,15 @@ module Mustermann
46
50
  nested
47
51
  end
48
52
 
53
+ translate :union do
54
+ payload.map { |e| t(e) }.inject(:+)
55
+ end
56
+
49
57
  # helper method for captures
50
58
  # @!visibility private
51
- def for_capture(node)
59
+ def for_capture(node, **options)
52
60
  name = node.name.to_sym
53
- pattern('%s', name, name => /(?!#{pattern_for(node)})./)
61
+ pattern('%s', name, name => /(?!#{pattern_for(node, **options)})./)
54
62
  end
55
63
 
56
64
  # maps sorted key list to sprintf patterns and filters
@@ -70,7 +78,7 @@ module Mustermann
70
78
  def add(ast)
71
79
  translate(ast).each do |keys, pattern, filter|
72
80
  self.keys.concat(keys).uniq!
73
- mappings[keys.uniq.sort] ||= [keys, pattern, filter]
81
+ mappings[keys.sort] ||= [keys, pattern, filter]
74
82
  end
75
83
  end
76
84
 
@@ -82,10 +90,12 @@ module Mustermann
82
90
 
83
91
  # @see Mustermann::Pattern#expand
84
92
  # @!visibility private
85
- def expand(**values)
86
- keys, pattern, filters = mappings.fetch(values.keys.sort) { error_for(values) }
93
+ def expand(values)
94
+ values = values.each_with_object({}){ |(key, value), new_hash|
95
+ new_hash[value.instance_of?(Array) ? [key] * value.length : key] = value }
96
+ keys, pattern, filters = mappings.fetch(values.keys.flatten.sort) { error_for(values) }
87
97
  filters.each { |key, filter| values[key] &&= escape(values[key], also_escape: filter) }
88
- pattern % values.values_at(*keys)
98
+ pattern % (values[keys] || values.values_at(*keys))
89
99
  end
90
100
 
91
101
  # @see Mustermann::Pattern#expandable?
@@ -112,7 +122,7 @@ module Mustermann
112
122
  # @see Mustermann::AST::Translator#expand
113
123
  # @!visibility private
114
124
  def escape(string, *args)
115
- # URI::Parser is pretty slow, let's not had every string to it, even if it's unnecessary
125
+ # URI::Parser is pretty slow, let's not send every string to it, even if it's unnecessary
116
126
  string =~ /\A\w*\Z/ ? string : super
117
127
  end
118
128
 
@@ -4,14 +4,23 @@ module Mustermann
4
4
  # @!visibility private
5
5
  class Node
6
6
  # @!visibility private
7
- attr_accessor :payload
7
+ attr_accessor :payload, :start, :stop
8
8
 
9
9
  # @!visibility private
10
10
  # @param [Symbol] name of the node
11
11
  # @return [Class] factory for the node
12
12
  def self.[](name)
13
- @names ||= {}
14
- @names.fetch(name) { Object.const_get(constant_name(name)) }
13
+ @names ||= {}
14
+ @names[name] ||= begin
15
+ const_name = constant_name(name)
16
+ Object.const_get(const_name) if Object.const_defined?(const_name)
17
+ end
18
+ end
19
+
20
+ # Turns a class name into a node identifier.
21
+ # @!visibility private
22
+ def self.type
23
+ name[/[^:]+$/].split(/(?<=.)(?=[A-Z])/).map(&:downcase).join(?_).to_sym
15
24
  end
16
25
 
17
26
  # @!visibility private
@@ -36,6 +45,12 @@ module Mustermann
36
45
  self.payload = payload
37
46
  end
38
47
 
48
+ # @!visibility private
49
+ def is_a?(type)
50
+ type = Node[type] if type.is_a? Symbol
51
+ super(type)
52
+ end
53
+
39
54
  # Double dispatch helper for reading from the buffer into the payload.
40
55
  # @!visibility private
41
56
  def parse
@@ -58,8 +73,38 @@ module Mustermann
58
73
  yield(self) unless called
59
74
  end
60
75
 
76
+ # @return [Integer] length of the substring
77
+ # @!visibility private
78
+ def length
79
+ stop - start if start and stop
80
+ end
81
+
82
+ # @return [Integer] minimum size for a node
83
+ # @!visibility private
84
+ def min_size
85
+ 0
86
+ end
87
+
88
+ # Turns a class name into a node identifier.
89
+ # @!visibility private
90
+ def type
91
+ self.class.type
92
+ end
93
+
61
94
  # @!visibility private
62
95
  class Capture < Node
96
+ # @see Mustermann::AST::Compiler::Capture#default
97
+ # @!visibility private
98
+ attr_accessor :constraint
99
+
100
+ # @see Mustermann::AST::Compiler::Capture#qualified
101
+ # @!visibility private
102
+ attr_accessor :qualifier
103
+
104
+ # @see Mustermann::AST::Pattern#map_param
105
+ # @!visibility private
106
+ attr_accessor :convert
107
+
63
108
  # @see Mustermann::AST::Node#parse
64
109
  # @!visibility private
65
110
  def parse
@@ -73,6 +118,11 @@ module Mustermann
73
118
 
74
119
  # @!visibility private
75
120
  class Char < Node
121
+ # @return [Integer] minimum size for a node
122
+ # @!visibility private
123
+ def min_size
124
+ 1
125
+ end
76
126
  end
77
127
 
78
128
  # AST node for template expressions.
@@ -83,13 +133,21 @@ module Mustermann
83
133
  end
84
134
 
85
135
  # @!visibility private
86
- class Group < Node
136
+ class Composition < Node
87
137
  # @!visibility private
88
138
  def initialize(payload = nil, **options)
89
139
  super(Array(payload), **options)
90
140
  end
91
141
  end
92
142
 
143
+ # @!visibility private
144
+ class Group < Composition
145
+ end
146
+
147
+ # @!visibility private
148
+ class Union < Composition
149
+ end
150
+
93
151
  # @!visibility private
94
152
  class Optional < Node
95
153
  end
@@ -113,6 +171,11 @@ module Mustermann
113
171
 
114
172
  # @!visibility private
115
173
  class Separator < Node
174
+ # @return [Integer] minimum size for a node
175
+ # @!visibility private
176
+ def min_size
177
+ 1
178
+ end
116
179
  end
117
180
 
118
181
  # @!visibility private
@@ -144,7 +207,8 @@ module Mustermann
144
207
  attr_accessor :head, :at_end
145
208
 
146
209
  # @!visibility private
147
- def initialize(payload, at_end)
210
+ def initialize(payload, at_end, **options)
211
+ super(**options)
148
212
  self.head, *self.payload = Array(payload)
149
213
  self.at_end = at_end
150
214
  end