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