mustermann 0.4.0 → 1.0.0.beta2

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