mustermann 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -1
  3. data/.travis.yml +4 -3
  4. data/.yardopts +1 -0
  5. data/README.md +53 -10
  6. data/Rakefile +4 -1
  7. data/bench/capturing.rb +42 -9
  8. data/bench/template_vs_addressable.rb +3 -0
  9. data/internals.md +64 -0
  10. data/lib/mustermann.rb +14 -5
  11. data/lib/mustermann/ast/compiler.rb +150 -0
  12. data/lib/mustermann/ast/expander.rb +112 -0
  13. data/lib/mustermann/ast/node.rb +155 -0
  14. data/lib/mustermann/ast/parser.rb +136 -0
  15. data/lib/mustermann/ast/pattern.rb +89 -0
  16. data/lib/mustermann/ast/transformer.rb +121 -0
  17. data/lib/mustermann/ast/translator.rb +111 -0
  18. data/lib/mustermann/ast/tree_renderer.rb +29 -0
  19. data/lib/mustermann/ast/validation.rb +40 -0
  20. data/lib/mustermann/error.rb +4 -12
  21. data/lib/mustermann/extension.rb +3 -6
  22. data/lib/mustermann/identity.rb +4 -4
  23. data/lib/mustermann/pattern.rb +34 -5
  24. data/lib/mustermann/rails.rb +7 -16
  25. data/lib/mustermann/regexp_based.rb +4 -4
  26. data/lib/mustermann/shell.rb +4 -4
  27. data/lib/mustermann/simple.rb +1 -1
  28. data/lib/mustermann/simple_match.rb +2 -2
  29. data/lib/mustermann/sinatra.rb +10 -20
  30. data/lib/mustermann/template.rb +11 -104
  31. data/lib/mustermann/version.rb +1 -1
  32. data/mustermann.gemspec +1 -1
  33. data/spec/extension_spec.rb +143 -0
  34. data/spec/mustermann_spec.rb +41 -0
  35. data/spec/pattern_spec.rb +16 -6
  36. data/spec/rails_spec.rb +77 -9
  37. data/spec/sinatra_spec.rb +6 -0
  38. data/spec/support.rb +5 -78
  39. data/spec/support/coverage.rb +18 -0
  40. data/spec/support/env.rb +6 -0
  41. data/spec/support/expand_matcher.rb +27 -0
  42. data/spec/support/match_matcher.rb +39 -0
  43. data/spec/support/pattern.rb +28 -0
  44. metadata +43 -43
  45. data/.test_queue_stats +0 -0
  46. data/lib/mustermann/ast.rb +0 -403
  47. data/spec/ast_spec.rb +0 -8
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9a623efd27ea171ba18818550616788aeca18ded
4
+ data.tar.gz: f8e7956944902e37495c44085662cb39cedea8a9
5
+ SHA512:
6
+ metadata.gz: f8e52f5e109283a3f6d9d2823b1410e3e41e3d8c3d3787e767c70fa8e7a018ff53738f994d856947c2473f8ef94c65eb56de79f51132aa729ca26a4d582ada2f
7
+ data.tar.gz: f3ff266371da60a7da88f492ad2ee3a46389a1a4806c2e57886f07b7b702408388ffaf3ff18175d59c1b67fdfafb2eb5a07d496ce8219cdccc3ae5fd90f06203
data/.gitignore CHANGED
@@ -3,10 +3,11 @@
3
3
  .bundle
4
4
  .config
5
5
  .yardoc
6
+ .test_queue_stats
7
+ .coverage
6
8
  Gemfile.lock
7
9
  InstalledFiles
8
10
  _yardoc
9
- coverage
10
11
  doc/
11
12
  lib/bundler/man
12
13
  pkg
@@ -1,6 +1,7 @@
1
1
  rvm:
2
2
  - 2.0.0
3
3
  - ruby-head
4
- matrix:
5
- allow_failures:
6
- - rvm: ruby-head
4
+ # - jruby-head
5
+ #matrix:
6
+ # allow_failures:
7
+ # - rvm: jruby-head
@@ -0,0 +1 @@
1
+ - README.md internals.md
data/README.md CHANGED
@@ -1,10 +1,8 @@
1
- # Mustermann
1
+ # The Amazing Mustermann
2
2
 
3
- [![Build Status](https://travis-ci.org/rkh/mustermann.png?branch=master)](https://travis-ci.org/rkh/mustermann)
4
- [![Coverage Status](https://coveralls.io/repos/rkh/mustermann/badge.png?branch=master)](https://coveralls.io/r/rkh/mustermann)
5
- [![Code Climate](https://codeclimate.com/github/rkh/mustermann.png)](https://codeclimate.com/github/rkh/mustermann)
6
- [![Dependency Status](https://gemnasium.com/rkh/mustermann.png)](https://gemnasium.com/rkh/mustermann)
7
- [![Gem Version](https://badge.fury.io/rb/mustermann.png)](http://badge.fury.io/rb/mustermann)
3
+ [![Build Status](https://travis-ci.org/rkh/mustermann.png?branch=master)](https://travis-ci.org/rkh/mustermann) [![Coverage Status](https://coveralls.io/repos/rkh/mustermann/badge.png?branch=master)](https://coveralls.io/r/rkh/mustermann) [![Code Climate](https://codeclimate.com/github/rkh/mustermann.png)](https://codeclimate.com/github/rkh/mustermann) [![Dependency Status](https://gemnasium.com/rkh/mustermann.png)](https://gemnasium.com/rkh/mustermann) [![Gem Version](https://badge.fury.io/rb/mustermann.png)](http://badge.fury.io/rb/mustermann)
4
+
5
+ *Make sure you view the correct docs: [latest release](http://rubydoc.info/gems/mustermann/frames), [master](http://rubydoc.info/github/rkh/mustermann/master/frames).*
8
6
 
9
7
  Welcome to [Mustermann](http://en.wikipedia.org/wiki/List_of_placeholder_names_by_language#German). Mustermann is your personal string matching expert. As an expert in the field of strings and patterns, Mustermann also has no runtime dependencies and is fully covered with specs and documentation.
10
8
 
@@ -29,14 +27,22 @@ pattern = Mustermann.new('/:prefix/*.*')
29
27
  pattern.params('/a/b.c') # => { "prefix" => "a", splat => ["b", "c"] }
30
28
  ```
31
29
 
32
- It's generally a good idea to reuse pattern objects, since as much computation as possible is happening during object creation, so that the actual matching is quite fast.
30
+ Similarly, it is also possible to generate a string from a pattern by expanding it with such a hash:
31
+
32
+ ``` ruby
33
+ pattern = Mustermann.new('/:file(.:ext)?')
34
+ pattern.expand(file: 'pony') # => "/pony"
35
+ pattern.expand(file: 'pony', ext: 'jpg') # => "/pony.jpg"
36
+ ```
37
+
38
+ It's generally a good idea to reuse pattern objects, since as much computation as possible is happening during object creation, so that the actual matching or expanding is quite fast.
33
39
 
34
40
  ## Types and Options
35
41
 
36
42
  You can pass in additional options to take fine grained control over the pattern:
37
43
 
38
44
  ``` ruby
39
- Mustermann.new('/:foo.:bar', capture: :alpha) # :foo and :bar will only match alphabetic character
45
+ Mustermann.new('/:foo.:bar', capture: :alpha) # :foo and :bar will only match alphabetic characters
40
46
  ```
41
47
 
42
48
  In fact, you can even completely change the pattern type:
@@ -639,6 +645,43 @@ you should set `uri_decode` to `false` in order to conform with the specificatio
639
645
 
640
646
  If you are looking for an alternative implementation that also supports expanding, check out [addressable](http://addressable.rubyforge.org/).
641
647
 
642
- ## Versioning
648
+ ## Requirements
649
+
650
+ Mustermann has no dependencies besides a Ruby 2.0 compatible Ruby implementation.
651
+
652
+ It is known to work on **MRI 2.0** and **MRI trunk**.
653
+ **JRuby** is not yet supported, but is likely to follow soon (see [issue #2](https://github.com/rkh/mustermann/issues/2) for up to date information on JRuby).
654
+ **Rubinius** is not yet able to parse the Mustermann source code.
655
+
656
+ ## Release History
657
+
658
+ Mustermann follows [Semantic Versioning 2.0](http://semver.org/). Anything documented in the README or via YARD and not declared private is part of the public API.
659
+
660
+ ### Stable Releases
661
+
662
+ There have been no stable releases yet. The code base is considered solid but I don't know of anyone using it in production yet.
663
+ As there has been no stable release yet, the API might still change, though I consider this unlikely.
664
+
665
+ ### Development Releases
666
+
667
+ * **Mustermann 0.0.1** (2013-04-27)
668
+ * More Infos:
669
+ [RubyGems.org](http://rubygems.org/gems/mustermann/versions/0.0.1),
670
+ [RubyDoc.info](rubydoc.info/gems/mustermann/0.0.1/frames),
671
+ [GitHub.com](https://github.com/rkh/mustermann/tree/v0.0.1)
672
+ * Initial Release.
673
+ * **Mustermann 0.1.0** (2013-05-12)
674
+ * Add `Pattern#expand` for generating strings from patterns.
675
+ * Add better internal API for working with the AST.
676
+ * Improved documentation.
677
+ * Avoids parsing the path twice when used as Sinatra extension.
678
+ * Better exceptions for unknown pattern types.
679
+ * Better handling of edge cases around extend.
680
+ * More specs to ensure API stability.
681
+ * Largely rework internals of Sinatra, Rails and Template patterns.
682
+
683
+ ### Upcoming Releases
643
684
 
644
- Mustermann follows [Semantic Versioning](http://semver.org/).
685
+ * **Mustermann 0.2.0** (next release with new features)
686
+ * **Mustermann 1.0.0** (before Sinatra 2.0)
687
+ * First stable release.
data/Rakefile CHANGED
@@ -1,3 +1,6 @@
1
+ ENV['JRUBY_OPTS'] = '--2.0'
2
+ ENV['RBXOPT'] = '-X20'
3
+
1
4
  task(:spec) { ruby '-w -S rspec' }
2
5
  task(:doc_stats) { ruby '-S yard stats' }
3
- task(default: [:spec, :doc_stats])
6
+ task(default: [:spec, :doc_stats])
@@ -2,23 +2,56 @@ $:.unshift File.expand_path('../lib', __dir__)
2
2
 
3
3
  require 'benchmark'
4
4
  require 'mustermann'
5
+ require 'mustermann/regexp_based'
5
6
  require 'addressable/template'
6
7
 
8
+
9
+ Mustermann.register(:regexp, Class.new(Mustermann::RegexpBased) {
10
+ def compile(string, **options)
11
+ Regexp.new(string)
12
+ end
13
+ }, load: false)
14
+
15
+ Mustermann.register(:addressable, Class.new(Mustermann::RegexpBased) {
16
+ def compile(string, **options)
17
+ Addressable::Template.new(string)
18
+ end
19
+ }, load: false)
20
+
7
21
  list = [
8
- /\A\/(?<splat>.*?)\/(?<name>[^\/\?#]+)\Z/,
9
- Mustermann.new('/*/:name', type: :sinatra),
10
- Mustermann.new('/*/:name', type: :simple),
11
- Mustermann.new('/*prefix/:name', type: :rails),
12
- Mustermann.new('{/prefix*}/{name}', type: :template),
13
- #Addressable::Template.new('{/prefix*}/{name}')
22
+ [:sinatra, '/*/:name' ],
23
+ [:rails, '/*prefix/:name' ],
24
+ [:simple, '/*/:name' ],
25
+ [:template, '{/prefix*}/{name}' ],
26
+ [:regexp, '\A\/(?<splat>.*?)\/(?<name>[^\/\?#]+)\Z' ],
27
+ [:addressable, '{/prefix*}/{name}' ]
14
28
  ]
15
29
 
30
+ def self.assert(value)
31
+ fail unless value
32
+ end
33
+
16
34
  string = '/a/b/c/d'
35
+ name = 'd'
36
+
37
+ GC.disable
38
+
39
+ puts "Compilation:"
40
+ Benchmark.bmbm do |x|
41
+ list.each do |type, pattern|
42
+ x.report(type) { 1_000.times { Mustermann.new(pattern, type: type) } }
43
+ end
44
+ end
17
45
 
46
+ puts "", "Matching with two captures (one splat, one normal):"
18
47
  Benchmark.bmbm do |x|
19
- list.each do |pattern|
20
- x.report pattern.class.to_s do
21
- 100_000.times { pattern.match(string).captures }
48
+ list.each do |type, pattern|
49
+ pattern = Mustermann.new(pattern, type: type)
50
+ x.report type do
51
+ 10_000.times do
52
+ match = pattern.match(string)
53
+ assert match[:name] == name
54
+ end
22
55
  end
23
56
  end
24
57
  end
@@ -18,6 +18,9 @@ require 'addressable/template'
18
18
  explode = klass.new("{/segments*}")
19
19
  x.report("explode, match") { 1_000.times { explode.match("/a/b/c").captures } }
20
20
  x.report("explode, miss") { 1_000.times { explode.match("/a/b/c.miss") } }
21
+
22
+ expand = klass.new("/prefix/{foo}/something/{bar}")
23
+ x.report("expand") { 100.times { expand.expand(foo: 'foo', bar: 'bar').to_s } }
21
24
  end
22
25
  puts
23
26
  end
@@ -0,0 +1,64 @@
1
+ # Internal API
2
+
3
+ This document describes how to use [Mustermann](README.md)'s internal API.
4
+
5
+ It is a secondary goal to keep the internal API as stable as possible, in a state where it would well be possible to interface with it.
6
+ However, the internal API is not covered by Semantic Versioning. As a rule of thumb, no backwards incompatible changes should be introduced to the API in minor releases (starting from 1.0.0).
7
+
8
+ Should the internal API gain widespread/production use, we might consider moving parts of it over into the public API.
9
+
10
+ Here is a quick example of what you can do with this:
11
+
12
+ ``` ruby
13
+ require 'mustermann/ast/pattern'
14
+
15
+ class MyPattern < Mustermann::AST::Pattern
16
+ on("~") { |c| node(:capture, buffer[1]) if expect(/\{(\w+)\}/) }
17
+ on("+") { |c| node(:named_splat, buffer[1]) if expect(/\{(\w+)\}/) }
18
+ on("?") { |c| node(:optional, node(:capture, buffer[1])) if expect(/\{(\w+)\}/) }
19
+ end
20
+
21
+ pattern = MyPattern.new("/+{prefix}/~{page}/?{optional}")
22
+ pattern.params("/a/") # => nil
23
+ pattern.params("/a/b/") # => { "prefix" => "a", "page" => "b", "optional" => nil }
24
+ pattern.params("/a/b/c") # => { "prefix" => "a", "page" => "b", "optional" => "c" }
25
+ pattern.params("/a/b/c/") # => { "prefix" => "a/b", "page" => "c", "optional" => nil }
26
+
27
+ pattern.expand(prefix: "a", page: "foo") # => "/a/foo/"
28
+ pattern.expand(prefix: "a/b", page: "c/d") # => "/a/b/c%2Fd/"
29
+
30
+ require 'mustermann'
31
+ Mustermann.register(:my_pattern, MyPattern, load: false)
32
+ Mustermann.new('/+{prefix}/~{page}/?{optional}', type: :my_pattern) # => #<MyPattern:"/+{prefix}/~{page}/?{optional}">
33
+
34
+ require 'sinatra/base'
35
+ class MyApp < Sinatra::Base
36
+ register Mustermann
37
+ set :pattern, type: :my_pattern
38
+
39
+ get '/hello/~{name}' do
40
+ "Hello #{params[:name].capitalize}!"
41
+ end
42
+ end
43
+
44
+ require 'mustermann/ast/tree_renderer'
45
+ ast = MyPattern::Parser.parse(pattern.to_s)
46
+ puts Mustermann::AST::TreeRenderer.render(ast)
47
+
48
+ ```
49
+
50
+ ## Pattern Registration
51
+
52
+ ...
53
+
54
+ ## Build Your Own Pattern
55
+
56
+ ...
57
+
58
+ ## Patterns Based on Regular Expressions
59
+
60
+ ...
61
+
62
+ ## AST-Based Patterns
63
+
64
+ ...
@@ -1,18 +1,27 @@
1
1
  # Namespace and main entry point for the Mustermann library.
2
2
  #
3
- # Under normal circumstences the only external API entry point you should be using is {Mustermann.new}.
3
+ # Under normal circumstances the only external API entry point you should be using is {Mustermann.new}.
4
4
  module Mustermann
5
- # @param [String] string The string represenation of the new pattern
5
+ # @param [String] string The string representation of the new pattern
6
6
  # @param [Hash] options The options hash
7
7
  # @return [Mustermann::Pattern] pattern corresponding to string.
8
+ # @raise (see [])
9
+ # @raise (see Mustermann::Pattern.new)
8
10
  # @see file:README.md#Types_and_Options "Types and Options" in the README
9
11
  def self.new(string, type: :sinatra, **options)
10
12
  options.any? ? self[type].new(string, **options) : self[type].new(string)
11
13
  end
12
14
 
13
- # @!visibility private
15
+ # Maps a type to its factory.
16
+ #
17
+ # @example
18
+ # Mustermann[:sinatra] # => Mustermann::Sinatra
19
+ #
20
+ # @param [Symbol] key a pattern type identifier
21
+ # @raise [ArgumentError] if the type is not supported
22
+ # @return [Class, #new] pattern factory
14
23
  def self.[](key)
15
- constant, library = register.fetch(key)
24
+ constant, library = register.fetch(key) { raise ArgumentError, "unsupported type %p" % key }
16
25
  require library if library
17
26
  constant.respond_to?(:new) ? constant : register[key] = const_get(constant)
18
27
  end
@@ -26,7 +35,7 @@ module Mustermann
26
35
 
27
36
  # @!visibility private
28
37
  def self.extend_object(object)
29
- return super unless defined? ::Sinatra::Base and object < ::Sinatra::Base
38
+ return super unless defined? ::Sinatra::Base and object.is_a? Class and object < ::Sinatra::Base
30
39
  require 'mustermann/extension'
31
40
  object.register Extension
32
41
  end
@@ -0,0 +1,150 @@
1
+ require 'mustermann/ast/translator'
2
+
3
+ module Mustermann
4
+ # @see Mustermann::AST::Pattern
5
+ module AST
6
+ # Regexp compilation logic.
7
+ # @!visibility private
8
+ class Compiler < Translator
9
+ raises CompileError
10
+
11
+ # Trivial compilations
12
+ translate(Array) { |**o| map { |e| t(e, **o) }.join }
13
+ translate(:node) { |**o| t(payload, **o) }
14
+ translate(:separator) { |**o| Regexp.escape(payload) }
15
+ translate(:optional) { |**o| "(?:%s)?" % t(payload, **o) }
16
+ translate(:char) { |**o| t.encoded(payload, **o) }
17
+
18
+ translate :expression do |greedy: true, **options|
19
+ t(payload, allow_reserved: operator.allow_reserved, greedy: greedy && !operator.allow_reserved,
20
+ parametric: operator.parametric, separator: operator.separator, **options)
21
+ end
22
+
23
+ translate :with_look_ahead do |**options|
24
+ lookahead = each_leaf.inject("") do |ahead, element|
25
+ ahead + t(element, skip_optional: true, lookahead: ahead, greedy: false, no_captures: true, **options).to_s
26
+ end
27
+ lookahead << (at_end ? '$' : '/')
28
+ t(head, lookahead: lookahead, **options) + t(payload, **options)
29
+ end
30
+
31
+ # Capture compilation is complex. :(
32
+ # @!visibility private
33
+ class Capture < NodeTranslator
34
+ register :capture
35
+
36
+ # @!visibility private
37
+ def translate(**options)
38
+ return pattern(options) if options[:no_captures]
39
+ "(?<#{name}>#{translate(no_captures: true, **options)})"
40
+ end
41
+
42
+ # @return [String] regexp without the named capture
43
+ # @!visibility private
44
+ def pattern(capture: nil, **options)
45
+ case capture
46
+ when Symbol then from_symbol(capture, **options)
47
+ when Array then from_array(capture, **options)
48
+ when Hash then from_hash(capture, **options)
49
+ when String then from_string(capture, **options)
50
+ when nil then from_nil(**options)
51
+ else capture
52
+ end
53
+ end
54
+
55
+ private
56
+ def qualified(string, greedy: true, **options) "#{string}+#{?? unless greedy}" end
57
+ def with_lookahead(string, lookahead: nil, **options) lookahead ? "(?:(?!#{lookahead})#{string})" : string end
58
+ def from_hash(hash, **options) pattern(capture: hash[name.to_sym], **options) end
59
+ def from_array(array, **options) Regexp.union(*array.map { |e| pattern(capture: e, **options) }) end
60
+ def from_symbol(symbol, **options) qualified(with_lookahead("[[:#{symbol}:]]", **options), **options) end
61
+ def from_string(string, **options) Regexp.new(string.chars.map { |c| t.encoded(c, **options) }.join) end
62
+ def from_nil(**options) qualified(with_lookahead(default(**options), **options), **options) end
63
+ def default(**options) "[^/\\?#]" end
64
+ end
65
+
66
+ # @!visibility private
67
+ class Splat < Capture
68
+ register :splat, :named_splat
69
+ # splats are always non-greedy
70
+ # @!visibility private
71
+ def pattern(**options)
72
+ ".*?"
73
+ end
74
+ end
75
+
76
+ # @!visibility private
77
+ class Variable < Capture
78
+ register :variable
79
+
80
+ # @!visibility private
81
+ def translate(**options)
82
+ return super(**options) if explode or not options[:parametric]
83
+ parametric super(parametric: false, **options)
84
+ end
85
+
86
+ # @!visibility private
87
+ def pattern(parametric: false, separator: nil, **options)
88
+ register_param(parametric: parametric, separator: separator, **options)
89
+ pattern = super(**options)
90
+ pattern = parametric(pattern) if parametric
91
+ pattern = "#{pattern}(?:#{Regexp.escape(separator)}#{pattern})*" if explode and separator
92
+ pattern
93
+ end
94
+
95
+ # @!visibility private
96
+ def parametric(string)
97
+ "#{Regexp.escape(name)}(?:=#{string})?"
98
+ end
99
+
100
+ # @!visibility private
101
+ def qualified(string, **options)
102
+ prefix ? "#{string}{1,#{prefix}}" : super(string, **options)
103
+ end
104
+
105
+ # @!visibility private
106
+ def default(allow_reserved: false, **options)
107
+ allow_reserved ? '[\w\-\.~%\:/\?#\[\]@\!\$\&\'\(\)\*\+,;=]' : '[\w\-\.~%]'
108
+ end
109
+
110
+ # @!visibility private
111
+ def register_param(parametric: false, split_params: nil, separator: nil, **options)
112
+ return unless explode and split_params
113
+ split_params[name] = { separator: separator, parametric: parametric }
114
+ end
115
+ end
116
+
117
+ # @return [String] Regular expression for matching the given character in all representations
118
+ # @!visibility private
119
+ def encoded(char, uri_decode: true, space_matches_plus: true, **options)
120
+ return Regexp.escape(char) unless uri_decode
121
+ encoded = escape(char, escape: /./)
122
+ list = [escape(char), encoded.downcase, encoded.upcase].uniq.map { |c| Regexp.escape(c) }
123
+ list << encoded('+') if space_matches_plus and char == " "
124
+ "(?:%s)" % list.join("|")
125
+ end
126
+
127
+ # Compiles an AST to a regular expression.
128
+ # @param [Mustermann::AST::Node] ast the tree
129
+ # @return [Regexp] corresponding regular expression.
130
+ #
131
+ # @!visibility private
132
+ def self.compile(ast, **options)
133
+ new.compile(ast, **options)
134
+ end
135
+
136
+ # Compiles an AST to a regular expression.
137
+ # @param [Mustermann::AST::Node] ast the tree
138
+ # @return [Regexp] corresponding regular expression.
139
+ #
140
+ # @!visibility private
141
+ def compile(ast, except: nil, **options)
142
+ except &&= "(?!#{translate(except, no_captures: true, **options)}\\Z)"
143
+ expression = "\\A#{except}#{translate(ast, **options)}\\Z"
144
+ Regexp.new(expression)
145
+ end
146
+ end
147
+
148
+ private_constant :Compiler
149
+ end
150
+ end