mustermann 0.4.0 → 1.0.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d7452f3272ea2fe09869f438b2cef90dcba6bf4b
4
- data.tar.gz: 5d79d32db97549662b82ef1b589d6d35f1f9df80
3
+ metadata.gz: 90d5c978f69743fdbee3eec3efc6bc6c05a83dfb
4
+ data.tar.gz: e777bda67af6ff598fc5a584ae786fa78eb68c4b
5
5
  SHA512:
6
- metadata.gz: 22ad9c7d0bfb6ae62a339cbfbe2581a91a9f860e4ba2f035ea26157ec0e31e16125d9f159561f43f821e2a07f239eda9e3df318ef3c29d63296a9107bcf65265
7
- data.tar.gz: 5394bb963e310669de6e569b06bc690f90060dded1ecbb991d9307c904f5dcb4d6a1c39da7683be8e79eec4782ec28502c716ae9bc528c4221ea7ae0e708434a
6
+ metadata.gz: 2b92c4b696adefa6c6c0ce16fc6a3ef7a3e85834e9579dbc8a7108fc535888da5dc907f39c087fac8abf5b26a387b81e85c7c45f161b691e40f9709e382df07b
7
+ data.tar.gz: fe32e08661344ce33c9066610e6d1591999fb62210597714e1f406b2161e4e517438d5c026fe801b84a262ec97cdbc76e471ecffae233275888aa4d1f6139814
data/README.md CHANGED
@@ -27,7 +27,7 @@ pattern.params('/a/b.c') # => { "prefix" => "a", splat => ["b", "c"] }
27
27
 
28
28
  * **[Pattern Types](#-pattern-types):** Mustermann supports a wide variety of different pattern types, making it compatible with a large variety of existing software.
29
29
  * **[Fine Grained Control](#-available-options):** You can easily adjust matching behavior and add constraints to the placeholders and capture groups.
30
- * **[Binary Operators](#-binary-operators):** Patterns can be combined into composite patterns using binary operators.
30
+ * **[Binary Operators](#-binary-operators) and [Concatenation](#-concatenation):** Patterns can be combined into composite patterns using binary operators.
31
31
  * **[Regexp Look Alike](#-regexp-look-alike):** Mustermann patterns can be used as a replacement for regular expressions.
32
32
  * **[Parameter Parsing](#-parameter-parsing):** Mustermann can parse matched parameters into a Sinatra-style "params" hash, including type casting.
33
33
  * **[Peeking](#-peeking):** Lets you check if the beginning of a string matches a pattern.
@@ -42,7 +42,6 @@ pattern.params('/a/b.c') # => { "prefix" => "a", splat => ["b", "c"] }
42
42
  These features are included in the library, but not loaded by default
43
43
 
44
44
  * **[Mapper](#-mapper):** A simple tool for mapping one string to another based on patterns.
45
- * **[Routers](#-routers):** Model execution flow based on pattern matching. Comes with a simple Rack router.
46
45
  * **[Sinatra Integration](#-sinatra-integration):** Mustermann can be used as a [Sinatra](http://www.sinatrarb.com/) extension. Sinatra 2.0 and beyond will use Mustermann by default.
47
46
 
48
47
  <a name="-pattern-types"></a>
@@ -95,6 +94,22 @@ first ^ second === "/foo/bar" # => false
95
94
 
96
95
  These resulting objects are fully functional pattern objects, allowing you to call methods like `params` or `to_proc` on them. Moreover, *or* patterns created solely from expandable patterns will also be expandable. The same logic also applies to generating templates from *or* patterns.
97
96
 
97
+ <a name="-concatenation"></a>
98
+ ## Concatenation
99
+
100
+ Similar to [Binary Operators](#-binary-operators), two patterns can be concatenated using `+`.
101
+
102
+ ``` ruby
103
+ require 'mustermann'
104
+
105
+ prefix = Mustermann.new("/:prefix")
106
+ about = prefix + "/about"
107
+
108
+ about.params("/main/about") # => {"prefix" => "main"}
109
+ ```
110
+
111
+ Patterns of different types can be mixed. The availability of `to_templates` and `expand` depends on the patterns being concatenated.
112
+
98
113
  <a name="-regexp-look-alike"></a>
99
114
  ## Regexp Look Alike
100
115
 
@@ -285,7 +300,40 @@ pattern.expand(:append, slug: "foo", value: "bar") # => "/foo?value=bar"
285
300
  <a name="-generating-templates"></a>
286
301
  ## Generating Templates
287
302
 
288
- ... TODO ...
303
+ You can generate a list of URI templates that correspond to a Mustermann pattern (it is a list rather than a single template, as most pattern types are significantly more expressive than URI templates).
304
+
305
+ This comes in quite handy since URI templates are not made for pattern matching. That way you can easily use a more precise template syntax and have it automatically generate hypermedia links for you.
306
+
307
+ Template generation is supported by almost all patterns (notable exceptions are `shell`, `regexp` and `simple` patterns).
308
+
309
+ ``` ruby
310
+ require 'mustermann'
311
+
312
+ Mustermann.new("/:name").to_templates # => ["/{name}"]
313
+ Mustermann.new("/:foo(@:bar)?/*baz").to_templates # => ["/{foo}@{bar}/{+baz}", "/{foo}/{+baz}"]
314
+ Mustermann.new("/{name}", type: :template).to_templates # => ["/{name}"
315
+ ```
316
+
317
+ Union Composite patterns (with the | operator) support template generation if all patterns they are composed of also support it.
318
+
319
+ ``` ruby
320
+ require 'mustermann'
321
+
322
+ pattern = Mustermann.new('/:name')
323
+ pattern |= Mustermann.new('/{name}', type: :template)
324
+ pattern |= Mustermann.new('/example/*nested')
325
+ pattern.to_templates # => ["/{name}", "/example/{+nested}"]
326
+ ```
327
+
328
+ If accepting arbitrary patterns, you can and should use `respond_to?` to check feature availability.
329
+
330
+ ``` ruby
331
+ if pattern.respond_to? :to_templates
332
+ pattern.to_templates
333
+ else
334
+ warn "does not support template generation"
335
+ end
336
+ ```
289
337
 
290
338
  <a name="-proc-look-alike"></a>
291
339
  ## Proc Look Alike
@@ -326,47 +374,6 @@ mapper['/foo.xml'] # => "/foo/view.xml"
326
374
  mapper['/foo/bar'] # => "/foo/bar"
327
375
  ```
328
376
 
329
- <a name="-routers"></a>
330
- ## Routers
331
-
332
- Mustermann comes with basic router implementations that will call certain callbacks depending on the input.
333
-
334
- ### Simple Router
335
-
336
- The simple router chooses callbacks based on an input string.
337
-
338
- ``` ruby
339
- require 'mustermann/router/simple'
340
-
341
- router = Mustermann::Router::Simple.new(default: 42)
342
- router.on(':name', capture: :digit) { |string| string.to_i }
343
- router.call("23") # => 23
344
- router.call("example") # => 42
345
- ```
346
-
347
- ### Rack Router
348
-
349
- This is not a full replacement for Rails, Sinatra, Cuba, etc, as it only cares about path based routing.
350
-
351
- ``` ruby
352
- require 'mustermann/router/rack'
353
-
354
- router = Mustermann::Router::Rack.new do
355
- on '/' do |env|
356
- [200, {'Content-Type' => 'text/plain'}, ['Hello World!']]
357
- end
358
-
359
- on '/:name' do |env|
360
- name = env['mustermann.params']['name']
361
- [200, {'Content-Type' => 'text/plain'}, ["Hello #{name}!"]]
362
- end
363
-
364
- on '/something/*', call: SomeApp
365
- end
366
-
367
- # in a config.ru
368
- run router
369
- ```
370
377
  <a name="-sinatra-integration"></a>
371
378
  ## Sinatra Integration
372
379
 
@@ -747,7 +754,15 @@ This comes with a few trade-offs:
747
754
 
748
755
  **Supported options:**
749
756
  [`uri_decode`](#-available-options--uri_decode),
750
- [`ignore_unknown_options`](#-available-options--ignore_unknown_options).
757
+ [`ignore_unknown_options`](#-available-options--ignore_unknown_options), `check_anchors`.
758
+
759
+ The pattern string (or actual Regexp instance) should not contain anchors (`^` outside of square brackets, `$`, `\A`, `\z`, or `\Z`).
760
+ Anchors will be injected where necessary by Mustermann.
761
+
762
+ By default, Mustermann will raise a `Mustermann::CompileError` if an anchor is encountered.
763
+ If you still want it to contain anchors at your own risk, set the `check_anchors` option to `false`.
764
+
765
+ Using anchors will break [peeking](#-peeking) and [concatenation](#-concatenation).
751
766
 
752
767
  <table>
753
768
  <thead>
@@ -811,7 +826,7 @@ This comes with a few trade-offs:
811
826
  </td>
812
827
  </tr>
813
828
  <tr>
814
- <td><b>(</b><i>expression</i><b>|</b><i>expression</i><b>|</b><i>...</i><b>)</b></td>
829
+ <td><i>expression</i><b>|</b><i>expression</i><b>|</b><i>...</i></td>
815
830
  <td>
816
831
  Will match anything matching the nested expressions. May contain any other syntax element, including captures.
817
832
  </td>
@@ -1,5 +1,6 @@
1
1
  require 'mustermann/pattern'
2
2
  require 'mustermann/composite'
3
+ require 'mustermann/concat'
3
4
  require 'thread'
4
5
 
5
6
  # Namespace and main entry point for the Mustermann library.
@@ -57,7 +58,7 @@ module Mustermann
57
58
  # @raise (see Mustermann::Pattern.new)
58
59
  # @raise [TypeError] if the passed object cannot be converted to a pattern
59
60
  # @see file:README.md#Types_and_Options "Types and Options" in the README
60
- def self.new(*input, type: DEFAULT_TYPE, **options)
61
+ def self.new(*input, type: DEFAULT_TYPE, operator: :|, **options)
61
62
  type ||= DEFAULT_TYPE
62
63
  input = input.first if input.size < 2
63
64
  case input
@@ -65,7 +66,7 @@ module Mustermann
65
66
  when Regexp then self[:regexp].new(input, **options)
66
67
  when String then self[type].new(input, **options)
67
68
  when Symbol then self[:sinatra].new(input.inspect, **options)
68
- when Array then Composite.new(input, type: type, **options)
69
+ when Array then input.map { |i| new(i, type: type, **options) }.inject(operator)
69
70
  else
70
71
  pattern = input.to_pattern(type: type, **options) if input.respond_to? :to_pattern
71
72
  raise TypeError, "#{input.class} can't be coerced into Mustermann::Pattern" if pattern.nil?
@@ -84,6 +84,8 @@ module Mustermann
84
84
  # @!visibility private
85
85
  def translate(**options)
86
86
  return super(**options) if explode or not options[:parametric]
87
+ # Remove this line after fixing broken compatibility between 2.1 and 2.2
88
+ options.delete(:parametric) if options.has_key?(:parametric)
87
89
  parametric super(parametric: false, **options)
88
90
  end
89
91
 
@@ -152,6 +152,10 @@ module Mustermann
152
152
  class Optional < Node
153
153
  end
154
154
 
155
+ # @!visibility private
156
+ class Or < Node
157
+ end
158
+
155
159
  # @!visibility private
156
160
  class Root < Node
157
161
  # @!visibility private
@@ -124,25 +124,8 @@ module Mustermann
124
124
  # @return [String, MatchData, nil]
125
125
  # @!visibility private
126
126
  def scan(regexp)
127
- match_buffer(:scan, regexp)
128
- end
129
-
130
- # Wrapper around {StringScanner#check} that turns strings into escaped
131
- # regular expressions and returns a MatchData if the regexp has any
132
- # named captures.
133
- #
134
- # @param [Regexp, String] regexp
135
- # @see StringScanner#check
136
- # @return [String, MatchData, nil]
137
- # @!visibility private
138
- def check(regexp)
139
- match_buffer(:check, regexp)
140
- end
141
-
142
- # @!visibility private
143
- def match_buffer(method, regexp)
144
127
  regexp = Regexp.new(Regexp.escape(regexp)) unless regexp.is_a? Regexp
145
- string = buffer.public_send(method, regexp)
128
+ string = buffer.scan(regexp)
146
129
  regexp.names.any? ? regexp.match(string) : string
147
130
  end
148
131
 
@@ -245,10 +228,6 @@ module Mustermann
245
228
  char = "space" if char == " "
246
229
  raise exception, "unexpected #{char || "end of string"} while parsing #{string.inspect}"
247
230
  end
248
-
249
- private :match_buffer
250
231
  end
251
-
252
- private_constant :Parser
253
232
  end
254
233
  end
@@ -7,7 +7,7 @@ require 'mustermann/ast/template_generator'
7
7
  require 'mustermann/ast/param_scanner'
8
8
  require 'mustermann/regexp_based'
9
9
  require 'mustermann/expander'
10
- require 'tool/equality_map'
10
+ require 'mustermann/equality_map'
11
11
 
12
12
  module Mustermann
13
13
  # @see Mustermann::AST::Pattern
@@ -86,7 +86,7 @@ module Mustermann
86
86
  # Internal AST representation of pattern.
87
87
  # @!visibility private
88
88
  def to_ast
89
- @ast_cache ||= Tool::EqualityMap.new
89
+ @ast_cache ||= EqualityMap.new
90
90
  @ast_cache.fetch(@string) do
91
91
  ast = parse(@string, pattern: self)
92
92
  ast &&= transform(ast)
@@ -133,4 +133,4 @@ module Mustermann
133
133
  private :compile, :parse, :transform, :validate, :generate_templates, :param_converters, :scan_params, :set_boundaries
134
134
  end
135
135
  end
136
- end
136
+ end
@@ -15,10 +15,59 @@ module Mustermann
15
15
  new.translate(tree)
16
16
  end
17
17
 
18
- translate(:node) { self }
19
- translate(:group, :root) do
20
- self.payload = t(payload)
21
- self
18
+ # recursive descent
19
+ translate(:node) do
20
+ node.payload = t(payload)
21
+ node
22
+ end
23
+
24
+ # ignore unknown objects on the tree
25
+ translate(Object) { node }
26
+
27
+ # turn a group containing or nodes into a union
28
+ # @!visibility private
29
+ class GroupTransformer < NodeTranslator
30
+ register :group
31
+
32
+ # @!visibility private
33
+ def translate
34
+ payload.flatten! if payload.is_a?(Array)
35
+ return union if payload.any? { |e| e.is_a? :or }
36
+ self.payload = t(payload)
37
+ self
38
+ end
39
+
40
+ # @!visibility private
41
+ def union
42
+ groups = split_payload.map { |g| group(g) }
43
+ Node[:union].new(groups, start: node.start, stop: node.stop)
44
+ end
45
+
46
+ # @!visibility private
47
+ def group(elements)
48
+ return t(elements.first) if elements.size == 1
49
+ start, stop = elements.first.start, elements.last.stop if elements.any?
50
+ Node[:group].new(t(elements), start: start, stop: stop)
51
+ end
52
+
53
+ # @!visibility private
54
+ def split_payload
55
+ groups = [[]]
56
+ payload.each { |e| e.is_a?(:or) ? groups << [] : groups.last << e }
57
+ groups.map!
58
+ end
59
+ end
60
+
61
+ # inject a union node right inside the root node if it contains or nodes
62
+ # @!visibility private
63
+ class RootTransformer < GroupTransformer
64
+ register :root
65
+
66
+ # @!visibility private
67
+ def union
68
+ self.payload = [super]
69
+ self
70
+ end
22
71
  end
23
72
 
24
73
  # URI expression transformations depending on operator
@@ -22,6 +22,7 @@ module Mustermann
22
22
  # @param [Array<Symbol, Regexp, #cast, #===>] types identifier for cast type (some need block)
23
23
  # @!visibility private
24
24
  def register(*types, &block)
25
+ return if types.empty? and block.nil?
25
26
  types << Any.new(&block) if types.empty?
26
27
  types.each { |type| self << caster_for(type, &block) }
27
28
  end
@@ -41,6 +42,7 @@ module Mustermann
41
42
  # @return [Hash] post-transform Hash
42
43
  # @!visibility private
43
44
  def cast(hash)
45
+ return hash if empty?
44
46
  merge = {}
45
47
  hash.delete_if do |key, value|
46
48
  next unless casted = lazy.map { |e| e.cast(key, value) }.detect { |e| e }
@@ -50,16 +52,6 @@ module Mustermann
50
52
  hash.update(merge)
51
53
  end
52
54
 
53
- # Specific cast for remove nil values.
54
- # @!visibility private
55
- module Nil
56
- # @see Mustermann::Caster#cast
57
- # @!visibility private
58
- def self.cast(key, value)
59
- {} if value.nil?
60
- end
61
- end
62
-
63
55
  # Class for block based casts that are triggered for every key/value pair.
64
56
  # @!visibility private
65
57
  class Any
@@ -8,9 +8,9 @@ module Mustermann
8
8
  supported_options :operator, :type
9
9
 
10
10
  # @see Mustermann::Pattern.supported?
11
- def self.supported?(option, **options)
11
+ def self.supported?(option, type: nil, **options)
12
12
  return true if super
13
- options[:type] and Mustermann[options[:type]].supported?(option, **options)
13
+ Mustermann[type || Mustermann::DEFAULT_TYPE].supported?(option, **options)
14
14
  end
15
15
 
16
16
  # @return [Mustermann::Pattern] a new composite pattern
@@ -33,6 +33,16 @@ module Mustermann
33
33
  patterns == patterns_from(pattern)
34
34
  end
35
35
 
36
+ # @see Mustermann::Pattern#eql?
37
+ def eql?(pattern)
38
+ patterns.eql? patterns_from(pattern)
39
+ end
40
+
41
+ # @see Mustermann::Pattern#hash
42
+ def hash
43
+ patterns.hash | operator.hash
44
+ end
45
+
36
46
  # @see Mustermann::Pattern#===
37
47
  def ===(string)
38
48
  patterns.map { |p| p === string }.inject(operator)
@@ -61,7 +71,7 @@ module Mustermann
61
71
  @expander.expand(behavior, values)
62
72
  end
63
73
 
64
- # (see Mustermann::Pattern#expand)
74
+ # (see Mustermann::Pattern#to_templates)
65
75
  def to_templates
66
76
  raise NotImplementedError, 'template generation not supported' unless respond_to? :to_templates
67
77
  patterns.flat_map(&:to_templates).uniq
@@ -0,0 +1,124 @@
1
+ module Mustermann
2
+ # Class for pattern objects that are a concatenation of other patterns.
3
+ # @see Mustermann::Pattern#+
4
+ class Concat < Composite
5
+ # Mixin for patterns to support native concatenation.
6
+ # @!visibility private
7
+ module Native
8
+ # @see Mustermann::Pattern#+
9
+ # @!visibility private
10
+ def +(other)
11
+ other &&= Mustermann.new(other, type: :identity, **options)
12
+ return super unless native = native_concat(other)
13
+ self.class.new(native, **options)
14
+ end
15
+
16
+ # @!visibility private
17
+ def native_concat(other)
18
+ "#{self}#{other}" if native_concat?(other)
19
+ end
20
+
21
+ # @!visibility private
22
+ def native_concat?(other)
23
+ other.class == self.class and other.options == options
24
+ end
25
+
26
+ private :native_concat, :native_concat?
27
+ end
28
+
29
+ # Should not be used directly.
30
+ # @!visibility private
31
+ def initialize(*)
32
+ super
33
+ AST::Validation.validate(combined_ast) if respond_to? :expand
34
+ end
35
+
36
+ # @see Mustermann::Composite#operator
37
+ # @return [Symbol] always :+
38
+ def operator
39
+ :+
40
+ end
41
+
42
+ # @see Mustermann::Pattern#===
43
+ def ===(string)
44
+ peek_size(string) == string.size
45
+ end
46
+
47
+ # @see Mustermann::Pattern#match
48
+ def match(string)
49
+ peeked = peek_match(string)
50
+ peeked if peeked.to_s == string
51
+ end
52
+
53
+ # @see Mustermann::Pattern#params
54
+ def params(string)
55
+ params, size = peek_params(string)
56
+ params if size == string.size
57
+ end
58
+
59
+ # @see Mustermann::Pattern#peek_size
60
+ def peek_size(string)
61
+ pump(string) { |p,s| p.peek_size(s) }
62
+ end
63
+
64
+ # @see Mustermann::Pattern#peek_match
65
+ def peek_match(string)
66
+ pump(string, initial: SimpleMatch.new) do |pattern, substring|
67
+ return unless match = pattern.peek_match(substring)
68
+ [match, match.to_s.size]
69
+ end
70
+ end
71
+
72
+ # @see Mustermann::Pattern#peek_params
73
+ def peek_params(string)
74
+ pump(string, inject_with: :merge, with_size: true) { |p, s| p.peek_params(s) }
75
+ end
76
+
77
+ # (see Mustermann::Pattern#expand)
78
+ def expand(behavior = nil, values = {})
79
+ raise NotImplementedError, 'expanding not supported' unless respond_to? :expand
80
+ @expander ||= Mustermann::Expander.new(self) { combined_ast }
81
+ @expander.expand(behavior, values)
82
+ end
83
+
84
+ # (see Mustermann::Pattern#to_templates)
85
+ def to_templates
86
+ raise NotImplementedError, 'template generation not supported' unless respond_to? :to_templates
87
+ @to_templates ||= patterns.inject(['']) { |list, pattern| list.product(pattern.to_templates).map(&:join) }.uniq
88
+ end
89
+
90
+ # @!visibility private
91
+ def respond_to_special?(method)
92
+ method = :to_ast if method.to_sym == :expand
93
+ patterns.all? { |p| p.respond_to?(method) }
94
+ end
95
+
96
+ # used to generate results for various methods by scanning through an input string
97
+ # @!visibility private
98
+ def pump(string, inject_with: :+, initial: nil, with_size: false)
99
+ substring = string
100
+ results = Array(initial)
101
+
102
+ patterns.each do |pattern|
103
+ result, size = yield(pattern, substring)
104
+ return unless result
105
+ results << result
106
+ size ||= result
107
+ substring = substring[size..-1]
108
+ end
109
+
110
+ results = results.inject(inject_with)
111
+ with_size ? [results, string.size - substring.size] : results
112
+ end
113
+
114
+ # generates one big AST from all patterns
115
+ # will not check if patterns support AST generation
116
+ # @!visibility private
117
+ def combined_ast
118
+ payload = patterns.map { |p| AST::Node[:group].new(p.to_ast.payload) }
119
+ AST::Node[:root].new(payload)
120
+ end
121
+
122
+ private :combined_ast, :pump
123
+ end
124
+ end