require 'mustermann/error' require 'mustermann/simple_match' require 'mustermann/equality_map' require 'uri' module Mustermann # Superclass for all pattern implementations. # @abstract class Pattern include Mustermann @@uri ||= URI::Parser.new # List of supported options. # # @overload supported_options # @return [Array] list of supported options # @overload supported_options(*list) # Adds options to the list. # # @api private # @param [Symbol] *list adds options to the list of supported options # @return [Array] list of supported options def self.supported_options(*list) @supported_options ||= [] options = @supported_options.concat(list) options += superclass.supported_options if self < Pattern options end # Registers the pattern with Mustermann. # @see Mustermann.register # @!visibility private def self.register(*names) names.each { |name| Mustermann.register(name, self) } end # @param [Symbol] option The option to check. # @return [Boolean] Whether or not option is supported. def self.supported?(option, **options) supported_options.include? option end # @overload new(string, **options) # @param (see #initialize) # @raise (see #initialize) # @raise [ArgumentError] if some option is not supported # @return [Mustermann::Pattern] a new instance of Mustermann::Pattern # @see #initialize def self.new(string, ignore_unknown_options: false, **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 ||= EqualityMap.new @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 # @param [Hash] options options for fine-tuning the pattern behavior # @raise [Mustermann::Error] if the pattern can't be generated from the string # @see file:README.md#Types_and_Options "Types and Options" in the README # @see Mustermann.new 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 def to_s @string.dup end # @param [String] string The string to match against # @return [MatchData, Mustermann::SimpleMatch, nil] MatchData or similar object if the pattern matches. # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-match Regexp#match # @see http://ruby-doc.org/core-2.0/MatchData.html MatchData # @see Mustermann::SimpleMatch def match(string) SimpleMatch.new(string) if self === string end # @param [String] string The string to match against # @return [Integer, nil] nil if pattern does not match the string, zero if it does. # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-3D-7E Regexp#=~ def =~(string) 0 if self === string end # @param [String] string The string to match against # @return [Boolean] Whether or not the pattern matches the given string # @note Needs to be overridden by subclass. # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-3D-3D-3D Regexp#=== def ===(string) raise NotImplementedError, 'subclass responsibility' end # Used by Ruby internally for hashing. # @return [Fixnum] same has value for patterns that are equal def hash self.class.hash | @string.hash | options.hash end # Two patterns are considered equal if they are of the same type, have the same pattern string # and the same options. # @return [true, false] def ==(other) other.class == self.class and other.to_s == @string and other.options == options end # Two patterns are considered equal if they are of the same type, have the same pattern string # and the same options. # @return [true, false] def eql?(other) other.class.eql?(self.class) and other.to_s.eql?(@string) and other.options.eql?(options) end # Tries to match the pattern against the beginning of the string (as opposed to the full string). # Will return the count of the matching characters if it matches. # # @example # pattern = Mustermann.new('/:name') # pattern.size("/Frank/Sinatra") # => 6 # # @param [String] string The string to match against # @return [Integer, nil] the number of characters that match def peek_size(string) # this is a very naive, unperformant implementation string.size.downto(0).detect { |s| self === string[0, s] } end # Tries to match the pattern against the beginning of the string (as opposed to the full string). # Will return the substring if it matches. # # @example # pattern = Mustermann.new('/:name') # pattern.peek("/Frank/Sinatra") # => "/Frank" # # @param [String] string The string to match against # @return [String, nil] matched subsctring def peek(string) size = peek_size(string) string[0, size] if size end # Tries to match the pattern against the beginning of the string (as opposed to the full string). # Will return a MatchData or similar instance for the matched substring. # # @example # pattern = Mustermann.new('/:name') # pattern.peek("/Frank/Sinatra") # => # # # @param [String] string The string to match against # @return [MatchData, Mustermann::SimpleMatch, nil] MatchData or similar object if the pattern matches. # @see #peek_params def peek_match(string) matched = peek(string) match(matched) if matched end # Tries to match the pattern against the beginning of the string (as opposed to the full string). # Will return a two element Array with the params parsed from the substring as first entry and the length of # the substring as second. # # @example # pattern = Mustermann.new('/:name') # params, _ = pattern.peek_params("/Frank/Sinatra") # # puts "Hello, #{params['name']}!" # Hello, Frank! # # @param [String] string The string to match against # @return [Array, nil] Array with params hash and length of substing if matched, nil otherwise def peek_params(string) match = peek_match(string) [params(captures: match), match.to_s.size] if match end # @return [Hash{String: Array}] capture names mapped to capture index. # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-named_captures Regexp#named_captures def named_captures {} end # @return [Array] capture names. # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-names Regexp#names def names [] end # @param [String] string the string to match against # @return [Hash{String: String, Array}, nil] Sinatra style params if pattern matches. def params(string = nil, captures: nil, offset: 0) return unless captures ||= match(string) params = named_captures.map do |name, positions| values = positions.map { |pos| map_param(name, captures[pos + offset]) }.flatten values = values.first if values.size < 2 and not always_array? name [name, values] end Hash[params] end # @note This method is only implemented by certain subclasses. # # @example Expanding a pattern # pattern = Mustermann.new('/:name(.:ext)?') # pattern.expand(name: 'hello') # => "/hello" # pattern.expand(name: 'hello', ext: 'png') # => "/hello.png" # # @example Checking if a pattern supports expanding # if pattern.respond_to? :expand # pattern.expand(name: "foo") # else # warn "does not support expanding" # end # # Expanding is supported by almost all patterns (notable execptions are {Mustermann::Shell}, # {Mustermann::Regular} and {Mustermann::Simple}). # # Union {Mustermann::Composite} patterns (with the | operator) support expanding if all # patterns they are composed of also support it. # # @param (see Mustermann::Expander#expand) # @return [String] expanded string # @raise [NotImplementedError] raised if expand is not supported. # @raise [Mustermann::ExpandError] raised if a value is missing or unknown # @see Mustermann::Expander def expand(behavior = nil, values = {}) raise NotImplementedError, "expanding not supported by #{self.class}" end # @note This method is only implemented by certain subclasses. # # Generates a list of URI template strings representing the pattern. # # Note that this transformation is lossy and the strings matching these # templates might not match the pattern (and vice versa). # # This comes in quite handy since URI templates are not made for pattern matching. # That way you can easily use a more precise template syntax and have it automatically # generate hypermedia links for you. # # @example generating templates # Mustermann.new("/:name").to_templates # => ["/{name}"] # Mustermann.new("/:foo(@:bar)?/*baz").to_templates # => ["/{foo}@{bar}/{+baz}", "/{foo}/{+baz}"] # Mustermann.new("/{name}", type: :template).to_templates # => ["/{name}"] # # @example generating templates from composite patterns # pattern = Mustermann.new('/:name') # pattern |= Mustermann.new('/{name}', type: :template) # pattern |= Mustermann.new('/example/*nested') # pattern.to_templates # => ["/{name}", "/example/{+nested}"] # # 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. # # @example Checking if a pattern supports expanding # if pattern.respond_to? :to_templates # pattern.to_templates # else # warn "does not support template generation" # end # # @return [Array] list of URI templates def to_templates raise NotImplementedError, "template generation not supported by #{self.class}" end # @overload |(other) # Creates a pattern that matches any string matching either one of the patterns. # If a string is supplied, it is treated as an identity pattern. # # @example # pattern = Mustermann.new('/foo/:name') | Mustermann.new('/:first/:second') # pattern === '/foo/bar' # => true # pattern === '/fox/bar' # => true # pattern === '/foo' # => false # # @overload &(other) # Creates a pattern that matches any string matching both of the patterns. # If a string is supplied, it is treated as an identity pattern. # # @example # pattern = Mustermann.new('/foo/:name') & Mustermann.new('/:first/:second') # pattern === '/foo/bar' # => true # pattern === '/fox/bar' # => false # pattern === '/foo' # => false # # @overload ^(other) # Creates a pattern that matches any string matching exactly one of the patterns. # If a string is supplied, it is treated as an identity pattern. # # @example # pattern = Mustermann.new('/foo/:name') ^ Mustermann.new('/:first/:second') # pattern === '/foo/bar' # => false # pattern === '/fox/bar' # => true # pattern === '/foo' # => false # # @param [Mustermann::Pattern, String] other the other pattern # @return [Mustermann::Pattern] a composite pattern def |(other) Mustermann::Composite.new(self, other, operator: __callee__, type: :identity) end 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/"] # strings.detect(&pattern) # => "/foo/bar" # # @return [Proc] proc wrapping {#===} def to_proc @to_proc ||= method(:===).to_proc end # @!visibility private # @return [Boolean] # @see Object#respond_to? def respond_to?(method, *args) return super unless %i[expand to_templates].include? method respond_to_special?(method) end # @!visibility private # @return [Boolean] # @see #respond_to? def respond_to_special?(method) method(method).owner != Mustermann::Pattern end # @!visibility private def inspect "#<%p:%p>" % [self.class, @string] end # @!visibility private def simple_inspect type = self.class.name[/[^:]+$/].downcase "%s:%p" % [type, @string] end # @!visibility private def map_param(key, value) unescape(value, true) end # @!visibility private def unescape(string, decode = uri_decode) return string unless decode and string @@uri.unescape(string) end # @!visibility private ALWAYS_ARRAY = %w[splat captures] # @!visibility private def always_array?(key) ALWAYS_ARRAY.include? key end private :unescape, :map_param, :respond_to_special? private_constant :ALWAYS_ARRAY end end