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.
@@ -0,0 +1,26 @@
1
+ module Mustermann
2
+ class Sinatra < AST::Pattern
3
+ # Generates a string that can safely be concatenated with other strings
4
+ # without chaning its semantics
5
+ # @see #safe_string
6
+ # @!visibility private
7
+ SafeRenderer = AST::Translator.create do
8
+ translate(:splat, :named_splat) { "{+#{name}}" }
9
+ translate(:char, :separator) { Sinatra.escape(payload) }
10
+ translate(:root) { t(payload) }
11
+ translate(:group) { "(#{t(payload)})" }
12
+ translate(:union) { "(#{t(payload, join: ?|)})" }
13
+ translate(:optional) { "#{t(payload)}?" }
14
+ translate(Array) { |join: ""| map { |e| t(e) }.join(join) }
15
+
16
+ translate(:capture) do
17
+ raise Mustermann::Error, 'cannot render variables' if node.is_a? :variable
18
+ raise Mustermann::Error, 'cannot translate constraints' if constraint or qualifier or convert
19
+ prefix = node.is_a?(:splat) ? "+" : ""
20
+ "{#{prefix}#{name}}"
21
+ end
22
+ end
23
+
24
+ private_constant :SafeRenderer
25
+ end
26
+ end
@@ -0,0 +1,48 @@
1
+ module Mustermann
2
+ class Sinatra < AST::Pattern
3
+ # Tries to translate objects to Sinatra patterns.
4
+ # @!visibility private
5
+ class TryConvert < AST::Translator
6
+ # @return [Mustermann::Sinatra, nil]
7
+ # @!visibility private
8
+ def self.convert(input, **options)
9
+ new(options).translate(input)
10
+ end
11
+
12
+ # Expected options for the resulting pattern.
13
+ # @!visibility private
14
+ attr_reader :options
15
+
16
+ # @!visibility private
17
+ def initialize(options)
18
+ @options = options
19
+ end
20
+
21
+ # @return [Mustermann::Sinatra]
22
+ # @!visibility private
23
+ def new(input, escape = false)
24
+ input = Mustermann::Sinatra.escape(input) if escape
25
+ Mustermann::Sinatra.new(input, **options)
26
+ end
27
+
28
+ # @return [true, false] whether or not expected pattern should have uri_decode option set
29
+ # @!visibility private
30
+ def uri_decode
31
+ options.fetch(:uri_decode, true)
32
+ end
33
+
34
+ translate(Object) { nil }
35
+ translate(String) { t.new(self, true) }
36
+
37
+ translate(Identity) { t.new(self, true) if uri_decode == t.uri_decode }
38
+ translate(Sinatra) { node if options == t.options }
39
+
40
+ translate AST::Pattern do
41
+ next unless options == t.options
42
+ t.new(SafeRenderer.translate(to_ast)) rescue nil
43
+ end
44
+ end
45
+
46
+ private_constant :TryConvert
47
+ end
48
+ end
@@ -1,3 +1,3 @@
1
1
  module Mustermann
2
- VERSION ||= '0.4.0'
2
+ VERSION ||= '1.0.0.beta2'
3
3
  end
@@ -10,9 +10,8 @@ Gem::Specification.new do |s|
10
10
  s.summary = %q{use patterns like regular expressions}
11
11
  s.description = %q{library implementing patterns that behave like regular expressions}
12
12
  s.license = 'MIT'
13
- s.required_ruby_version = '>= 2.1.0'
13
+ s.required_ruby_version = '>= 2.2.0'
14
14
  s.files = `git ls-files`.split("\n")
15
15
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
16
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
- s.add_dependency 'tool', '~> 0.2'
18
17
  end
@@ -12,6 +12,14 @@ describe Mustermann::Composite do
12
12
  pattern = Mustermann.new('/foo')
13
13
  Mustermann::Composite.new(pattern).should be == pattern
14
14
  end
15
+
16
+ example 'with supported type specific arguments' do
17
+ Mustermann::Composite.new("/a", "/b", greedy: true)
18
+ end
19
+
20
+ example 'with unsupported type specific arguments' do
21
+ expect { Mustermann::Composite.new("/a", "/b", greedy: true, type: :identity) }.to raise_error(ArgumentError)
22
+ end
15
23
  end
16
24
 
17
25
  context :| do
@@ -64,6 +72,13 @@ describe Mustermann::Composite do
64
72
  example { expect { subject.to_templates }.to raise_error(NotImplementedError) }
65
73
  end
66
74
  end
75
+
76
+ describe :eql? do
77
+ example { should be_eql(pattern) }
78
+ example { should be_eql(Mustermann.new('/foo/:name', '/:first/:second', operator: :|)) }
79
+ example { should_not be_eql(Mustermann.new('/bar/:name', '/:first/:second', operator: :|)) }
80
+ example { should_not be_eql(Mustermann.new('/foo/:name', '/:first/:second', operator: :&)) }
81
+ end
67
82
  end
68
83
 
69
84
  context :& do
@@ -136,12 +151,12 @@ describe Mustermann::Composite do
136
151
 
137
152
  describe :inspect do
138
153
  let(:sinatra) { Mustermann.new('x') }
139
- let(:rails) { Mustermann.new('x', type: :rails) }
154
+ let(:shell) { Mustermann.new('x', type: :shell) }
140
155
  let(:identity) { Mustermann.new('x', type: :identity) }
141
156
 
142
- example { (sinatra | rails) .inspect.should include('(sinatra:"x" | rails:"x")') }
143
- example { (sinatra ^ rails) .inspect.should include('(sinatra:"x" ^ rails:"x")') }
144
- example { (sinatra | rails | identity) .inspect.should include('(sinatra:"x" | rails:"x" | identity:"x")') }
145
- example { (sinatra | rails & identity) .inspect.should include('(sinatra:"x" | (rails:"x" & identity:"x"))') }
157
+ example { (sinatra | shell) .inspect.should include('(sinatra:"x" | shell:"x")') }
158
+ example { (sinatra ^ shell) .inspect.should include('(sinatra:"x" ^ shell:"x")') }
159
+ example { (sinatra | shell | identity) .inspect.should include('(sinatra:"x" | shell:"x" | identity:"x")') }
160
+ example { (sinatra | shell & identity) .inspect.should include('(sinatra:"x" | (shell:"x" & identity:"x"))') }
146
161
  end
147
162
  end
@@ -0,0 +1,114 @@
1
+ require 'support'
2
+ require 'mustermann'
3
+
4
+ describe Mustermann::Concat do
5
+ describe Mustermann::Concat::Native do
6
+ context "sinatra + sinatra" do
7
+ subject(:pattern) { Mustermann.new("/:foo") + Mustermann.new("/:bar") }
8
+ its(:class) { should be == Mustermann::Sinatra }
9
+ its(:to_s) { should be == "/{foo}/{bar}" }
10
+ end
11
+
12
+ context "sinatra + string" do
13
+ subject(:pattern) { Mustermann.new("/:foo") + "/:bar" }
14
+ its(:class) { should be == Mustermann::Sinatra }
15
+ its(:to_s) { should be == "/{foo}/\\:bar" }
16
+ end
17
+
18
+ context "regular + regular" do
19
+ subject(:pattern) { Mustermann.new(/foo/) + Mustermann.new(/bar/) }
20
+ its(:class) { should be == Mustermann::Regular }
21
+ its(:to_s) { should be == "foobar" }
22
+ end
23
+
24
+ context "sinatra + rails" do
25
+ subject(:pattern) { Mustermann.new("/:foo") + Mustermann.new("/:bar", type: :rails) }
26
+ its(:class) { should be == Mustermann::Sinatra }
27
+ its(:to_s) { should be == "/{foo}/{bar}" }
28
+ end
29
+
30
+ context "sinatra + flask" do
31
+ subject(:pattern) { Mustermann.new("/:foo") + Mustermann.new("/<bar>", type: :flask) }
32
+ its(:class) { should be == Mustermann::Sinatra }
33
+ its(:to_s) { should be == "/{foo}/{bar}" }
34
+ end
35
+
36
+ context "sinatra + flask (typed)" do
37
+ subject(:pattern) { Mustermann.new("/:foo") + Mustermann.new("/<int:bar>", type: :flask) }
38
+ its(:class) { should be == Mustermann::Concat }
39
+ its(:to_s) { should be == '(sinatra:"/:foo" + flask:"/<int:bar>")' }
40
+ end
41
+
42
+ context "sinatra + sinatra (different options)" do
43
+ subject(:pattern) { Mustermann.new("/:foo") + Mustermann.new("/:bar", uri_decode: false) }
44
+ its(:class) { should be == Mustermann::Concat }
45
+ its(:to_s) { should be == '(sinatra:"/:foo" + sinatra:"/:bar")' }
46
+ end
47
+
48
+ context "sinatra + rails (different options)" do
49
+ subject(:pattern) { Mustermann.new("/:foo") + Mustermann.new("/:bar", type: :rails, uri_decode: false) }
50
+ its(:class) { should be == Mustermann::Concat }
51
+ its(:to_s) { should be == '(sinatra:"/:foo" + rails:"/:bar")' }
52
+ end
53
+
54
+ context "sinatra + rails (different options) + sinatra" do
55
+ subject(:pattern) { Mustermann.new("/:foo") + Mustermann.new("/:bar", type: :rails, uri_decode: false) + Mustermann.new("/:baz") }
56
+ its(:class) { should be == Mustermann::Concat }
57
+ its(:to_s) { should be == '(sinatra:"/:foo" + rails:"/:bar" + sinatra:"/:baz")' }
58
+ end
59
+ end
60
+
61
+ subject(:pattern) { Mustermann::Concat.new("/:foo", "/:bar") }
62
+
63
+ describe :=== do
64
+ example { (pattern === "/foo/bar") .should be true }
65
+ example { (pattern === "/foo/bar/") .should be false }
66
+ example { (pattern === "/foo") .should be false }
67
+ end
68
+
69
+ describe :match do
70
+ it { should match("/foo/bar").capturing(foo: "foo", bar: "bar") }
71
+ it { should_not match("/foo/bar/") }
72
+ it { should_not match("/foo/") }
73
+ end
74
+
75
+ describe :params do
76
+ example { pattern.params("/foo/bar") .should be == { "foo" => "foo", "bar" => "bar" }}
77
+ example { pattern.params("/foo/bar/") .should be_nil }
78
+ example { pattern.params("/foo") .should be_nil }
79
+ end
80
+
81
+ describe :peek do
82
+ example { pattern.peek("/foo/bar/baz") .should be == "/foo/bar" }
83
+ example { pattern.peek("/foo") .should be_nil }
84
+ end
85
+
86
+ describe :peek_params do
87
+ example { pattern.peek_params("/foo/bar/baz") .should be == [{ "foo" => "foo", "bar" => "bar" }, 8]}
88
+ example { pattern.peek_params("/foo") .should be_nil }
89
+ end
90
+
91
+ describe :peek_match do
92
+ example { pattern.peek_match("/foo/bar/baz").to_s .should be == "/foo/bar" }
93
+ example { pattern.peek_match("/foo") .should be_nil }
94
+ end
95
+
96
+ describe :peek_size do
97
+ example { pattern.peek_size("/foo/bar/baz") .should be == 8 }
98
+ example { pattern.peek_size("/foo") .should be_nil }
99
+ end
100
+
101
+ describe :expand do
102
+ it { should expand(foo: :bar, bar: :foo) .to('/bar/foo') }
103
+ it { should expand(:append, foo: :bar, bar: :foo, baz: 42) .to('/bar/foo?baz=42') }
104
+ it { should_not expand(foo: :bar) }
105
+ end
106
+
107
+ describe :to_templates do
108
+ subject(:pattern) { Mustermann::Concat.new("/:foo|:bar", "(/:baz)?") }
109
+ it { should generate_template("/{foo}/{baz}") }
110
+ it { should generate_template("{bar}/{baz}") }
111
+ it { should generate_template("/{foo}") }
112
+ it { should generate_template("{bar}") }
113
+ end
114
+ end
@@ -0,0 +1,25 @@
1
+ require 'support'
2
+ require 'mustermann/equality_map'
3
+
4
+ RSpec.describe Mustermann::EqualityMap do
5
+ before { GC.disable }
6
+ after { GC.enable }
7
+
8
+ describe :fetch do
9
+ subject { Mustermann::EqualityMap.new }
10
+ specify 'with existing entry' do
11
+ next if subject.is_a? Hash
12
+ subject.fetch("foo") { "foo" }
13
+ result = subject.fetch("foo") { "bar" }
14
+ expect(result).to be == "foo"
15
+ end
16
+
17
+ specify 'with GC-removed entry' do
18
+ next if subject.is_a? Hash
19
+ subject.fetch("foo") { "foo" }
20
+ expect(subject.map).to receive(:[]).and_return(nil)
21
+ result = subject.fetch("foo") { "bar" }
22
+ expect(result).to be == "bar"
23
+ end
24
+ end
25
+ end
@@ -37,11 +37,14 @@ describe Mustermann do
37
37
  end
38
38
 
39
39
  context "multiple arguments" do
40
- example { Mustermann.new('', '') .should be_a(Mustermann::Composite) }
41
- example { Mustermann.new('', '').patterns.first .should be_a(Mustermann::Sinatra) }
42
- example { Mustermann.new('', '').operator .should be == :| }
43
- example { Mustermann.new('', '', operator: :&).operator .should be == :& }
44
- example { Mustermann.new('', '', greedy: true) .should be_a(Mustermann::Composite) }
40
+ example { Mustermann.new(':a', ':b/:a') .should be_a(Mustermann::Composite) }
41
+ example { Mustermann.new(':a', ':b/:a').patterns.first .should be_a(Mustermann::Sinatra) }
42
+ example { Mustermann.new(':a', ':b/:a').operator .should be == :| }
43
+ example { Mustermann.new(':a', ':b/:a', operator: :&).operator .should be == :& }
44
+ example { Mustermann.new(':a', ':b/:a', greedy: true) .should be_a(Mustermann::Composite) }
45
+
46
+ example { Mustermann.new('/foo', ':bar') .should be_a(Mustermann::Sinatra) }
47
+ example { Mustermann.new('/foo', ':bar').to_s .should be == "/foo|{bar}" }
45
48
  end
46
49
 
47
50
  context "invalid arguments" do
@@ -37,6 +37,46 @@ describe Mustermann::Regular do
37
37
  it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' }
38
38
  end
39
39
 
40
+ describe :check_achnors do
41
+ context 'raises on anchors' do
42
+ example { expect { Mustermann::Regular.new('^foo') }.to raise_error(Mustermann::CompileError) }
43
+ example { expect { Mustermann::Regular.new('foo$') }.to raise_error(Mustermann::CompileError) }
44
+ example { expect { Mustermann::Regular.new('\Afoo') }.to raise_error(Mustermann::CompileError) }
45
+ example { expect { Mustermann::Regular.new('foo\Z') }.to raise_error(Mustermann::CompileError) }
46
+ example { expect { Mustermann::Regular.new('foo\z') }.to raise_error(Mustermann::CompileError) }
47
+ example { expect { Mustermann::Regular.new(/^foo/) }.to raise_error(Mustermann::CompileError) }
48
+ example { expect { Mustermann::Regular.new(/foo$/) }.to raise_error(Mustermann::CompileError) }
49
+ example { expect { Mustermann::Regular.new(/\Afoo/) }.to raise_error(Mustermann::CompileError) }
50
+ example { expect { Mustermann::Regular.new(/foo\Z/) }.to raise_error(Mustermann::CompileError) }
51
+ example { expect { Mustermann::Regular.new(/foo\z/) }.to raise_error(Mustermann::CompileError) }
52
+ example { expect { Mustermann::Regular.new('[^f]') }.not_to raise_error }
53
+ example { expect { Mustermann::Regular.new('\\\A') }.not_to raise_error }
54
+ example { expect { Mustermann::Regular.new('[[:digit:]]') }.not_to raise_error }
55
+ example { expect { Mustermann::Regular.new(/[^f]/) }.not_to raise_error }
56
+ example { expect { Mustermann::Regular.new(/\\A/) }.not_to raise_error }
57
+ example { expect { Mustermann::Regular.new(/[[:digit:]]/) }.not_to raise_error }
58
+ end
59
+
60
+ context 'with check_anchors disabled' do
61
+ example { expect { Mustermann::Regular.new('^foo', check_anchors: false) }.not_to raise_error }
62
+ example { expect { Mustermann::Regular.new('foo$', check_anchors: false) }.not_to raise_error }
63
+ example { expect { Mustermann::Regular.new('\Afoo', check_anchors: false) }.not_to raise_error }
64
+ example { expect { Mustermann::Regular.new('foo\Z', check_anchors: false) }.not_to raise_error }
65
+ example { expect { Mustermann::Regular.new('foo\z', check_anchors: false) }.not_to raise_error }
66
+ example { expect { Mustermann::Regular.new(/^foo/, check_anchors: false) }.not_to raise_error }
67
+ example { expect { Mustermann::Regular.new(/foo$/, check_anchors: false) }.not_to raise_error }
68
+ example { expect { Mustermann::Regular.new(/\Afoo/, check_anchors: false) }.not_to raise_error }
69
+ example { expect { Mustermann::Regular.new(/foo\Z/, check_anchors: false) }.not_to raise_error }
70
+ example { expect { Mustermann::Regular.new(/foo\z/, check_anchors: false) }.not_to raise_error }
71
+ example { expect { Mustermann::Regular.new('[^f]', check_anchors: false) }.not_to raise_error }
72
+ example { expect { Mustermann::Regular.new('\\\A', check_anchors: false) }.not_to raise_error }
73
+ example { expect { Mustermann::Regular.new('[[:digit:]]', check_anchors: false) }.not_to raise_error }
74
+ example { expect { Mustermann::Regular.new(/[^f]/, check_anchors: false) }.not_to raise_error }
75
+ example { expect { Mustermann::Regular.new(/\\A/, check_anchors: false) }.not_to raise_error }
76
+ example { expect { Mustermann::Regular.new(/[[:digit:]]/, check_anchors: false) }.not_to raise_error }
77
+ end
78
+ end
79
+
40
80
  context "peeking" do
41
81
  subject(:pattern) { Mustermann::Regular.new("(?<name>[^/]+)") }
42
82
 
@@ -463,6 +463,40 @@ describe Mustermann::Sinatra do
463
463
  it { should_not expand(a: 'foo', b: 'bar', c: 'baz') }
464
464
  end
465
465
 
466
+ pattern "/:a/:b|:c" do
467
+ it { should match("foo") .capturing c: 'foo' }
468
+ it { should match("/foo/bar") .capturing a: 'foo', b: 'bar' }
469
+
470
+ it { should generate_template('/{a}/{b}') }
471
+ it { should generate_template('{c}') }
472
+
473
+ it { should expand(a: 'foo', b: 'bar') .to('/foo/bar') }
474
+ it { should expand(c: 'foo') .to('foo') }
475
+ it { should_not expand(a: 'foo', b: 'bar', c: 'baz') }
476
+ end
477
+
478
+ pattern "/({foo}|{bar})", capture: { foo: /\d+/, bar: /\w+/ } do
479
+ it { should match("/a") .capturing foo: nil, bar: 'a' }
480
+ it { should match("/1234") .capturing foo: "1234", bar: nil }
481
+
482
+ it { should_not match("/a/b") }
483
+ end
484
+
485
+ pattern "/{foo|bar}", capture: { foo: /\d+/, bar: /\w+/ } do
486
+ it { should match("/a") .capturing foo: nil, bar: 'a' }
487
+ it { should match("/1234") .capturing foo: "1234", bar: nil }
488
+
489
+ it { should_not match("/a/b") }
490
+ end
491
+
492
+ pattern "/{foo|bar|baz}", capture: { foo: /\d+/, bar: /[ab]+/, baz: /[cde]+/ } do
493
+ it { should match("/ab") .capturing foo: nil, bar: 'ab', baz: nil }
494
+ it { should match("/1234") .capturing foo: "1234", bar: nil, baz: nil }
495
+ it { should match("/ccddee") .capturing foo: nil, bar: nil, baz: "ccddee" }
496
+
497
+ it { should_not match("/a/b") }
498
+ end
499
+
466
500
  pattern '/:foo', capture: /\d+/ do
467
501
  it { should match('/1') .capturing foo: '1' }
468
502
  it { should match('/123') .capturing foo: '123' }
@@ -648,11 +682,6 @@ describe Mustermann::Sinatra do
648
682
  to raise_error(Mustermann::ParseError, 'unexpected ? while parsing "foo??bar"')
649
683
  end
650
684
 
651
- example '| outside of group' do
652
- expect { Mustermann::Sinatra.new('foo|bar') }.
653
- to raise_error(Mustermann::ParseError, 'unexpected | while parsing "foo|bar"')
654
- end
655
-
656
685
  example 'dangling escape' do
657
686
  expect { Mustermann::Sinatra.new('foo\\') }.
658
687
  to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "foo\\\\"')
@@ -745,4 +774,55 @@ describe Mustermann::Sinatra do
745
774
  example { pattern.peek_params("/foo bar") .should be_nil }
746
775
  end
747
776
  end
777
+
778
+ describe :| do
779
+ let(:first) { Mustermann.new("a") }
780
+ let(:second) { Mustermann.new("b") }
781
+ subject(:composite) { first | second }
782
+
783
+ context "with no capture names" do
784
+ its(:class) { should be == Mustermann::Sinatra }
785
+ its(:to_s) { should be == "a|b" }
786
+ end
787
+
788
+ context "only first has captures" do
789
+ let(:first) { Mustermann.new(":a") }
790
+ its(:class) { should be == Mustermann::Sinatra }
791
+ its(:to_s) { should be == "{a}|b" }
792
+ end
793
+
794
+ context "only second has captures" do
795
+ let(:second) { Mustermann.new(":b") }
796
+ its(:class) { should be == Mustermann::Sinatra }
797
+ its(:to_s) { should be == "a|{b}" }
798
+ end
799
+
800
+ context "both have captures" do
801
+ let(:first) { Mustermann.new(":a") }
802
+ let(:second) { Mustermann.new(":b") }
803
+ its(:class) { should be == Mustermann::Composite }
804
+ end
805
+
806
+ context "options mismatch" do
807
+ let(:second) { Mustermann.new(":b", greedy: false) }
808
+ its(:class) { should be == Mustermann::Composite }
809
+ end
810
+
811
+ context "argument is a string" do
812
+ let(:second) { ":b" }
813
+ its(:class) { should be == Mustermann::Sinatra }
814
+ its(:to_s) { should be == "a|\\:b" }
815
+ end
816
+
817
+ context "argument is an identity pattern" do
818
+ let(:second) { Mustermann::Identity.new(":b") }
819
+ its(:class) { should be == Mustermann::Sinatra }
820
+ its(:to_s) { should be == "a|\\:b" }
821
+ end
822
+
823
+ context "argument is an identity pattern, but options mismatch" do
824
+ let(:second) { Mustermann::Identity.new(":b", uri_decode: false) }
825
+ its(:class) { should be == Mustermann::Composite }
826
+ end
827
+ end
748
828
  end