mustermann 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,49 @@
1
+ require 'set'
2
+ require 'thread'
3
+ require 'mustermann'
4
+
5
+ module Mustermann
6
+ # A simple, persistent cache for creating repositories.
7
+ #
8
+ # @example
9
+ # require 'mustermann/pattern_cache'
10
+ # cache = Mustermann::PatternCache.new
11
+ #
12
+ # # use this instead of Mustermann.new
13
+ # pattern = cache.create_pattern("/:name", type: :rails)
14
+ #
15
+ # @note
16
+ # {Mustermann::Pattern.new} (which is used by {Mustermann.new}) will reuse instances that have
17
+ # not yet been garbage collected. You only need an extra cache if you do not keep a reference to
18
+ # the patterns around.
19
+ #
20
+ # @api private
21
+ class PatternCache
22
+ # @param [Hash] pattern_options default options used for {#create_pattern}
23
+ def initialize(**pattern_options)
24
+ @cached = Set.new
25
+ @mutex = Mutex.new
26
+ @pattern_options = pattern_options
27
+ end
28
+
29
+ # @param (see Mustermann.new)
30
+ # @return (see Mustermann.new)
31
+ # @raise (see Mustermann.new)
32
+ # @see Mustermann.new
33
+ def create_pattern(string, **pattern_options)
34
+ pattern = Mustermann.new(string, **pattern_options, **@pattern_options)
35
+ @mutex.synchronize { @cached.add(pattern) } unless @cached.include? pattern
36
+ pattern
37
+ end
38
+
39
+ # Removes all pattern instances from the cache.
40
+ def clear
41
+ @mutex.synchronize { @cached.clear }
42
+ end
43
+
44
+ # @return [Integer] number of currently cached patterns
45
+ def size
46
+ @mutex.synchronize { @cached.size }
47
+ end
48
+ end
49
+ end
@@ -0,0 +1 @@
1
+ require 'mustermann/regular'
@@ -15,7 +15,24 @@ module Mustermann
15
15
  # @see (see Mustermann::Pattern#initialize)
16
16
  def initialize(string, **options)
17
17
  super
18
- @regexp = compile(**options)
18
+ regexp = compile(**options)
19
+ @peek_regexp = /\A(#{regexp})/
20
+ @regexp = /\A#{regexp}\Z/
21
+ end
22
+
23
+ # @param (see Mustermann::Pattern#peek_size)
24
+ # @return (see Mustermann::Pattern#peek_size)
25
+ # @see (see Mustermann::Pattern#peek_size)
26
+ def peek_size(string)
27
+ return unless match = peek_match(string)
28
+ match.to_s.size
29
+ end
30
+
31
+ # @param (see Mustermann::Pattern#peek_match)
32
+ # @return (see Mustermann::Pattern#peek_match)
33
+ # @see (see Mustermann::Pattern#peek_match)
34
+ def peek_match(string)
35
+ @peek_regexp.match(string)
19
36
  end
20
37
 
21
38
  extend Forwardable
@@ -1,3 +1,4 @@
1
+ require 'mustermann'
1
2
  require 'mustermann/regexp_based'
2
3
 
3
4
  module Mustermann
@@ -9,6 +10,8 @@ module Mustermann
9
10
  # @see Mustermann::Pattern
10
11
  # @see file:README.md#simple Syntax description in the README
11
12
  class Regular < RegexpBased
13
+ register :regexp, :regular
14
+
12
15
  # @param (see Mustermann::Pattern#initialize)
13
16
  # @return (see Mustermann::Pattern#initialize)
14
17
  # @see (see Mustermann::Pattern#initialize)
@@ -18,7 +21,7 @@ module Mustermann
18
21
  end
19
22
 
20
23
  def compile(**options)
21
- /\A#{@string}\Z/
24
+ /#{@string}/
22
25
  end
23
26
 
24
27
  private :compile
@@ -26,5 +26,10 @@ module Mustermann
26
26
  def [](*args)
27
27
  captures[*args]
28
28
  end
29
+
30
+ # @return [String] string representation
31
+ def inspect
32
+ "#<%p %p>" % [self.class, @string]
33
+ end
29
34
  end
30
35
  end
@@ -1,3 +1,4 @@
1
+ require 'mustermann'
1
2
  require 'mustermann/ast/pattern'
2
3
 
3
4
  module Mustermann
@@ -9,11 +10,27 @@ module Mustermann
9
10
  # @see Mustermann::Pattern
10
11
  # @see file:README.md#sinatra Syntax description in the README
11
12
  class Sinatra < AST::Pattern
12
- on(nil, ??, ?)) { |c| unexpected(c) }
13
- on(?*) { |c| scan(/\w+/) ? node(:named_splat, buffer.matched) : node(:splat) }
14
- on(?() { |c| node(:group) { read unless scan(?)) } }
15
- on(?:) { |c| node(:capture) { scan(/\w+/) } }
16
- on(?\\) { |c| node(:char, expect(/./)) }
13
+ register :sinatra
14
+
15
+ on(nil, ??, ?), ?|) { |c| unexpected(c) }
16
+
17
+ on(?*) { |c| scan(/\w+/) ? node(:named_splat, buffer.matched) : node(:splat) }
18
+ on(?:) { |c| node(:capture) { scan(/\w+/) } }
19
+ on(?\\) { |c| node(:char, expect(/./)) }
20
+
21
+ on ?( do |char|
22
+ groups = []
23
+ groups << node(:group) { read unless check(?)) or scan(?|) } until scan(?))
24
+ groups.size == 1 ? groups.first : node(:union, groups)
25
+ end
26
+
27
+ on ?{ do |char|
28
+ type = scan(?+) ? :named_splat : :capture
29
+ name = expect(/[\w\.]+/)
30
+ type = :splat if type == :named_splat and name == 'splat'
31
+ expect(?})
32
+ node(type, name)
33
+ end
17
34
 
18
35
  suffix ?? do |char, element|
19
36
  node(:optional, element)
@@ -16,8 +16,11 @@ module Mustermann
16
16
  #
17
17
  # Foo.new.to_pattern # => #<Mustermann::Sinatra:":foo/:bar">
18
18
  #
19
- # By default included into {String}, {Symbol}, {Regexp} and {Mustermann::Pattern}.
19
+ # By default included into String, Symbol, Regexp, Array and {Mustermann::Pattern}.
20
20
  module ToPattern
21
+ PRIMITIVES = [String, Symbol, Array, Regexp, Mustermann::Pattern]
22
+ private_constant :PRIMITIVES
23
+
21
24
  # Converts the object into a {Mustermann::Pattern}.
22
25
  #
23
26
  # @example converting a string
@@ -30,16 +33,18 @@ module Mustermann
30
33
  # /.*/.to_pattern # => #<Mustermann::Regular:".*">
31
34
  #
32
35
  # @example converting a pattern
33
- # Mustermann.new("foo").to_pattern # => #<Mustermann::Sinatra:"foo">
36
+ # Mustermann.new("foo").to_pattern # => #<Mustermann::Sinatra:"foo">
34
37
  #
35
38
  # @param [Hash] options The options hash.
36
39
  # @return [Mustermann::Pattern] pattern corresponding to object.
37
40
  def to_pattern(**options)
38
- Mustermann.new(self, **options)
41
+ input = self if PRIMITIVES.any? { |p| self.is_a? p }
42
+ input ||= __getobj__ if respond_to?(:__getobj__)
43
+ Mustermann.new(input || to_s, **options)
39
44
  end
40
45
 
41
- append_features String
42
- append_features Regexp
43
- append_features Mustermann::Pattern
46
+ PRIMITIVES.each do |klass|
47
+ append_features(klass)
48
+ end
44
49
  end
45
50
  end
@@ -1,3 +1,3 @@
1
1
  module Mustermann
2
- VERSION ||= '0.3.1'
2
+ VERSION ||= '0.4.0'
3
3
  end
@@ -10,22 +10,9 @@ 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
14
  s.files = `git ls-files`.split("\n")
14
15
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
16
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
- s.extra_rdoc_files = %w[README.md internals.md]
17
- s.require_path = 'lib'
18
- s.required_ruby_version = '>= 2.0.0'
19
-
20
17
  s.add_dependency 'tool', '~> 0.2'
21
- s.add_development_dependency 'rspec' #, '~> 2.14'
22
- s.add_development_dependency 'rspec-its'
23
- s.add_development_dependency 'addressable'
24
- s.add_development_dependency 'sinatra', '~> 1.4'
25
- s.add_development_dependency 'rack-test'
26
- s.add_development_dependency 'rake'
27
- s.add_development_dependency 'yard'
28
- s.add_development_dependency 'redcarpet'
29
- s.add_development_dependency 'simplecov'
30
- s.add_development_dependency 'coveralls'
31
18
  end
@@ -0,0 +1,14 @@
1
+ require 'support'
2
+ require 'mustermann/ast/node'
3
+
4
+ describe Mustermann::AST do
5
+ describe :type do
6
+ example { Mustermann::AST::Node[:char].type .should be == :char }
7
+ example { Mustermann::AST::Node[:char].new.type .should be == :char }
8
+ end
9
+
10
+ describe :min_size do
11
+ example { Mustermann::AST::Node[:char].new.min_size.should be == 1 }
12
+ example { Mustermann::AST::Node[:node].new.min_size.should be == 0 }
13
+ end
14
+ end
@@ -0,0 +1,147 @@
1
+ require 'support'
2
+ require 'mustermann'
3
+
4
+ describe Mustermann::Composite do
5
+ describe :new do
6
+ example 'with no argument' do
7
+ expect { Mustermann::Composite.new }.
8
+ to raise_error(ArgumentError, 'cannot create empty composite pattern')
9
+ end
10
+
11
+ example 'with one argument' do
12
+ pattern = Mustermann.new('/foo')
13
+ Mustermann::Composite.new(pattern).should be == pattern
14
+ end
15
+ end
16
+
17
+ context :| do
18
+ subject(:pattern) { Mustermann.new('/foo/:name', '/:first/:second') }
19
+
20
+ describe :== do
21
+ example { subject.should be == subject }
22
+ example { subject.should be == Mustermann.new('/foo/:name', '/:first/:second') }
23
+ example { subject.should_not be == Mustermann.new('/foo/:name') }
24
+ example { subject.should_not be == Mustermann.new('/foo/:name', '/:first/:second', operator: :&) }
25
+ end
26
+
27
+ describe :=== do
28
+ example { subject.should be === "/foo/bar" }
29
+ example { subject.should be === "/fox/bar" }
30
+ example { subject.should_not be === "/foo" }
31
+ end
32
+
33
+ describe :params do
34
+ example { subject.params("/foo/bar") .should be == { "name" => "bar" } }
35
+ example { subject.params("/fox/bar") .should be == { "first" => "fox", "second" => "bar" } }
36
+ example { subject.params("/foo") .should be_nil }
37
+ end
38
+
39
+ describe :=== do
40
+ example { subject.should match("/foo/bar") }
41
+ example { subject.should match("/fox/bar") }
42
+ example { subject.should_not match("/foo") }
43
+ end
44
+
45
+ describe :expand do
46
+ example { subject.should respond_to(:expand) }
47
+ example { subject.expand(name: 'bar') .should be == '/foo/bar' }
48
+ example { subject.expand(first: 'fox', second: 'bar') .should be == '/fox/bar' }
49
+
50
+ context "without expandable patterns" do
51
+ subject(:pattern) { Mustermann.new('/foo/:name', '/:first/:second', type: :simple) }
52
+ example { subject.should_not respond_to(:expand) }
53
+ example { expect { subject.expand(name: 'bar') }.to raise_error(NotImplementedError) }
54
+ end
55
+ end
56
+
57
+ describe :to_templates do
58
+ example { should respond_to(:to_templates) }
59
+ example { should generate_templates('/foo/{name}', '/{first}/{second}') }
60
+
61
+ context "without patterns implementing to_templates" do
62
+ subject(:pattern) { Mustermann.new('/foo/:name', '/:first/:second', type: :simple) }
63
+ example { should_not respond_to(:to_templates) }
64
+ example { expect { subject.to_templates }.to raise_error(NotImplementedError) }
65
+ end
66
+ end
67
+ end
68
+
69
+ context :& do
70
+ subject(:pattern) { Mustermann.new('/foo/:name', '/:first/:second', operator: :&) }
71
+
72
+ describe :== do
73
+ example { subject.should be == subject }
74
+ example { subject.should be == Mustermann.new('/foo/:name', '/:first/:second', operator: :&) }
75
+ example { subject.should_not be == Mustermann.new('/foo/:name') }
76
+ example { subject.should_not be == Mustermann.new('/foo/:name', '/:first/:second') }
77
+ end
78
+
79
+ describe :=== do
80
+ example { subject.should be === "/foo/bar" }
81
+ example { subject.should_not be === "/fox/bar" }
82
+ example { subject.should_not be === "/foo" }
83
+ end
84
+
85
+ describe :params do
86
+ example { subject.params("/foo/bar") .should be == { "name" => "bar" } }
87
+ example { subject.params("/fox/bar") .should be_nil }
88
+ example { subject.params("/foo") .should be_nil }
89
+ end
90
+
91
+ describe :match do
92
+ example { subject.should match("/foo/bar") }
93
+ example { subject.should_not match("/fox/bar") }
94
+ example { subject.should_not match("/foo") }
95
+ end
96
+
97
+ describe :expand do
98
+ example { subject.should_not respond_to(:expand) }
99
+ example { expect { subject.expand(name: 'bar') }.to raise_error(NotImplementedError) }
100
+ end
101
+ end
102
+
103
+ context :^ do
104
+ subject(:pattern) { Mustermann.new('/foo/:name', '/:first/:second', operator: :^) }
105
+
106
+ describe :== do
107
+ example { subject.should be == subject }
108
+ example { subject.should_not be == Mustermann.new('/foo/:name', '/:first/:second') }
109
+ example { subject.should_not be == Mustermann.new('/foo/:name') }
110
+ example { subject.should_not be == Mustermann.new('/foo/:name', '/:first/:second', operator: :&) }
111
+ end
112
+
113
+ describe :=== do
114
+ example { subject.should_not be === "/foo/bar" }
115
+ example { subject.should be === "/fox/bar" }
116
+ example { subject.should_not be === "/foo" }
117
+ end
118
+
119
+ describe :params do
120
+ example { subject.params("/foo/bar") .should be_nil }
121
+ example { subject.params("/fox/bar") .should be == { "first" => "fox", "second" => "bar" } }
122
+ example { subject.params("/foo") .should be_nil }
123
+ end
124
+
125
+ describe :match do
126
+ example { subject.should_not match("/foo/bar") }
127
+ example { subject.should match("/fox/bar") }
128
+ example { subject.should_not match("/foo") }
129
+ end
130
+
131
+ describe :expand do
132
+ example { subject.should_not respond_to(:expand) }
133
+ example { expect { subject.expand(name: 'bar') }.to raise_error(NotImplementedError) }
134
+ end
135
+ end
136
+
137
+ describe :inspect do
138
+ let(:sinatra) { Mustermann.new('x') }
139
+ let(:rails) { Mustermann.new('x', type: :rails) }
140
+ let(:identity) { Mustermann.new('x', type: :identity) }
141
+
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"))') }
146
+ end
147
+ end
@@ -30,6 +30,21 @@ describe Mustermann::Expander do
30
30
  expander.expand(foo: 'pony', ext: nil).should be == '/pony'
31
31
  end
32
32
 
33
+ it 'supports splat' do
34
+ expander = Mustermann::Expander.new << Mustermann.new("/foo/*/baz")
35
+ expander.expand(splat: 'bar').should be == '/foo/bar/baz'
36
+ end
37
+
38
+ it 'supports multiple splats' do
39
+ expander = Mustermann::Expander.new << Mustermann.new("/foo/*/bar/*")
40
+ expander.expand(splat: [123, 456]).should be == '/foo/123/bar/456'
41
+ end
42
+
43
+ it 'supports identity patterns' do
44
+ expander = Mustermann::Expander.new('/:foo', type: :identity)
45
+ expander.expand.should be == '/:foo'
46
+ end
47
+
33
48
  describe :additional_values do
34
49
  context "illegal value" do
35
50
  example { expect { Mustermann::Expander.new(additional_values: :foo) }.to raise_error(ArgumentError) }
@@ -7,6 +7,21 @@ describe Mustermann::Identity do
7
7
  pattern '' do
8
8
  it { should match('') }
9
9
  it { should_not match('/') }
10
+
11
+ it { should respond_to(:expand) }
12
+ it { should respond_to(:to_templates) }
13
+
14
+
15
+ it { should generate_template('') }
16
+ it { should expand.to('') }
17
+ it { should expand(:ignore, a: 10).to('') }
18
+ it { should expand(:append, a: 10).to('?a=10') }
19
+ it { should_not expand(:raise, a: 10) }
20
+ it { should_not expand(a: 10) }
21
+
22
+ example do
23
+ pattern.match('').inspect.should be == '#<Mustermann::SimpleMatch "">'
24
+ end
10
25
  end
11
26
 
12
27
  pattern '/' do
@@ -15,6 +30,9 @@ describe Mustermann::Identity do
15
30
 
16
31
  example { pattern.params('/').should be == {} }
17
32
  example { pattern.params('').should be_nil }
33
+
34
+ it { should generate_template('/') }
35
+ it { should expand.to('/') }
18
36
  end
19
37
 
20
38
  pattern '/foo' do
@@ -37,6 +55,9 @@ describe Mustermann::Identity do
37
55
  it { should_not match('/foo/bar') }
38
56
  it { should_not match('/') }
39
57
  it { should_not match('/foo/') }
58
+
59
+ it { should generate_template('/:foo') }
60
+ it { should expand.to('/:foo') }
40
61
  end
41
62
 
42
63
  pattern '/föö' do
@@ -57,6 +78,7 @@ describe Mustermann::Identity do
57
78
  it { should match('/path%20with%20spaces') }
58
79
  it { should_not match('/path%2Bwith%2Bspaces') }
59
80
  it { should_not match('/path+with+spaces') }
81
+ it { should generate_template('/path%20with%20spaces') }
60
82
  end
61
83
 
62
84
  pattern '/foo&bar' do
@@ -79,4 +101,26 @@ describe Mustermann::Identity do
79
101
  it { should_not match('/path%2Bwith%2Bspaces') }
80
102
  it { should_not match('/path+with+spaces') }
81
103
  end
104
+
105
+ context "peeking" do
106
+ subject(:pattern) { Mustermann::Identity.new("foo bar") }
107
+
108
+ describe :peek_size do
109
+ example { pattern.peek_size("foo bar blah") .should be == "foo bar".size }
110
+ example { pattern.peek_size("foo%20bar blah") .should be == "foo%20bar".size }
111
+ example { pattern.peek_size("foobar") .should be_nil }
112
+ end
113
+
114
+ describe :peek_match do
115
+ example { pattern.peek_match("foo bar blah").to_s .should be == "foo bar" }
116
+ example { pattern.peek_match("foo%20bar blah").to_s .should be == "foo%20bar" }
117
+ example { pattern.peek_match("foobar") .should be_nil }
118
+ end
119
+
120
+ describe :peek_params do
121
+ example { pattern.peek_params("foo bar blah") .should be == [{}, "foo bar".size] }
122
+ example { pattern.peek_params("foo%20bar blah") .should be == [{}, "foo%20bar".size] }
123
+ example { pattern.peek_params("foobar") .should be_nil }
124
+ end
125
+ end
82
126
  end