Permalink
Browse files

add concatenation

  • Loading branch information...
rkh committed Nov 29, 2014
1 parent 435c46b commit 500a603fffe0594ab842d72addcc449eedd6d5be
@@ -10,6 +10,7 @@ module Mustermann
# @see Mustermann::Pattern
# @see file:README.md#flask Syntax description in the README
class Flask < AST::Pattern
include Concat::Native
register :flask
on(nil, ?>, ?:) { |c| unexpected(c) }
@@ -11,6 +11,7 @@ module Mustermann
# @see Mustermann::Pattern
# @see file:README.md#shell Syntax description in the README
class Shell < Pattern
include Concat::Native
register :shell
# @!visibility private
@@ -11,6 +11,7 @@ module Mustermann
# @see file:README.md#template Syntax description in the README
# @see http://tools.ietf.org/html/rfc6570 RFC 6570
class Template < AST::Pattern
include Concat::Native
register :template, :uri_template
on ?{ do |char|
View
@@ -27,7 +27,7 @@ pattern.params('/a/b.c') # => { "prefix" => "a", splat => ["b", "c"] }
* **[Pattern Types](#-pattern-types):** Mustermann supports a wide variety of different pattern types, making it compatible with a large variety of existing software.
* **[Fine Grained Control](#-available-options):** You can easily adjust matching behavior and add constraints to the placeholders and capture groups.
* **[Binary Operators](#-binary-operators):** Patterns can be combined into composite patterns using binary operators.
* **[Binary Operators](#-binary-operators) and [Concatenation](#-concatenation):** Patterns can be combined into composite patterns using binary operators.
* **[Regexp Look Alike](#-regexp-look-alike):** Mustermann patterns can be used as a replacement for regular expressions.
* **[Parameter Parsing](#-parameter-parsing):** Mustermann can parse matched parameters into a Sinatra-style "params" hash, including type casting.
* **[Peeking](#-peeking):** Lets you check if the beginning of a string matches a pattern.
@@ -95,6 +95,22 @@ first ^ second === "/foo/bar" # => false
These resulting objects are fully functional pattern objects, allowing you to call methods like `params` or `to_proc` on them. Moreover, *or* patterns created solely from expandable patterns will also be expandable. The same logic also applies to generating templates from *or* patterns.
<a name="-concatenation"></a>
## Concatenation
Similar to [Binary Operators](#-binary-operators), two patterns can be concatenated using `+`.
``` ruby
require 'mustermann'
prefix = Mustermann.new("/:prefix")
about = prefix + "/about"
about.params("/main/about") # => {"prefix" => "main"}
```
Patterns of different types can be mixed. The availability of `to_templates` and `expand` depends on the patterns being concatenated.
<a name="-regexp-look-alike"></a>
## Regexp Look Alike
@@ -1,5 +1,6 @@
require 'mustermann/pattern'
require 'mustermann/composite'
require 'mustermann/concat'
require 'thread'
# Namespace and main entry point for the Mustermann library.
@@ -61,7 +61,7 @@ def expand(behavior = nil, values = {})
@expander.expand(behavior, values)
end
# (see Mustermann::Pattern#expand)
# (see Mustermann::Pattern#to_templates)
def to_templates
raise NotImplementedError, 'template generation not supported' unless respond_to? :to_templates
patterns.flat_map(&:to_templates).uniq
@@ -0,0 +1,124 @@
module Mustermann
# Class for pattern objects that are a concatenation of other patterns.
# @see Mustermann::Pattern#+
class Concat < Composite
# Mixin for patterns to support native concatenation.
# @!visibility private
module Native
# @see Mustermann::Pattern#+
# @!visibility private
def +(other)
other &&= Mustermann.new(other, type: :identity, **options)
return super unless native = native_concat(other)
self.class.new(native, **options)
end
# @!visibility private
def native_concat(other)
"#{self}#{other}" if native_concat?(other)
end
# @!visibility private
def native_concat?(other)
other.class == self.class and other.options == options
end
private :native_concat, :native_concat?
end
# Should not be used directly.
# @!visibility private
def initialize(*)
super
AST::Validation.validate(combined_ast) if respond_to? :expand
end
# @see Mustermann::Composite#operator
# @return [Symbol] always :+
def operator
:+
end
# @see Mustermann::Pattern#===
def ===(string)
peek_size(string) == string.size
end
# @see Mustermann::Pattern#match
def match(string)
peeked = peek_match(string)
peeked if peeked.to_s == string
end
# @see Mustermann::Pattern#params
def params(string)
params, size = peek_params(string)
params if size == string.size
end
# @see Mustermann::Pattern#peek_size
def peek_size(string)
pump(string) { |p,s| p.peek_size(s) }
end
# @see Mustermann::Pattern#peek_match
def peek_match(string)
pump(string, initial: SimpleMatch.new) do |pattern, substring|
return unless match = pattern.peek_match(substring)
[match, match.to_s.size]
end
end
# @see Mustermann::Pattern#peek_params
def peek_params(string)
pump(string, inject_with: :merge, with_size: true) { |p, s| p.peek_params(s) }
end
# (see Mustermann::Pattern#expand)
def expand(behavior = nil, values = {})
raise NotImplementedError, 'expanding not supported' unless respond_to? :expand
@expander ||= Mustermann::Expander.new(self) { combined_ast }
@expander.expand(behavior, values)
end
# (see Mustermann::Pattern#to_templates)
def to_templates
raise NotImplementedError, 'template generation not supported' unless respond_to? :to_templates
@to_templates ||= patterns.inject(['']) { |list, pattern| list.product(pattern.to_templates).map(&:join) }.uniq
end
# @!visibility private
def respond_to_special?(method)
method = :to_ast if method.to_sym == :expand
patterns.all? { |p| p.respond_to?(method) }
end
# used to generate results for various methods by scanning through an input string
# @!visibility private
def pump(string, inject_with: :+, initial: nil, with_size: false)
substring = string
results = Array(initial)
patterns.each do |pattern|
result, size = yield(pattern, substring)
return unless result
results << result
size ||= result
substring = substring[size..-1]
end
results = results.inject(inject_with)
with_size ? [results, string.size - substring.size] : results
end
# generates one big AST from all patterns
# will not check if patterns support AST generation
# @!visibility private
def combined_ast
payload = patterns.map { |p| AST::Node[:group].new(p.to_ast.payload) }
AST::Node[:root].new(payload)
end
private :combined_ast, :pump
end
end
@@ -17,7 +17,7 @@ class Expander
# @param [Array<#to_str, Mustermann::Pattern>] patterns list of patterns to expand, see {#add}.
# @param [Symbol] additional_values behavior when encountering additional values, see {#expand}.
# @param [Hash] options used when creating/expanding patterns, see {Mustermann.new}.
def initialize(*patterns, additional_values: :raise, **options)
def initialize(*patterns, additional_values: :raise, **options, &block)
unless additional_values == :raise or additional_values == :ignore or additional_values == :append
raise ArgumentError, "Illegal value %p for additional_values" % additional_values
end
@@ -27,7 +27,7 @@ def initialize(*patterns, additional_values: :raise, **options)
@additional_values = additional_values
@options = options
@caster = Caster.new(Caster::Nil)
add(*patterns)
add(*patterns, &block)
end
# Add patterns to expand.
@@ -42,8 +42,12 @@ def initialize(*patterns, additional_values: :raise, **options)
def add(*patterns)
patterns.each do |pattern|
pattern = Mustermann.new(pattern, **@options)
raise NotImplementedError, "expanding not supported for #{pattern.class}" unless pattern.respond_to? :to_ast
@api_expander.add(pattern.to_ast)
if block_given?
@api_expander.add(yield(pattern))
else
raise NotImplementedError, "expanding not supported for #{pattern.class}" unless pattern.respond_to? :to_ast
@api_expander.add(pattern.to_ast)
end
@patterns << pattern
end
self
@@ -11,6 +11,7 @@ module Mustermann
# @see Mustermann::Pattern
# @see file:README.md#identity Syntax description in the README
class Identity < Pattern
include Concat::Native
register :identity
# @param (see Mustermann::Pattern#===)
@@ -298,6 +298,24 @@ def |(other)
alias_method :&, :|
alias_method :^, :|
# @example
# require 'mustermann'
# prefix = Mustermann.new("/:prefix")
# about = prefix + "/about"
# about.params("/main/about") # => {"prefix" => "main"}
#
# Creates a concatenated pattern by combingin self with the other pattern supplied.
# Patterns of different types can be mixed. The availability of `to_templates` and
# `expand` depends on the patterns being concatenated.
#
# String input is treated as identity pattern.
#
# @param [Mustermann::Pattern, String] other pattern to be appended
# @return [Mustermann::Pattern] concatenated pattern
def +(other)
Concat.new(self, other, type: :identity)
end
# @example
# pattern = Mustermann.new('/:a/:b')
# strings = ["foo/bar", "/foo/bar", "/foo/bar/"]
@@ -10,6 +10,7 @@ module Mustermann
# @see Mustermann::Pattern
# @see file:README.md#simple Syntax description in the README
class Regular < RegexpBased
include Concat::Native
register :regexp, :regular
# @param (see Mustermann::Pattern#initialize)
@@ -3,8 +3,10 @@ module Mustermann
# @see http://ruby-doc.org/core-2.0/MatchData.html MatchData
class SimpleMatch
# @api private
def initialize(string)
@string = string.dup
def initialize(string = "", names: [], captures: [])
@string = string.dup
@names = names
@captures = captures
end
# @return [String] the string that was matched against
@@ -14,17 +16,28 @@ def to_s
# @return [Array<String>] empty array for imitating MatchData interface
def names
[]
@names.dup
end
# @return [Array<String>] empty array for imitating MatchData interface
def captures
[]
@captures.dup
end
# @return [nil] imitates MatchData interface
def [](*args)
captures[*args]
args.map! do |arg|
next arg unless arg.is_a? Symbol or arg.is_a? String
names.index(arg.to_s)
end
@captures[*args]
end
# @!visibility private
def +(other)
SimpleMatch.new(@string + other.to_s,
names: @names + other.names,
captures: @captures + other.captures)
end
# @return [String] string representation
@@ -11,6 +11,29 @@ module Mustermann
# @see Mustermann::Pattern
# @see file:README.md#sinatra Syntax description in the README
class Sinatra < AST::Pattern
include Concat::Native
# Generates a string that can safely be concatenated with other strings
# without chaning its semantics
# @see #safe_string
# @!visibility private
SafeRenderer = AST::Translator.create do
translate(:splat, :named_splat) { "{+#{name}}" }
translate(:char, :separator) { Sinatra.escape(payload) }
translate(:root) { t(payload) }
translate(:group) { "(#{t(payload)})" }
translate(:union) { "(#{t(payload, join: ?|)})" }
translate(:optional) { "#{t(payload)}?" }
translate(Array) { |join: ""| map { |e| t(e) }.join(join) }
translate(:capture) do
raise Mustermann::Error, 'cannot render variables' if node.is_a? :variable
raise Mustermann::Error, 'cannot translate constraints' if constraint or qualifier or convert
prefix = node.is_a?(:splat) ? "+" : ""
"{#{prefix}#{name}}"
end
end
register :sinatra
on(nil, ??, ?)) { |c| unexpected(c) }
@@ -51,9 +74,10 @@ def self.escape(string)
# @!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
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
when AST::Pattern then new(SafeRenderer.translate(input.to_ast), **options) rescue nil if input.options == options
end
end
@@ -78,7 +102,30 @@ def self.try_convert(input, **options)
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)
self.class.new(safe_string + "|" + converted.safe_string, **options)
end
# Generates a string represenation of the pattern that can safely be used for def interpolation
# without changing its semantics.
#
# @example
# require 'mustermann'
# unsafe = Mustermann.new("/:name")
#
# Mustermann.new("#{unsafe}bar").params("/foobar") # => { "namebar" => "foobar" }
# Mustermann.new("#{unsafe.safe_string}bar").params("/foobar") # => { "name" => "bar" }
#
# @return [String] string representatin of the pattern
def safe_string
@safe_string ||= SafeRenderer.translate(to_ast)
end
# @!visibility private
def native_concat(other)
return unless converted = self.class.try_convert(other, **options)
safe_string + converted.safe_string
end
private :native_concat
end
end
Oops, something went wrong.

0 comments on commit 500a603

Please sign in to comment.