From fb2026197b013828e90a53169c0379bb1d30a66d Mon Sep 17 00:00:00 2001 From: Konstantin Haase Date: Thu, 27 Nov 2014 00:04:05 +0100 Subject: [PATCH] Sinatra 2.0 syntax: allow | outside of parens, see #31 --- mustermann-visualizer/spec/visualizer_spec.rb | 10 ++++ mustermann/README.md | 2 +- mustermann/lib/mustermann/ast/node.rb | 4 ++ mustermann/lib/mustermann/ast/parser.rb | 21 +------ mustermann/lib/mustermann/ast/transformer.rb | 56 +++++++++++++++++-- mustermann/lib/mustermann/sinatra.rb | 10 +--- mustermann/spec/sinatra_spec.rb | 17 ++++-- 7 files changed, 83 insertions(+), 37 deletions(-) diff --git a/mustermann-visualizer/spec/visualizer_spec.rb b/mustermann-visualizer/spec/visualizer_spec.rb index fc0130e..8cd6150 100644 --- a/mustermann-visualizer/spec/visualizer_spec.rb +++ b/mustermann-visualizer/spec/visualizer_spec.rb @@ -54,6 +54,16 @@ its(:to_sexp) { should be == '(root (char a) (char " ") (char b))' } end + context 'a|b' do + let(:pattern) { Mustermann.new('a|b') } + its(:to_sexp) { should be == '(root (union (char a) | (char b)))' } + end + + context '(a|b)' do + let(:pattern) { Mustermann.new('(a|b)c') } + its(:to_sexp) { should be == '(root (union "(" (char a) | (char b) ")") (char c))' } + end + context '\:a' do let(:pattern) { Mustermann.new('\:a') } its(:to_sexp) { should be == '(root (escaped "\\\\" (escaped_char :)) (char a))' } diff --git a/mustermann/README.md b/mustermann/README.md index d9704b8..8ba2344 100644 --- a/mustermann/README.md +++ b/mustermann/README.md @@ -811,7 +811,7 @@ This comes with a few trade-offs: - (expression|expression|...) + expression|expression|... Will match anything matching the nested expressions. May contain any other syntax element, including captures. diff --git a/mustermann/lib/mustermann/ast/node.rb b/mustermann/lib/mustermann/ast/node.rb index cfe9960..50d28ef 100644 --- a/mustermann/lib/mustermann/ast/node.rb +++ b/mustermann/lib/mustermann/ast/node.rb @@ -152,6 +152,10 @@ class Union < Composition class Optional < Node end + # @!visibility private + class Or < Node + end + # @!visibility private class Root < Node # @!visibility private diff --git a/mustermann/lib/mustermann/ast/parser.rb b/mustermann/lib/mustermann/ast/parser.rb index 6f9d358..65423c8 100644 --- a/mustermann/lib/mustermann/ast/parser.rb +++ b/mustermann/lib/mustermann/ast/parser.rb @@ -124,25 +124,8 @@ def read_suffix(element) # @return [String, MatchData, nil] # @!visibility private def scan(regexp) - match_buffer(:scan, regexp) - end - - # Wrapper around {StringScanner#check} that turns strings into escaped - # regular expressions and returns a MatchData if the regexp has any - # named captures. - # - # @param [Regexp, String] regexp - # @see StringScanner#check - # @return [String, MatchData, nil] - # @!visibility private - def check(regexp) - match_buffer(:check, regexp) - end - - # @!visibility private - def match_buffer(method, regexp) regexp = Regexp.new(Regexp.escape(regexp)) unless regexp.is_a? Regexp - string = buffer.public_send(method, regexp) + string = buffer.scan(regexp) regexp.names.any? ? regexp.match(string) : string end @@ -245,8 +228,6 @@ def unexpected(char = nil, exception: ParseError) char = "space" if char == " " raise exception, "unexpected #{char || "end of string"} while parsing #{string.inspect}" end - - private :match_buffer end private_constant :Parser diff --git a/mustermann/lib/mustermann/ast/transformer.rb b/mustermann/lib/mustermann/ast/transformer.rb index 914a237..1b38388 100644 --- a/mustermann/lib/mustermann/ast/transformer.rb +++ b/mustermann/lib/mustermann/ast/transformer.rb @@ -15,10 +15,58 @@ def self.transform(tree) new.translate(tree) end - translate(:node) { self } - translate(:group, :root) do - self.payload = t(payload) - self + # recursive descent + translate(:node) do + node.payload = t(payload) + node + end + + # ignore unknown objects on the tree + translate(Object) { node } + + # turn a group containing or nodes into a union + # @!visibility private + class GroupTransformer < NodeTranslator + register :group + + # @!visibility private + def translate + return union if payload.any? { |e| e.is_a? :or } + self.payload = t(payload) + self + end + + # @!visibility private + def union + groups = split_payload.map { |g| group(g) } + Node[:union].new(groups, start: node.start, stop: node.stop) + end + + # @!visibility private + def group(elements) + return t(elements.first) if elements.size == 1 + start, stop = elements.first.start, elements.last.stop if elements.any? + Node[:group].new(t(elements), start: start, stop: stop) + end + + # @!visibility private + def split_payload + groups = [[]] + payload.each { |e| e.is_a?(:or) ? groups << [] : groups.last << e } + groups.map! + end + end + + # inject a union node right inside the root node if it contains or nodes + # @!visibility private + class RootTransformer < GroupTransformer + register :root + + # @!visibility private + def union + self.payload = [super] + self + end end # URI expression transformations depending on operator diff --git a/mustermann/lib/mustermann/sinatra.rb b/mustermann/lib/mustermann/sinatra.rb index 58535a3..5825aba 100644 --- a/mustermann/lib/mustermann/sinatra.rb +++ b/mustermann/lib/mustermann/sinatra.rb @@ -12,17 +12,13 @@ module Mustermann class Sinatra < AST::Pattern register :sinatra - on(nil, ??, ?), ?|) { |c| unexpected(c) } + on(nil, ??, ?)) { |c| unexpected(c) } on(?*) { |c| scan(/\w+/) ? node(:named_splat, buffer.matched) : node(:splat) } on(?:) { |c| node(:capture) { scan(/\w+/) } } on(?\\) { |c| node(:char, expect(/./)) } - - on ?( do |char| - groups = [] - groups << node(:group) { read unless check(?)) or scan(?|) } until scan(?)) - groups.size == 1 ? groups.first : node(:union, groups) - end + on(?() { |c| node(:group) { read unless scan(?)) } } + on(?|) { |c| node(:or) } on ?{ do |char| type = scan(?+) ? :named_splat : :capture diff --git a/mustermann/spec/sinatra_spec.rb b/mustermann/spec/sinatra_spec.rb index c376c47..509113e 100644 --- a/mustermann/spec/sinatra_spec.rb +++ b/mustermann/spec/sinatra_spec.rb @@ -463,6 +463,18 @@ it { should_not expand(a: 'foo', b: 'bar', c: 'baz') } end + pattern "/:a/:b|:c" do + it { should match("foo") .capturing c: 'foo' } + it { should match("/foo/bar") .capturing a: 'foo', b: 'bar' } + + it { should generate_template('/{a}/{b}') } + it { should generate_template('{c}') } + + it { should expand(a: 'foo', b: 'bar') .to('/foo/bar') } + it { should expand(c: 'foo') .to('foo') } + it { should_not expand(a: 'foo', b: 'bar', c: 'baz') } + end + pattern '/:foo', capture: /\d+/ do it { should match('/1') .capturing foo: '1' } it { should match('/123') .capturing foo: '123' } @@ -648,11 +660,6 @@ to raise_error(Mustermann::ParseError, 'unexpected ? while parsing "foo??bar"') end - example '| outside of group' do - expect { Mustermann::Sinatra.new('foo|bar') }. - to raise_error(Mustermann::ParseError, 'unexpected | while parsing "foo|bar"') - end - example 'dangling escape' do expect { Mustermann::Sinatra.new('foo\\') }. to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "foo\\\\"')