mustermann 0.3.1 → 0.4.0

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