Permalink
Browse files

have Mustermann::Sinatra#| generate sinatra patterns instead of compo…

…site patterns if we can be sure it will behave the same
  • Loading branch information...
rkh committed Nov 29, 2014
1 parent cab773a commit 1962330cb73d334258e0088debcef406747cf5af
@@ -57,15 +57,15 @@ module Mustermann
# @raise (see Mustermann::Pattern.new)
# @raise [TypeError] if the passed object cannot be converted to a pattern
# @see file:README.md#Types_and_Options "Types and Options" in the README
def self.new(*input, type: DEFAULT_TYPE, **options)
def self.new(*input, type: DEFAULT_TYPE, operator: :|, **options)
type ||= DEFAULT_TYPE
input = input.first if input.size < 2
case input
when Pattern then input
when Regexp then self[:regexp].new(input, **options)
when String then self[type].new(input, **options)
when Symbol then self[:sinatra].new(input.inspect, **options)
when Array then Composite.new(input, type: type, **options)
when Array then input.map { |i| new(i, type: type, **options) }.inject(operator)
else
pattern = input.to_pattern(type: type, **options) if input.respond_to? :to_pattern
raise TypeError, "#{input.class} can't be coerced into Mustermann::Pattern" if pattern.nil?
@@ -8,9 +8,9 @@ class Composite < Pattern
supported_options :operator, :type
# @see Mustermann::Pattern.supported?
def self.supported?(option, **options)
def self.supported?(option, type: nil, **options)
return true if super
options[:type] and Mustermann[options[:type]].supported?(option, **options)
Mustermann[type || Mustermann::DEFAULT_TYPE].supported?(option, **options)
end
# @return [Mustermann::Pattern] a new composite pattern
@@ -47,16 +47,23 @@ def self.supported?(option, **options)
# @return [Mustermann::Pattern] a new instance of Mustermann::Pattern
# @see #initialize
def self.new(string, ignore_unknown_options: false, **options)
unless ignore_unknown_options
if ignore_unknown_options
options = options.select { |key, value| supported?(key, **options) if key != :ignore_unknown_options }
else
unsupported = options.keys.detect { |key| not supported?(key, **options) }
raise ArgumentError, "unsupported option %p for %p" % [unsupported, self] if unsupported
end
@map ||= Tool::EqualityMap.new
@map.fetch(string, options) { super(string, options) }
@map.fetch(string, options) { super(string, options) { options } }
end
supported_options :uri_decode, :ignore_unknown_options
attr_reader :uri_decode
# options hash passed to new (with unsupported options removed)
# @!visibility private
attr_reader :options
# @overload initialize(string, **options)
# @param [String] string the string representation of the pattern
@@ -67,6 +74,7 @@ def self.new(string, ignore_unknown_options: false, **options)
def initialize(string, uri_decode: true, **options)
@uri_decode = uri_decode
@string = string.to_s.dup
@options = yield.freeze if block_given?
end
# @return [String] the string representation of the pattern
@@ -234,7 +242,7 @@ def expand(behavior = nil, values = {})
# pattern |= Mustermann.new('/example/*nested')
# pattern.to_templates # => ["/{name}", "/example/{+nested}"]
#
# Template generation is supported by almost all patterns (notable execptions are
# Template generation is supported by almost all patterns (notable exceptions are
# {Mustermann::Shell}, {Mustermann::Regular} and {Mustermann::Simple}).
# Union {Mustermann::Composite} patterns (with the | operator) support template generation
# if all patterns they are composed of also support it.
@@ -284,7 +292,7 @@ def to_templates
# @param [Mustermann::Pattern, String] other the other pattern
# @return [Mustermann::Pattern] a composite pattern
def |(other)
Mustermann.new(self, other, operator: __callee__, type: :identity)
Mustermann::Composite.new(self, other, operator: __callee__, type: :identity)
end
alias_method :&, :|
@@ -332,7 +340,7 @@ def map_param(key, value)
end
# @!visibility private
def unescape(string, decode = @uri_decode)
def unescape(string, decode = uri_decode)
return string unless decode and string
@@uri.unescape(string)
end
@@ -1,4 +1,5 @@
require 'mustermann'
require 'mustermann/identity'
require 'mustermann/ast/pattern'
module Mustermann
@@ -31,5 +32,53 @@ class Sinatra < AST::Pattern
suffix ?? do |char, element|
node(:optional, element)
end
# Takes a string and espaces any characters that have special meaning for Sinatra patterns.
#
# @example
# require 'mustermann/sinatra'
# Mustermann::Sinatra.escape("/:name") # => "/\\:name"
#
# @param [#to_s] string the input string
# @return [String] the escaped string
def self.escape(string)
string.to_s.gsub(/[\?\(\)\*:\\\|\{\}]/) { |c| "\\#{c}" }
end
# Tries to convert the given input object to a Sinatra pattern with the given options, without
# changing its parsing semantics.
# @return [Mustermann::Sinatra, nil] the converted pattern, if possible
# @!visibility private
def self.try_convert(input, **options)
case input
when String then new(escape(input), **options)
when Identity then new(escape(input), **options) if input.uri_decode == options.fetch(:uri_decode, true)
when self then input if input.options == options
end
end
# Creates a pattern that matches any string matching either one of the patterns.
# If a string is supplied, it is treated as a fully escaped Sinatra pattern.
#
# If the other pattern is also a Sintara pattern, it might join the two to a third
# sinatra pattern instead of generating a composite for efficency reasons.
#
# This only happens if the sinatra pattern behaves exactly the same as a composite
# would in regards to matching, parsing, expanding and template generation.
#
# @example
# pattern = Mustermann.new('/foo/:name') | Mustermann.new('/:first/:second')
# pattern === '/foo/bar' # => true
# pattern === '/fox/bar' # => true
# pattern === '/foo' # => false
#
# @param [Mustermann::Pattern, String] other the other pattern
# @return [Mustermann::Pattern] a composite pattern
# @see Mustermann::Pattern#|
def |(other)
return super unless converted = self.class.try_convert(other, **options)
return super unless converted.names.empty? or names.empty?
self.class.new(@string + "|" + converted.to_s, **options)
end
end
end
@@ -12,6 +12,14 @@
pattern = Mustermann.new('/foo')
Mustermann::Composite.new(pattern).should be == pattern
end
example 'with supported type specific arguments' do
Mustermann::Composite.new("/a", "/b", greedy: true)
end
example 'with unsupported type specific arguments' do
expect { Mustermann::Composite.new("/a", "/b", greedy: true, type: :identity) }.to raise_error(ArgumentError)
end
end
context :| do
@@ -37,11 +37,14 @@
end
context "multiple arguments" do
example { Mustermann.new('', '') .should be_a(Mustermann::Composite) }
example { Mustermann.new('', '').patterns.first .should be_a(Mustermann::Sinatra) }
example { Mustermann.new('', '').operator .should be == :| }
example { Mustermann.new('', '', operator: :&).operator .should be == :& }
example { Mustermann.new('', '', greedy: true) .should be_a(Mustermann::Composite) }
example { Mustermann.new(':a', ':b/:a') .should be_a(Mustermann::Composite) }
example { Mustermann.new(':a', ':b/:a').patterns.first .should be_a(Mustermann::Sinatra) }
example { Mustermann.new(':a', ':b/:a').operator .should be == :| }
example { Mustermann.new(':a', ':b/:a', operator: :&).operator .should be == :& }
example { Mustermann.new(':a', ':b/:a', greedy: true) .should be_a(Mustermann::Composite) }
example { Mustermann.new('/foo', ':bar') .should be_a(Mustermann::Sinatra) }
example { Mustermann.new('/foo', ':bar').to_s .should be == "/foo|:bar" }
end
context "invalid arguments" do
@@ -752,4 +752,55 @@
example { pattern.peek_params("/foo bar") .should be_nil }
end
end
describe :| do
let(:first) { Mustermann.new("a") }
let(:second) { Mustermann.new("b") }
subject(:composite) { first | second }
context "with no capture names" do
its(:class) { should be == Mustermann::Sinatra }
its(:to_s) { should be == "a|b" }
end
context "only first has captures" do
let(:first) { Mustermann.new(":a") }
its(:class) { should be == Mustermann::Sinatra }
its(:to_s) { should be == ":a|b" }
end
context "only second has captures" do
let(:second) { Mustermann.new(":b") }
its(:class) { should be == Mustermann::Sinatra }
its(:to_s) { should be == "a|:b" }
end
context "both have captures" do
let(:first) { Mustermann.new(":a") }
let(:second) { Mustermann.new(":b") }
its(:class) { should be == Mustermann::Composite }
end
context "options mismatch" do
let(:second) { Mustermann.new(":b", greedy: false) }
its(:class) { should be == Mustermann::Composite }
end
context "argument is a string" do
let(:second) { ":b" }
its(:class) { should be == Mustermann::Sinatra }
its(:to_s) { should be == "a|\\:b" }
end
context "argument is an identity pattern" do
let(:second) { Mustermann::Identity.new(":b") }
its(:class) { should be == Mustermann::Sinatra }
its(:to_s) { should be == "a|\\:b" }
end
context "argument is an identity pattern, but options mismatch" do
let(:second) { Mustermann::Identity.new(":b", uri_decode: false) }
its(:class) { should be == Mustermann::Composite }
end
end
end

0 comments on commit 1962330

Please sign in to comment.