mustermann 0.3.1 → 0.4.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 (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