mirror of
https://github.com/moparisthebest/mailcatcher
synced 2025-01-07 03:38:00 -05:00
wip
This commit is contained in:
parent
783992bbe2
commit
a8c4843b13
36
.gitmodules
vendored
Normal file
36
.gitmodules
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
[submodule "vendor/gems/net-http-server"]
|
||||
path = vendor/gems/net-http-server
|
||||
url = git://github.com/postmodern/net-http-server.git
|
||||
[submodule "vendor/gems/parslet"]
|
||||
path = vendor/gems/parslet
|
||||
url = git://github.com/kschiess/parslet.git
|
||||
[submodule "vendor/gems/gserver"]
|
||||
path = vendor/gems/gserver
|
||||
url = git://github.com/ruby/gserver.git
|
||||
[submodule "vendor/gems/mail"]
|
||||
path = vendor/gems/mail
|
||||
url = git://github.com/mikel/mail.git
|
||||
[submodule "vendor/gems/sinatra"]
|
||||
path = vendor/gems/sinatra
|
||||
url = git://github.com/sinatra/sinatra.git
|
||||
[submodule "vendor/gems/rack"]
|
||||
path = vendor/gems/rack
|
||||
url = git://github.com/rack/rack.git
|
||||
[submodule "vendor/gems/websocket-ruby"]
|
||||
path = vendor/gems/websocket-ruby
|
||||
url = git://github.com/imanel/websocket-ruby.git
|
||||
[submodule "vendor/gems/midi-smtp-server"]
|
||||
path = vendor/gems/midi-smtp-server
|
||||
url = git://github.com/4commerce-technologies-AG/midi-smtp-server.git
|
||||
[submodule "vendor/gems/mime-types"]
|
||||
path = vendor/gems/mime-types
|
||||
url = git://github.com/mime-types/ruby-mime-types.git
|
||||
[submodule "vendor/gems/mime-types-data"]
|
||||
path = vendor/gems/mime-types-data
|
||||
url = git://github.com/mime-types/mime-types-data.git
|
||||
[submodule "vendor/gems/rack-protection"]
|
||||
path = vendor/gems/rack-protection
|
||||
url = git://github.com/sinatra/rack-protection.git
|
||||
[submodule "vendor/gems/tilt"]
|
||||
path = vendor/gems/tilt
|
||||
url = git://github.com/rtomayko/tilt.git
|
2
Gemfile
2
Gemfile
@ -2,6 +2,8 @@ source "https://rubygems.org"
|
||||
|
||||
gemspec
|
||||
|
||||
gem "sqlite3"
|
||||
|
||||
# mime-types 3+, required by mail, requires ruby 2.0+
|
||||
gem "mime-types", "< 3" if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2")
|
||||
|
||||
|
@ -1,15 +1,13 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
begin
|
||||
require 'mail'
|
||||
rescue LoadError
|
||||
require 'rubygems'
|
||||
require 'mail'
|
||||
end
|
||||
# Make sure we can require vendored gems
|
||||
$:.unshift(*Dir.glob(File.expand_path("../../vendor/gems/*/lib", __FILE__)))
|
||||
|
||||
require 'optparse'
|
||||
require "optparse"
|
||||
|
||||
options = {:smtp_ip => '127.0.0.1', :smtp_port => 1025}
|
||||
require "mail"
|
||||
|
||||
options = {:smtp_ip => "127.0.0.1", :smtp_port => 1025}
|
||||
|
||||
OptionParser.new do |parser|
|
||||
parser.banner = <<-BANNER.gsub /^ +/, ""
|
||||
@ -17,34 +15,34 @@ OptionParser.new do |parser|
|
||||
sendmail-like interface to forward mail to MailCatcher.
|
||||
BANNER
|
||||
|
||||
parser.on('--ip IP') do |ip|
|
||||
parser.on("--ip IP") do |ip|
|
||||
options[:smtp_ip] = ip
|
||||
end
|
||||
|
||||
parser.on('--smtp-ip IP', 'Set the ip address of the smtp server') do |ip|
|
||||
parser.on("--smtp-ip IP", "Set the ip address of the smtp server") do |ip|
|
||||
options[:smtp_ip] = ip
|
||||
end
|
||||
|
||||
parser.on('--smtp-port PORT', Integer, 'Set the port of the smtp server') do |port|
|
||||
parser.on("--smtp-port PORT", Integer, "Set the port of the smtp server") do |port|
|
||||
options[:smtp_port] = port
|
||||
end
|
||||
|
||||
parser.on('-f FROM', 'Set the sending address') do |from|
|
||||
parser.on("-f FROM", "Set the sending address") do |from|
|
||||
options[:from] = from
|
||||
end
|
||||
|
||||
parser.on('-oi', 'Ignored option -oi') do |ignored|
|
||||
parser.on("-oi", "Ignored option -oi") do |ignored|
|
||||
end
|
||||
parser.on('-t', 'Ignored option -t') do |ignored|
|
||||
parser.on("-t", "Ignored option -t") do |ignored|
|
||||
end
|
||||
parser.on('-q', 'Ignored option -q') do |ignored|
|
||||
parser.on("-q", "Ignored option -q") do |ignored|
|
||||
end
|
||||
|
||||
parser.on('-x', '--no-exit', 'Can\'t exit from the application') do
|
||||
parser.on("-x", "--no-exit", "Can't exit from the application") do
|
||||
options[:no_exit] = true
|
||||
end
|
||||
|
||||
parser.on('-h', '--help', 'Display this help information') do
|
||||
parser.on("-h", "--help", "Display this help information") do
|
||||
puts parser
|
||||
exit!
|
||||
end
|
||||
|
@ -1,5 +1,8 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
require 'mail_catcher'
|
||||
# Make sure we can require vendored gems
|
||||
$:.unshift(*Dir.glob(File.expand_path("../../vendor/gems/*/lib", __FILE__)))
|
||||
|
||||
require "mail_catcher"
|
||||
|
||||
MailCatcher.run!
|
||||
|
@ -1,32 +1,10 @@
|
||||
# Apparently rubygems won't activate these on its own, so here we go. Let's
|
||||
# repeat the invention of Bundler all over again.
|
||||
gem "eventmachine", "1.0.9.1"
|
||||
gem "mail", "~> 2.3"
|
||||
gem "rack", "~> 1.5"
|
||||
gem "sinatra", "~> 1.2"
|
||||
gem "sqlite3", "~> 1.3"
|
||||
gem "thin", "~> 1.5.0"
|
||||
gem "skinny", "~> 0.2.3"
|
||||
|
||||
require "logger"
|
||||
require "open3"
|
||||
require "optparse"
|
||||
require "rbconfig"
|
||||
|
||||
require "eventmachine"
|
||||
require "thin"
|
||||
|
||||
module EventMachine
|
||||
# Monkey patch fix for 10deb4
|
||||
# See https://github.com/eventmachine/eventmachine/issues/569
|
||||
def self.reactor_running?
|
||||
(@reactor_running || false)
|
||||
end
|
||||
end
|
||||
|
||||
require "mail_catcher/events"
|
||||
require "mail_catcher/mail"
|
||||
require "mail_catcher/smtp"
|
||||
require "mail_catcher/web"
|
||||
require "mail_catcher/http"
|
||||
require "mail_catcher/version"
|
||||
|
||||
module MailCatcher extend self
|
||||
@ -140,6 +118,12 @@ module MailCatcher extend self
|
||||
end
|
||||
end
|
||||
|
||||
def logger
|
||||
@logger ||= Logger.new(STDOUT).tap do |logger|
|
||||
logger.level = Logger::INFO
|
||||
end
|
||||
end
|
||||
|
||||
def run! options=nil
|
||||
# If we are passed options, fill in the blanks
|
||||
options &&= options.reverse_merge @@defaults
|
||||
@ -156,46 +140,56 @@ module MailCatcher extend self
|
||||
|
||||
puts "Starting MailCatcher"
|
||||
|
||||
Thin::Logging.silent = (ENV["MAILCATCHER_ENV"] != "development")
|
||||
# Start up an SMTP server
|
||||
@smtp_server = MailCatcher::SMTP.new(host: options[:smtp_ip], port: options[:smtp_port], logger: logger)
|
||||
@smtp_server.start
|
||||
|
||||
# One EventMachine loop...
|
||||
EventMachine.run do
|
||||
# Set up an SMTP server to run within EventMachine
|
||||
rescue_port options[:smtp_port] do
|
||||
EventMachine.start_server options[:smtp_ip], options[:smtp_port], Smtp
|
||||
puts "==> #{smtp_url}"
|
||||
end
|
||||
# Start up an HTTP server
|
||||
@http_server = MailCatcher::HTTP.new(host: options[:http_ip], port: options[:http_port], logger: logger)
|
||||
@http_server.start
|
||||
|
||||
# Let Thin set itself up inside our EventMachine loop
|
||||
# (Skinny/WebSockets just works on the inside)
|
||||
rescue_port options[:http_port] do
|
||||
Thin::Server.start(options[:http_ip], options[:http_port], Web)
|
||||
puts "==> #{http_url}"
|
||||
end
|
||||
# Set up some signal traps to gracefully quit
|
||||
#Signal.trap("INT") { quit! }
|
||||
#Signal.trap("TERM") { quit! }
|
||||
|
||||
# Open the web browser before detatching console
|
||||
if options[:browse]
|
||||
EventMachine.next_tick do
|
||||
browse http_url
|
||||
end
|
||||
end
|
||||
# Tell her about it
|
||||
puts "==> #{smtp_url}"
|
||||
puts "==> #{http_url}"
|
||||
|
||||
# Daemonize, if we should, but only after the servers have started.
|
||||
if options[:daemon]
|
||||
EventMachine.next_tick do
|
||||
if quittable?
|
||||
puts "*** MailCatcher runs as a daemon by default. Go to the web interface to quit."
|
||||
else
|
||||
puts "*** MailCatcher is now running as a daemon that cannot be quit."
|
||||
end
|
||||
Process.daemon
|
||||
end
|
||||
end
|
||||
# Open a browser if we were asked to
|
||||
if options[:browse]
|
||||
browse http_url
|
||||
end
|
||||
|
||||
# Daemonize, if we should, but only after the servers have started.
|
||||
if options[:daemon]
|
||||
if quittable?
|
||||
puts "*** MailCatcher runs as a daemon by default. Go to the web interface to quit."
|
||||
else
|
||||
puts "*** MailCatcher is now running as a daemon that cannot be quit."
|
||||
end
|
||||
|
||||
Process.daemon
|
||||
end
|
||||
|
||||
# Now wait for shutdown
|
||||
@smtp_server.join
|
||||
@http_server.join
|
||||
|
||||
logger.info "Bye! 👋"
|
||||
end
|
||||
|
||||
def quit!
|
||||
EventMachine.next_tick { EventMachine.stop_event_loop }
|
||||
unless quitting?
|
||||
@smtp_server.stop
|
||||
@http_server.stop
|
||||
|
||||
@quitting = true
|
||||
end
|
||||
end
|
||||
|
||||
def quitting?
|
||||
!!@quitting
|
||||
end
|
||||
|
||||
protected
|
||||
|
@ -1,7 +0,0 @@
|
||||
require "eventmachine"
|
||||
|
||||
module MailCatcher
|
||||
module Events
|
||||
MessageAdded = EventMachine::Channel.new
|
||||
end
|
||||
end
|
10
lib/mail_catcher/http.rb
Normal file
10
lib/mail_catcher/http.rb
Normal file
@ -0,0 +1,10 @@
|
||||
require "net/http/server"
|
||||
require "rack/handler/http"
|
||||
|
||||
require "mail_catcher/web"
|
||||
|
||||
class MailCatcher::HTTP < Net::HTTP::Server::Daemon
|
||||
def initialize(logger: nil, **options)
|
||||
super(handler: Rack::Handler::HTTP.new(MailCatcher::Web::Application.new), log: logger, **options)
|
||||
end
|
||||
end
|
@ -1,4 +1,3 @@
|
||||
require "eventmachine"
|
||||
require "json"
|
||||
require "mail"
|
||||
require "sqlite3"
|
||||
@ -37,11 +36,11 @@ module MailCatcher::Mail extend self
|
||||
end
|
||||
end
|
||||
|
||||
def add_message(message)
|
||||
def add_message(from:, to:, data:)
|
||||
@add_message_query ||= db.prepare("INSERT INTO message (sender, recipients, subject, source, type, size, created_at) VALUES (?, ?, ?, ?, ?, ?, datetime('now'))")
|
||||
|
||||
mail = Mail.new(message[:source])
|
||||
@add_message_query.execute(message[:sender], JSON.generate(message[:recipients]), mail.subject, message[:source], mail.mime_type || "text/plain", message[:source].length)
|
||||
mail = Mail.new(data)
|
||||
@add_message_query.execute(from, JSON.generate(to), mail.subject, data, mail.mime_type || "text/plain", data.length)
|
||||
message_id = db.last_insert_row_id
|
||||
parts = mail.all_parts
|
||||
parts = [mail] if parts.empty?
|
||||
@ -51,11 +50,6 @@ module MailCatcher::Mail extend self
|
||||
cid = part.cid if part.respond_to? :cid
|
||||
add_message_part(message_id, cid, part.mime_type || "text/plain", part.attachment? ? 1 : 0, part.filename, part.charset, body, body.length)
|
||||
end
|
||||
|
||||
EventMachine.next_tick do
|
||||
message = MailCatcher::Mail.message message_id
|
||||
MailCatcher::Events::MessageAdded.push message
|
||||
end
|
||||
end
|
||||
|
||||
def add_message_part(*args)
|
||||
|
9
lib/mail_catcher/pubsub.rb
Normal file
9
lib/mail_catcher/pubsub.rb
Normal file
@ -0,0 +1,9 @@
|
||||
module MailCatcher
|
||||
module Pubsub extend self
|
||||
def pub(name, data)
|
||||
end
|
||||
|
||||
def sub(*names)
|
||||
end
|
||||
end
|
||||
end
|
@ -1,61 +1,32 @@
|
||||
require "eventmachine"
|
||||
require "mail"
|
||||
require "midi-smtp-server"
|
||||
|
||||
require "mail_catcher/mail"
|
||||
|
||||
class MailCatcher::Smtp < EventMachine::Protocols::SmtpServer
|
||||
# We override EM's mail from processing to allow multiple mail-from commands
|
||||
# per [RFC 2821](http://tools.ietf.org/html/rfc2821#section-4.1.1.2)
|
||||
def process_mail_from sender
|
||||
if @state.include? :mail_from
|
||||
@state -= [:mail_from, :rcpt, :data]
|
||||
receive_reset
|
||||
end
|
||||
class MailCatcher::SMTP < MidiSmtpServer::Smtpd
|
||||
public :start
|
||||
|
||||
super
|
||||
def initialize(host:, port:, logger: nil, **options)
|
||||
super(port, host, 256, do_dns_reverse_lookup: false, logger: logger)
|
||||
end
|
||||
|
||||
def current_message
|
||||
@current_message ||= {}
|
||||
end
|
||||
def on_message_data_event(envelope:, message:, **context)
|
||||
MailCatcher::Mail.add_message(from: envelope[:from], to: envelope[:to], data: message[:data])
|
||||
|
||||
def receive_reset
|
||||
@current_message = nil
|
||||
true
|
||||
end
|
||||
|
||||
def receive_sender(sender)
|
||||
current_message[:sender] = sender
|
||||
true
|
||||
end
|
||||
|
||||
def receive_recipient(recipient)
|
||||
current_message[:recipients] ||= []
|
||||
current_message[:recipients] << recipient
|
||||
true
|
||||
end
|
||||
|
||||
def receive_data_chunk(lines)
|
||||
current_message[:source] ||= ""
|
||||
lines.each do |line|
|
||||
current_message[:source] << line << "\r\n"
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
def receive_message
|
||||
MailCatcher::Mail.add_message current_message
|
||||
puts "==> SMTP: Received message from '#{current_message[:sender]}' (#{current_message[:source].length} bytes)"
|
||||
true
|
||||
puts "==> SMTP: Received message from '#{envelope[:from]}' (#{message[:data].bytesize} bytes)"
|
||||
rescue
|
||||
puts "*** Error receiving message: #{current_message.inspect}"
|
||||
puts "*** Error receiving message"
|
||||
puts " MailCatcher v#{MailCatcher::VERSION}"
|
||||
puts " From: #{envelope[:from].inspect}"
|
||||
puts " To: #{envelope[:to].inspect}"
|
||||
puts " Data: #{message[:data].inspect}"
|
||||
puts " Exception: #{$!}"
|
||||
puts " Backtrace:"
|
||||
$!.backtrace.each do |line|
|
||||
puts " #{line}"
|
||||
end
|
||||
puts " Please submit this as an issue at http://github.com/sj26/mailcatcher/issues"
|
||||
false
|
||||
ensure
|
||||
@current_message = nil
|
||||
|
||||
raise MidiSmtpServer::Smtpd451Exception.new("Error receiving message, see MailCatcher log for details")
|
||||
end
|
||||
end
|
||||
|
@ -1,3 +1,3 @@
|
||||
module MailCatcher
|
||||
VERSION = "0.6.4"
|
||||
VERSION = "2.0.0.alpha"
|
||||
end
|
||||
|
@ -3,47 +3,22 @@ require "net/http"
|
||||
require "uri"
|
||||
|
||||
require "sinatra"
|
||||
require "skinny"
|
||||
|
||||
require "mail_catcher/events"
|
||||
require "mail_catcher/mail"
|
||||
|
||||
class Sinatra::Request
|
||||
include Skinny::Helpers
|
||||
end
|
||||
|
||||
module MailCatcher
|
||||
module Web
|
||||
class Application < Sinatra::Base
|
||||
set :development, ENV["MAILCATCHER_ENV"] == "development"
|
||||
set :root, File.expand_path("#{__FILE__}/../../../..")
|
||||
|
||||
if development?
|
||||
require "sprockets-helpers"
|
||||
|
||||
configure do
|
||||
require "mail_catcher/web/assets"
|
||||
Sprockets::Helpers.configure do |config|
|
||||
config.environment = Assets
|
||||
config.prefix = "/assets"
|
||||
config.digest = false
|
||||
config.public_path = public_folder
|
||||
config.debug = true
|
||||
end
|
||||
helpers do
|
||||
def javascript_tag(name)
|
||||
%{<script src="/assets/#{name}.js"></script>}
|
||||
end
|
||||
|
||||
helpers do
|
||||
include Sprockets::Helpers
|
||||
end
|
||||
else
|
||||
helpers do
|
||||
def javascript_tag(name)
|
||||
%{<script src="/assets/#{name}.js"></script>}
|
||||
end
|
||||
|
||||
def stylesheet_tag(name)
|
||||
%{<link rel="stylesheet" href="/assets/#{name}.css">}
|
||||
end
|
||||
def stylesheet_tag(name)
|
||||
%{<link rel="stylesheet" href="/assets/#{name}.css">}
|
||||
end
|
||||
end
|
||||
|
||||
@ -61,18 +36,8 @@ module MailCatcher
|
||||
end
|
||||
|
||||
get "/messages" do
|
||||
if request.websocket?
|
||||
request.websocket!(
|
||||
:on_start => proc do |websocket|
|
||||
subscription = Events::MessageAdded.subscribe { |message| websocket.send_message(JSON.generate(message)) }
|
||||
websocket.on_close do |websocket|
|
||||
Events::MessageAdded.unsubscribe subscription
|
||||
end
|
||||
end)
|
||||
else
|
||||
content_type :json
|
||||
JSON.generate(Mail.messages)
|
||||
end
|
||||
content_type :json
|
||||
JSON.generate(Mail.messages)
|
||||
end
|
||||
|
||||
delete "/messages" do
|
||||
|
@ -23,6 +23,7 @@ Gem::Specification.new do |s|
|
||||
"lib/**/*.rb",
|
||||
"public/**/*",
|
||||
"views/**/*",
|
||||
"vendor/gems/*/lib/**/*.rb",
|
||||
] - Dir["lib/mail_catcher/web/assets.rb"]
|
||||
s.require_paths = ["lib"]
|
||||
s.executables = ["mailcatcher", "catchmail"]
|
||||
@ -30,14 +31,6 @@ Gem::Specification.new do |s|
|
||||
|
||||
s.required_ruby_version = ">= 1.9.3"
|
||||
|
||||
s.add_dependency "eventmachine", "1.0.9.1"
|
||||
s.add_dependency "mail", "~> 2.3"
|
||||
s.add_dependency "rack", "~> 1.5"
|
||||
s.add_dependency "sinatra", "~> 1.2"
|
||||
s.add_dependency "sqlite3", "~> 1.3"
|
||||
s.add_dependency "thin", "~> 1.5.0"
|
||||
s.add_dependency "skinny", "~> 0.2.3"
|
||||
|
||||
s.add_development_dependency "coffee-script"
|
||||
s.add_development_dependency "compass"
|
||||
s.add_development_dependency "minitest", "~> 5.0"
|
||||
|
1
vendor/gems/gserver
vendored
Submodule
1
vendor/gems/gserver
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit f8d68f9065f486ac33f723b9a584f8b686f2bd16
|
1
vendor/gems/mail
vendored
Submodule
1
vendor/gems/mail
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit a217776355befa3d8191c4bd3c1fad54e0e27471
|
1
vendor/gems/midi-smtp-server
vendored
Submodule
1
vendor/gems/midi-smtp-server
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit d955cbb9cd4cc902dc5c7f0670e91f3ac50490b8
|
1
vendor/gems/mime-types
vendored
Submodule
1
vendor/gems/mime-types
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit aa499d1ea849584c7e2e63518f10289e76c00ec6
|
1
vendor/gems/mime-types-data
vendored
Submodule
1
vendor/gems/mime-types-data
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 4ea7e6b9f8d49dff6cd59703dc234a0b411175e2
|
133
vendor/gems/mustermann/lib/mustermann.rb
vendored
Normal file
133
vendor/gems/mustermann/lib/mustermann.rb
vendored
Normal file
@ -0,0 +1,133 @@
|
||||
require 'mustermann/pattern'
|
||||
require 'mustermann/composite'
|
||||
require 'mustermann/concat'
|
||||
require 'thread'
|
||||
|
||||
# Namespace and main entry point for the Mustermann library.
|
||||
#
|
||||
# Under normal circumstances the only external API entry point you should be using is {Mustermann.new}.
|
||||
module Mustermann
|
||||
# Type to use if no type is given.
|
||||
# @api private
|
||||
DEFAULT_TYPE = :sinatra
|
||||
|
||||
# Creates a new pattern based on input.
|
||||
#
|
||||
# * From {Mustermann::Pattern}: returns given pattern.
|
||||
# * From String: creates a pattern from the string, depending on type option (defaults to {Mustermann::Sinatra})
|
||||
# * From Regexp: creates a {Mustermann::Regular} pattern.
|
||||
# * From Symbol: creates a {Mustermann::Sinatra} pattern with a single named capture named after the input.
|
||||
# * From an Array or multiple inputs: creates a new pattern from each element, combines them to a {Mustermann::Composite}.
|
||||
# * From anything else: Will try to call to_pattern on it or raise a TypeError.
|
||||
#
|
||||
# Note that if the input is a {Mustermann::Pattern}, Regexp or Symbol, the type option is ignored and if to_pattern is
|
||||
# called on the object, the type will be handed on but might be ignored by the input object.
|
||||
#
|
||||
# If you want to enforce the pattern type, you should create them via their expected class.
|
||||
#
|
||||
# @example creating patterns
|
||||
# require 'mustermann'
|
||||
#
|
||||
# Mustermann.new("/:name") # => #<Mustermann::Sinatra:"/example">
|
||||
# Mustermann.new("/{name}", type: :template) # => #<Mustermann::Template:"/{name}">
|
||||
# Mustermann.new(/.*/) # => #<Mustermann::Regular:".*">
|
||||
# Mustermann.new(:name, capture: :word) # => #<Mustermann::Sinatra:":name">
|
||||
# Mustermann.new("/", "/*.jpg", type: :shell) # => #<Mustermann::Composite:(shell:"/" | shell:"/*.jpg")>
|
||||
#
|
||||
# @example using custom #to_pattern
|
||||
# require 'mustermann'
|
||||
#
|
||||
# class MyObject
|
||||
# def to_pattern(**options)
|
||||
# Mustermann.new("/:name", **options)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Mustermann.new(MyObject.new, type: :rails) # => #<Mustermann::Rails:"/:name">
|
||||
#
|
||||
# @example enforcing type
|
||||
# require 'mustermann/sinatra'
|
||||
#
|
||||
# Mustermann::Sinatra.new("/:name")
|
||||
#
|
||||
# @param [String, Pattern, Regexp, Symbol, #to_pattern, Array<String, Pattern, Regexp, Symbol, #to_pattern>]
|
||||
# input The representation of the pattern
|
||||
# @param [Hash] options The options hash
|
||||
# @return [Mustermann::Pattern] pattern corresponding to string.
|
||||
# @raise (see [])
|
||||
# @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, 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 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?
|
||||
pattern
|
||||
end
|
||||
end
|
||||
|
||||
@mutex ||= Mutex.new
|
||||
@types ||= {}
|
||||
|
||||
# Maps a type to its factory.
|
||||
#
|
||||
# @example
|
||||
# Mustermann[:sinatra] # => Mustermann::Sinatra
|
||||
#
|
||||
# @param [Symbol] name a pattern type identifier
|
||||
# @raise [ArgumentError] if the type is not supported
|
||||
# @return [Class, #new] pattern factory
|
||||
def self.[](name)
|
||||
return name if name.respond_to? :new
|
||||
@types.fetch(normalized = normalized_type(name)) do
|
||||
@mutex.synchronize do
|
||||
error = try_require "mustermann/#{normalized}"
|
||||
@types.fetch(normalized) { raise ArgumentError, "unsupported type %p#{" (#{error.message})" if error}" % name }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# @return [LoadError, nil]
|
||||
# @!visibility private
|
||||
def self.try_require(path)
|
||||
require(path)
|
||||
nil
|
||||
rescue LoadError => error
|
||||
raise(error) unless error.path == path
|
||||
error
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def self.register(name, type)
|
||||
@types[normalized_type(name)] = type
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def self.normalized_type(type)
|
||||
type.to_s.gsub('-', '_').downcase
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def self.extend_object(object)
|
||||
return super unless defined? ::Sinatra::Base and object.is_a? Class and object < ::Sinatra::Base
|
||||
require 'mustermann/extension'
|
||||
object.register Extension
|
||||
end
|
||||
end
|
||||
|
||||
# :nocov:
|
||||
begin
|
||||
require 'mustermann/visualizer' if defined?(Pry) or defined?(IRB)
|
||||
rescue LoadError => error
|
||||
raise error unless error.path == 'mustermann/visualizer'
|
||||
$stderr.puts(error.message) if caller_locations[1].absolute_path =~ %r{/lib/pry/|/irb/|^\((?:irb|pry)\)$}
|
||||
end
|
||||
# :nocov:
|
44
vendor/gems/mustermann/lib/mustermann/ast/boundaries.rb
vendored
Normal file
44
vendor/gems/mustermann/lib/mustermann/ast/boundaries.rb
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
require 'mustermann/ast/translator'
|
||||
|
||||
module Mustermann
|
||||
module AST
|
||||
# Make sure #start and #stop is set on every node and within its parents #start and #stop.
|
||||
# @!visibility private
|
||||
class Boundaries < Translator
|
||||
# @return [Mustermann::AST::Node] the ast passed as first argument
|
||||
# @!visibility private
|
||||
def self.set_boundaries(ast, string: nil, start: 0, stop: string.length)
|
||||
new.translate(ast, start, stop)
|
||||
ast
|
||||
end
|
||||
|
||||
translate(:node) do |start, stop|
|
||||
t.set_boundaries(node, start, stop)
|
||||
t(payload, node.start, node.stop)
|
||||
end
|
||||
|
||||
translate(:with_look_ahead) do |start, stop|
|
||||
t.set_boundaries(node, start, stop)
|
||||
t(head, node.start, node.stop)
|
||||
t(payload, node.start, node.stop)
|
||||
end
|
||||
|
||||
translate(Array) do |start, stop|
|
||||
each do |subnode|
|
||||
t(subnode, start, stop)
|
||||
start = subnode.stop
|
||||
end
|
||||
end
|
||||
|
||||
translate(Object) { |*| node }
|
||||
|
||||
# Checks that a node is within the given boundaries.
|
||||
# @!visibility private
|
||||
def set_boundaries(node, start, stop)
|
||||
node.start = start if node.start.nil? or node.start < start
|
||||
node.stop = node.start + node.min_size if node.stop.nil? or node.stop < node.start
|
||||
node.stop = stop if node.stop > stop
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
158
vendor/gems/mustermann/lib/mustermann/ast/compiler.rb
vendored
Normal file
158
vendor/gems/mustermann/lib/mustermann/ast/compiler.rb
vendored
Normal file
@ -0,0 +1,158 @@
|
||||
require 'mustermann/ast/translator'
|
||||
|
||||
module Mustermann
|
||||
# @see Mustermann::AST::Pattern
|
||||
module AST
|
||||
# Regexp compilation logic.
|
||||
# @!visibility private
|
||||
class Compiler < Translator
|
||||
raises CompileError
|
||||
|
||||
# Trivial compilations
|
||||
translate(Array) { |**o| map { |e| t(e, **o) }.join }
|
||||
translate(:node) { |**o| t(payload, **o) }
|
||||
translate(:separator) { |**o| Regexp.escape(payload) }
|
||||
translate(:optional) { |**o| "(?:%s)?" % t(payload, **o) }
|
||||
translate(:char) { |**o| t.encoded(payload, **o) }
|
||||
|
||||
translate :union do |**options|
|
||||
"(?:%s)" % payload.map { |e| "(?:%s)" % t(e, **options) }.join(?|)
|
||||
end
|
||||
|
||||
translate :expression do |greedy: true, **options|
|
||||
t(payload, allow_reserved: operator.allow_reserved, greedy: greedy && !operator.allow_reserved,
|
||||
parametric: operator.parametric, separator: operator.separator, **options)
|
||||
end
|
||||
|
||||
translate :with_look_ahead do |**options|
|
||||
lookahead = each_leaf.inject("") do |ahead, element|
|
||||
ahead + t(element, skip_optional: true, lookahead: ahead, greedy: false, no_captures: true, **options).to_s
|
||||
end
|
||||
lookahead << (at_end ? '$' : '/')
|
||||
t(head, lookahead: lookahead, **options) + t(payload, **options)
|
||||
end
|
||||
|
||||
# Capture compilation is complex. :(
|
||||
# @!visibility private
|
||||
class Capture < NodeTranslator
|
||||
register :capture
|
||||
|
||||
# @!visibility private
|
||||
def translate(**options)
|
||||
return pattern(options) if options[:no_captures]
|
||||
"(?<#{name}>#{translate(no_captures: true, **options)})"
|
||||
end
|
||||
|
||||
# @return [String] regexp without the named capture
|
||||
# @!visibility private
|
||||
def pattern(capture: nil, **options)
|
||||
case capture
|
||||
when Symbol then from_symbol(capture, **options)
|
||||
when Array then from_array(capture, **options)
|
||||
when Hash then from_hash(capture, **options)
|
||||
when String then from_string(capture, **options)
|
||||
when nil then from_nil(**options)
|
||||
else capture
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def qualified(string, greedy: true, **options) "#{string}#{qualifier || "+#{?? unless greedy}"}" end
|
||||
def with_lookahead(string, lookahead: nil, **options) lookahead ? "(?:(?!#{lookahead})#{string})" : string end
|
||||
def from_hash(hash, **options) pattern(capture: hash[name.to_sym], **options) end
|
||||
def from_array(array, **options) Regexp.union(*array.map { |e| pattern(capture: e, **options) }) end
|
||||
def from_symbol(symbol, **options) qualified(with_lookahead("[[:#{symbol}:]]", **options), **options) end
|
||||
def from_string(string, **options) Regexp.new(string.chars.map { |c| t.encoded(c, **options) }.join) end
|
||||
def from_nil(**options) qualified(with_lookahead(default(**options), **options), **options) end
|
||||
def default(**options) constraint || "[^/\\?#]" end
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
class Splat < Capture
|
||||
register :splat, :named_splat
|
||||
# splats are always non-greedy
|
||||
# @!visibility private
|
||||
def pattern(**options)
|
||||
constraint || ".*?"
|
||||
end
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
class Variable < Capture
|
||||
register :variable
|
||||
|
||||
# @!visibility private
|
||||
def translate(**options)
|
||||
return super(**options) if explode or not options[:parametric]
|
||||
# Remove this line after fixing broken compatibility between 2.1 and 2.2
|
||||
options.delete(:parametric) if options.has_key?(:parametric)
|
||||
parametric super(parametric: false, **options)
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def pattern(parametric: false, separator: nil, **options)
|
||||
register_param(parametric: parametric, separator: separator, **options)
|
||||
pattern = super(**options)
|
||||
pattern = parametric(pattern) if parametric
|
||||
pattern = "#{pattern}(?:#{Regexp.escape(separator)}#{pattern})*" if explode and separator
|
||||
pattern
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def parametric(string)
|
||||
"#{Regexp.escape(name)}(?:=#{string})?"
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def qualified(string, **options)
|
||||
prefix ? "#{string}{1,#{prefix}}" : super(string, **options)
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def default(allow_reserved: false, **options)
|
||||
allow_reserved ? '[\w\-\.~%\:/\?#\[\]@\!\$\&\'\(\)\*\+,;=]' : '[\w\-\.~%]'
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def register_param(parametric: false, split_params: nil, separator: nil, **options)
|
||||
return unless explode and split_params
|
||||
split_params[name] = { separator: separator, parametric: parametric }
|
||||
end
|
||||
end
|
||||
|
||||
# @return [String] Regular expression for matching the given character in all representations
|
||||
# @!visibility private
|
||||
def encoded(char, uri_decode: true, space_matches_plus: true, **options)
|
||||
return Regexp.escape(char) unless uri_decode
|
||||
encoded = escape(char, escape: /./)
|
||||
list = [escape(char), encoded.downcase, encoded.upcase].uniq.map { |c| Regexp.escape(c) }
|
||||
if char == " "
|
||||
list << encoded('+') if space_matches_plus
|
||||
list << " "
|
||||
end
|
||||
"(?:%s)" % list.join("|")
|
||||
end
|
||||
|
||||
# Compiles an AST to a regular expression.
|
||||
# @param [Mustermann::AST::Node] ast the tree
|
||||
# @return [Regexp] corresponding regular expression.
|
||||
#
|
||||
# @!visibility private
|
||||
def self.compile(ast, **options)
|
||||
new.compile(ast, **options)
|
||||
end
|
||||
|
||||
# Compiles an AST to a regular expression.
|
||||
# @param [Mustermann::AST::Node] ast the tree
|
||||
# @return [Regexp] corresponding regular expression.
|
||||
#
|
||||
# @!visibility private
|
||||
def compile(ast, except: nil, **options)
|
||||
except &&= "(?!#{translate(except, no_captures: true, **options)}\\Z)"
|
||||
Regexp.new("#{except}#{translate(ast, **options)}")
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Compiler
|
||||
end
|
||||
end
|
143
vendor/gems/mustermann/lib/mustermann/ast/expander.rb
vendored
Normal file
143
vendor/gems/mustermann/lib/mustermann/ast/expander.rb
vendored
Normal file
@ -0,0 +1,143 @@
|
||||
require 'mustermann/ast/translator'
|
||||
require 'mustermann/ast/compiler'
|
||||
|
||||
module Mustermann
|
||||
module AST
|
||||
# Looks at an AST, remembers the important bits of information to do an
|
||||
# ultra fast expansion.
|
||||
#
|
||||
# @!visibility private
|
||||
class Expander < Translator
|
||||
raises ExpandError
|
||||
|
||||
translate Array do |*args|
|
||||
inject(t.pattern) do |pattern, element|
|
||||
t.add_to(pattern, t(element, *args))
|
||||
end
|
||||
end
|
||||
|
||||
translate :capture do |**options|
|
||||
t.for_capture(node, **options)
|
||||
end
|
||||
|
||||
translate :named_splat, :splat do
|
||||
t.pattern + t.for_capture(node)
|
||||
end
|
||||
|
||||
translate :expression do
|
||||
t(payload, allow_reserved: operator.allow_reserved)
|
||||
end
|
||||
|
||||
translate :root, :group do
|
||||
t(payload)
|
||||
end
|
||||
|
||||
translate :char do
|
||||
t.pattern(t.escape(payload, also_escape: /[\/\?#\&\=%]/).gsub(?%, "%%"))
|
||||
end
|
||||
|
||||
translate :separator do
|
||||
t.pattern(payload.gsub(?%, "%%"))
|
||||
end
|
||||
|
||||
translate :with_look_ahead do
|
||||
t.add_to(t(head), t(payload))
|
||||
end
|
||||
|
||||
translate :optional do
|
||||
nested = t(payload)
|
||||
nested += t.pattern unless nested.any? { |n| n.first.empty? }
|
||||
nested
|
||||
end
|
||||
|
||||
translate :union do
|
||||
payload.map { |e| t(e) }.inject(:+)
|
||||
end
|
||||
|
||||
# helper method for captures
|
||||
# @!visibility private
|
||||
def for_capture(node, **options)
|
||||
name = node.name.to_sym
|
||||
pattern('%s', name, name => /(?!#{pattern_for(node, **options)})./)
|
||||
end
|
||||
|
||||
# maps sorted key list to sprintf patterns and filters
|
||||
# @!visibility private
|
||||
def mappings
|
||||
@mappings ||= {}
|
||||
end
|
||||
|
||||
# all the known keys
|
||||
# @!visibility private
|
||||
def keys
|
||||
@keys ||= []
|
||||
end
|
||||
|
||||
# add a tree for expansion
|
||||
# @!visibility private
|
||||
def add(ast)
|
||||
translate(ast).each do |keys, pattern, filter|
|
||||
self.keys.concat(keys).uniq!
|
||||
mappings[keys.sort] ||= [keys, pattern, filter]
|
||||
end
|
||||
end
|
||||
|
||||
# helper method for getting a capture's pattern.
|
||||
# @!visibility private
|
||||
def pattern_for(node, **options)
|
||||
Compiler.new.decorator_for(node).pattern(**options)
|
||||
end
|
||||
|
||||
# @see Mustermann::Pattern#expand
|
||||
# @!visibility private
|
||||
def expand(values)
|
||||
values = values.each_with_object({}){ |(key, value), new_hash|
|
||||
new_hash[value.instance_of?(Array) ? [key] * value.length : key] = value }
|
||||
keys, pattern, filters = mappings.fetch(values.keys.flatten.sort) { error_for(values) }
|
||||
filters.each { |key, filter| values[key] &&= escape(values[key], also_escape: filter) }
|
||||
pattern % (values[keys] || values.values_at(*keys))
|
||||
end
|
||||
|
||||
# @see Mustermann::Pattern#expandable?
|
||||
# @!visibility private
|
||||
def expandable?(values)
|
||||
values = values.keys if values.respond_to? :keys
|
||||
values = values.sort if values.respond_to? :sort
|
||||
mappings.include? values
|
||||
end
|
||||
|
||||
# @see Mustermann::Expander#with_rest
|
||||
# @!visibility private
|
||||
def expandable_keys(keys)
|
||||
mappings.keys.select { |k| (k - keys).empty? }.max_by(&:size) || keys
|
||||
end
|
||||
|
||||
# helper method for raising an error for unexpandable values
|
||||
# @!visibility private
|
||||
def error_for(values)
|
||||
expansions = mappings.keys.map(&:inspect).join(" or ")
|
||||
raise error_class, "cannot expand with keys %p, possible expansions: %s" % [values.keys.sort, expansions]
|
||||
end
|
||||
|
||||
# @see Mustermann::AST::Translator#expand
|
||||
# @!visibility private
|
||||
def escape(string, *args)
|
||||
# URI::Parser is pretty slow, let's not send every string to it, even if it's unnecessary
|
||||
string =~ /\A\w*\Z/ ? string : super
|
||||
end
|
||||
|
||||
# Turns a sprintf pattern into our secret internal data structure.
|
||||
# @!visibility private
|
||||
def pattern(string = "", *keys, **filters)
|
||||
[[keys, string, filters]]
|
||||
end
|
||||
|
||||
# Creates the product of two of our secret internal data structures.
|
||||
# @!visibility private
|
||||
def add_to(list, result)
|
||||
list << [[], ""] if list.empty?
|
||||
list.inject([]) { |l, (k1, p1, f1)| l + result.map { |k2, p2, f2| [k1+k2, p1+p2, **f1, **f2] } }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
222
vendor/gems/mustermann/lib/mustermann/ast/node.rb
vendored
Normal file
222
vendor/gems/mustermann/lib/mustermann/ast/node.rb
vendored
Normal file
@ -0,0 +1,222 @@
|
||||
module Mustermann
|
||||
# @see Mustermann::AST::Pattern
|
||||
module AST
|
||||
# @!visibility private
|
||||
class Node
|
||||
# @!visibility private
|
||||
attr_accessor :payload, :start, :stop
|
||||
|
||||
# @!visibility private
|
||||
# @param [Symbol] name of the node
|
||||
# @return [Class] factory for the node
|
||||
def self.[](name)
|
||||
@names ||= {}
|
||||
@names[name] ||= begin
|
||||
const_name = constant_name(name)
|
||||
Object.const_get(const_name) if Object.const_defined?(const_name)
|
||||
end
|
||||
end
|
||||
|
||||
# Turns a class name into a node identifier.
|
||||
# @!visibility private
|
||||
def self.type
|
||||
name[/[^:]+$/].split(/(?<=.)(?=[A-Z])/).map(&:downcase).join(?_).to_sym
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
# @param [Symbol] name of the node
|
||||
# @return [String] qualified name of factory for the node
|
||||
def self.constant_name(name)
|
||||
return self.name if name.to_sym == :node
|
||||
name = name.to_s.split(?_).map(&:capitalize).join
|
||||
"#{self.name}::#{name}"
|
||||
end
|
||||
|
||||
# Helper for creating a new instance and calling #parse on it.
|
||||
# @return [Mustermann::AST::Node]
|
||||
# @!visibility private
|
||||
def self.parse(*args, &block)
|
||||
new(*args).tap { |n| n.parse(&block) }
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def initialize(payload = nil, **options)
|
||||
options.each { |key, value| public_send("#{key}=", value) }
|
||||
self.payload = payload
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def is_a?(type)
|
||||
type = Node[type] if type.is_a? Symbol
|
||||
super(type)
|
||||
end
|
||||
|
||||
# Double dispatch helper for reading from the buffer into the payload.
|
||||
# @!visibility private
|
||||
def parse
|
||||
self.payload ||= []
|
||||
while element = yield
|
||||
payload << element
|
||||
end
|
||||
end
|
||||
|
||||
# Loop through all nodes that don't have child nodes.
|
||||
# @!visibility private
|
||||
def each_leaf(&block)
|
||||
return enum_for(__method__) unless block_given?
|
||||
called = false
|
||||
Array(payload).each do |entry|
|
||||
next unless entry.respond_to? :each_leaf
|
||||
entry.each_leaf(&block)
|
||||
called = true
|
||||
end
|
||||
yield(self) unless called
|
||||
end
|
||||
|
||||
# @return [Integer] length of the substring
|
||||
# @!visibility private
|
||||
def length
|
||||
stop - start if start and stop
|
||||
end
|
||||
|
||||
# @return [Integer] minimum size for a node
|
||||
# @!visibility private
|
||||
def min_size
|
||||
0
|
||||
end
|
||||
|
||||
# Turns a class name into a node identifier.
|
||||
# @!visibility private
|
||||
def type
|
||||
self.class.type
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
class Capture < Node
|
||||
# @see Mustermann::AST::Compiler::Capture#default
|
||||
# @!visibility private
|
||||
attr_accessor :constraint
|
||||
|
||||
# @see Mustermann::AST::Compiler::Capture#qualified
|
||||
# @!visibility private
|
||||
attr_accessor :qualifier
|
||||
|
||||
# @see Mustermann::AST::Pattern#map_param
|
||||
# @!visibility private
|
||||
attr_accessor :convert
|
||||
|
||||
# @see Mustermann::AST::Node#parse
|
||||
# @!visibility private
|
||||
def parse
|
||||
self.payload ||= ""
|
||||
super
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
alias_method :name, :payload
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
class Char < Node
|
||||
# @return [Integer] minimum size for a node
|
||||
# @!visibility private
|
||||
def min_size
|
||||
1
|
||||
end
|
||||
end
|
||||
|
||||
# AST node for template expressions.
|
||||
# @!visibility private
|
||||
class Expression < Node
|
||||
# @!visibility private
|
||||
attr_accessor :operator
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
class Composition < Node
|
||||
# @!visibility private
|
||||
def initialize(payload = nil, **options)
|
||||
super(Array(payload), **options)
|
||||
end
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
class Group < Composition
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
class Union < Composition
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
class Optional < Node
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
class Or < Node
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
class Root < Node
|
||||
# @!visibility private
|
||||
attr_accessor :pattern
|
||||
|
||||
# Will trigger transform.
|
||||
#
|
||||
# @see Mustermann::AST::Node.parse
|
||||
# @!visibility private
|
||||
def self.parse(string, &block)
|
||||
root = new
|
||||
root.pattern = string
|
||||
root.parse(&block)
|
||||
root
|
||||
end
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
class Separator < Node
|
||||
# @return [Integer] minimum size for a node
|
||||
# @!visibility private
|
||||
def min_size
|
||||
1
|
||||
end
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
class Splat < Capture
|
||||
# @see Mustermann::AST::Node::Capture#name
|
||||
# @!visibility private
|
||||
def name
|
||||
"splat"
|
||||
end
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
class NamedSplat < Splat
|
||||
# @see Mustermann::AST::Node::Capture#name
|
||||
# @!visibility private
|
||||
alias_method :name, :payload
|
||||
end
|
||||
|
||||
# AST node for template variables.
|
||||
# @!visibility private
|
||||
class Variable < Capture
|
||||
# @!visibility private
|
||||
attr_accessor :prefix, :explode
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
class WithLookAhead < Node
|
||||
# @!visibility private
|
||||
attr_accessor :head, :at_end
|
||||
|
||||
# @!visibility private
|
||||
def initialize(payload, at_end, **options)
|
||||
super(**options)
|
||||
self.head, *self.payload = Array(payload)
|
||||
self.at_end = at_end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
20
vendor/gems/mustermann/lib/mustermann/ast/param_scanner.rb
vendored
Normal file
20
vendor/gems/mustermann/lib/mustermann/ast/param_scanner.rb
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
require 'mustermann/ast/translator'
|
||||
|
||||
module Mustermann
|
||||
module AST
|
||||
# Scans an AST for param converters.
|
||||
# @!visibility private
|
||||
# @see Mustermann::AST::Pattern#to_templates
|
||||
class ParamScanner < Translator
|
||||
# @!visibility private
|
||||
def self.scan_params(ast)
|
||||
new.translate(ast)
|
||||
end
|
||||
|
||||
translate(:node) { t(payload) }
|
||||
translate(Array) { map { |e| t(e) }.inject(:merge) }
|
||||
translate(Object) { {} }
|
||||
translate(:capture) { convert ? { name => convert } : {} }
|
||||
end
|
||||
end
|
||||
end
|
233
vendor/gems/mustermann/lib/mustermann/ast/parser.rb
vendored
Normal file
233
vendor/gems/mustermann/lib/mustermann/ast/parser.rb
vendored
Normal file
@ -0,0 +1,233 @@
|
||||
require 'mustermann/ast/node'
|
||||
require 'forwardable'
|
||||
require 'strscan'
|
||||
|
||||
module Mustermann
|
||||
# @see Mustermann::AST::Pattern
|
||||
module AST
|
||||
# Simple, StringScanner based parser.
|
||||
# @!visibility private
|
||||
class Parser
|
||||
# @param [String] string to be parsed
|
||||
# @return [Mustermann::AST::Node] parse tree for string
|
||||
# @!visibility private
|
||||
def self.parse(string, **options)
|
||||
new(**options).parse(string)
|
||||
end
|
||||
|
||||
# Defines another grammar rule for first character.
|
||||
#
|
||||
# @see Mustermann::Rails
|
||||
# @see Mustermann::Sinatra
|
||||
# @see Mustermann::Template
|
||||
# @!visibility private
|
||||
def self.on(*chars, &block)
|
||||
chars.each do |char|
|
||||
define_method("read %p" % char, &block)
|
||||
end
|
||||
end
|
||||
|
||||
# Defines another grammar rule for a suffix.
|
||||
#
|
||||
# @see Mustermann::Sinatra
|
||||
# @!visibility private
|
||||
def self.suffix(pattern = /./, after: :node, &block)
|
||||
@suffix ||= []
|
||||
@suffix << [pattern, after, block] if block
|
||||
@suffix
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
attr_reader :buffer, :string, :pattern
|
||||
|
||||
extend Forwardable
|
||||
def_delegators :buffer, :eos?, :getch, :pos
|
||||
|
||||
# @!visibility private
|
||||
def initialize(pattern: nil, **options)
|
||||
@pattern = pattern
|
||||
end
|
||||
|
||||
# @param [String] string to be parsed
|
||||
# @return [Mustermann::AST::Node] parse tree for string
|
||||
# @!visibility private
|
||||
def parse(string)
|
||||
@string = string
|
||||
@buffer = ::StringScanner.new(string)
|
||||
node(:root, string) { read unless eos? }
|
||||
end
|
||||
|
||||
# @example
|
||||
# node(:char, 'x').compile =~ 'x' # => true
|
||||
#
|
||||
# @param [Symbol] type node type
|
||||
# @return [Mustermann::AST::Node]
|
||||
# @!visibility private
|
||||
def node(type, *args, &block)
|
||||
type = Node[type] unless type.respond_to? :new
|
||||
start = pos
|
||||
node = block ? type.parse(*args, &block) : type.new(*args)
|
||||
min_size(start, pos, node)
|
||||
end
|
||||
|
||||
# Create a node for a character we don't have an explicit rule for.
|
||||
#
|
||||
# @param [String] char the character
|
||||
# @return [Mustermann::AST::Node] the node
|
||||
# @!visibility private
|
||||
def default_node(char)
|
||||
char == ?/ ? node(:separator, char) : node(:char, char)
|
||||
end
|
||||
|
||||
# Reads the next element from the buffer.
|
||||
# @return [Mustermann::AST::Node] next element
|
||||
# @!visibility private
|
||||
def read
|
||||
start = pos
|
||||
char = getch
|
||||
method = "read %p" % char
|
||||
element= respond_to?(method) ? send(method, char) : default_node(char)
|
||||
min_size(start, pos, element)
|
||||
read_suffix(element)
|
||||
end
|
||||
|
||||
# sets start on node to start if it's not set to a lower value.
|
||||
# sets stop on node to stop if it's not set to a higher value.
|
||||
# @return [Mustermann::AST::Node] the node passed as third argument
|
||||
# @!visibility private
|
||||
def min_size(start, stop, node)
|
||||
stop ||= start
|
||||
start ||= stop
|
||||
node.start = start unless node.start and node.start < start
|
||||
node.stop = stop unless node.stop and node.stop > stop
|
||||
node
|
||||
end
|
||||
|
||||
# Checks for a potential suffix on the buffer.
|
||||
# @param [Mustermann::AST::Node] element node without suffix
|
||||
# @return [Mustermann::AST::Node] node with suffix
|
||||
# @!visibility private
|
||||
def read_suffix(element)
|
||||
self.class.suffix.inject(element) do |ele, (regexp, after, callback)|
|
||||
next ele unless ele.is_a?(after) and payload = scan(regexp)
|
||||
content = instance_exec(payload, ele, &callback)
|
||||
min_size(element.start, pos, content)
|
||||
end
|
||||
end
|
||||
|
||||
# Wrapper around {StringScanner#scan} that turns strings into escaped
|
||||
# regular expressions and returns a MatchData if the regexp has any
|
||||
# named captures.
|
||||
#
|
||||
# @param [Regexp, String] regexp
|
||||
# @see StringScanner#scan
|
||||
# @return [String, MatchData, nil]
|
||||
# @!visibility private
|
||||
def scan(regexp)
|
||||
regexp = Regexp.new(Regexp.escape(regexp)) unless regexp.is_a? Regexp
|
||||
string = buffer.scan(regexp)
|
||||
regexp.names.any? ? regexp.match(string) : string
|
||||
end
|
||||
|
||||
# Asserts a regular expression matches what's next on the buffer.
|
||||
# Will return corresponding MatchData if regexp includes named captures.
|
||||
#
|
||||
# @param [Regexp] regexp expected to match
|
||||
# @return [String, MatchData] the match
|
||||
# @raise [Mustermann::ParseError] if expectation wasn't met
|
||||
# @!visibility private
|
||||
def expect(regexp, char: nil, **options)
|
||||
scan(regexp) || unexpected(char, **options)
|
||||
end
|
||||
|
||||
# Allows to read a string inside brackets. It does not expect the string
|
||||
# to start with an opening bracket.
|
||||
#
|
||||
# @example
|
||||
# buffer.string = "fo<o>>ba<r>"
|
||||
# read_brackets(?<, ?>) # => "fo<o>"
|
||||
# buffer.rest # => "ba<r>"
|
||||
#
|
||||
# @!visibility private
|
||||
def read_brackets(open, close, char: nil, escape: ?\\, quote: false, **options)
|
||||
result = ""
|
||||
escape = false if escape.nil?
|
||||
while current = getch
|
||||
case current
|
||||
when close then return result
|
||||
when open then result << open << read_brackets(open, close) << close
|
||||
when escape then result << escape << getch
|
||||
else result << current
|
||||
end
|
||||
end
|
||||
unexpected(char, **options)
|
||||
end
|
||||
|
||||
|
||||
# Reads an argument string of the format arg1,args2,key:value
|
||||
#
|
||||
# @!visibility private
|
||||
def read_args(key_separator, close, separator: ?,, symbol_keys: true, **options)
|
||||
list, map = [], {}
|
||||
while buffer.peek(1) != close
|
||||
scan(separator)
|
||||
entries = read_list(close, separator, separator: key_separator, **options)
|
||||
case entries.size
|
||||
when 1 then list += entries
|
||||
when 2 then map[symbol_keys ? entries.first.to_sym : entries.first] = entries.last
|
||||
else unexpected(key_separator)
|
||||
end
|
||||
buffer.pos -= 1
|
||||
end
|
||||
expect(close)
|
||||
[list, map]
|
||||
end
|
||||
|
||||
# Reads a separated list with the ability to quote, escape and add spaces.
|
||||
#
|
||||
# @!visibility private
|
||||
def read_list(*close, separator: ?,, escape: ?\\, quotes: [?", ?'], ignore: " ", **options)
|
||||
result = []
|
||||
while current = getch
|
||||
element = result.empty? ? result : result.last
|
||||
case current
|
||||
when *close then return result
|
||||
when ignore then nil # do nothing
|
||||
when separator then result << ""
|
||||
when escape then element << getch
|
||||
when *quotes then element << read_escaped(current, escape: escape)
|
||||
else element << current
|
||||
end
|
||||
end
|
||||
unexpected(current, **options)
|
||||
end
|
||||
|
||||
# Read a string until a terminating character, ignoring escaped versions of said character.
|
||||
#
|
||||
# @!visibility private
|
||||
def read_escaped(close, escape: ?\\, **options)
|
||||
result = ""
|
||||
while current = getch
|
||||
case current
|
||||
when close then return result
|
||||
when escape then result << getch
|
||||
else result << current
|
||||
end
|
||||
end
|
||||
unexpected(current, **options)
|
||||
end
|
||||
|
||||
# Helper for raising an exception for an unexpected character.
|
||||
# Will read character from buffer if buffer is passed in.
|
||||
#
|
||||
# @param [String, nil] char the unexpected character
|
||||
# @raise [Mustermann::ParseError, Exception]
|
||||
# @!visibility private
|
||||
def unexpected(char = nil, exception: ParseError)
|
||||
char ||= getch
|
||||
char = "space" if char == " "
|
||||
raise exception, "unexpected #{char || "end of string"} while parsing #{string.inspect}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
136
vendor/gems/mustermann/lib/mustermann/ast/pattern.rb
vendored
Normal file
136
vendor/gems/mustermann/lib/mustermann/ast/pattern.rb
vendored
Normal file
@ -0,0 +1,136 @@
|
||||
require 'mustermann/ast/parser'
|
||||
require 'mustermann/ast/boundaries'
|
||||
require 'mustermann/ast/compiler'
|
||||
require 'mustermann/ast/transformer'
|
||||
require 'mustermann/ast/validation'
|
||||
require 'mustermann/ast/template_generator'
|
||||
require 'mustermann/ast/param_scanner'
|
||||
require 'mustermann/regexp_based'
|
||||
require 'mustermann/expander'
|
||||
require 'mustermann/equality_map'
|
||||
|
||||
module Mustermann
|
||||
# @see Mustermann::AST::Pattern
|
||||
module AST
|
||||
# Superclass for pattern styles that parse an AST from the string pattern.
|
||||
# @abstract
|
||||
class Pattern < Mustermann::RegexpBased
|
||||
supported_options :capture, :except, :greedy, :space_matches_plus
|
||||
|
||||
extend Forwardable, SingleForwardable
|
||||
single_delegate on: :parser, suffix: :parser
|
||||
instance_delegate %i[parser compiler transformer validation template_generator param_scanner boundaries] => 'self.class'
|
||||
instance_delegate parse: :parser, transform: :transformer, validate: :validation,
|
||||
generate_templates: :template_generator, scan_params: :param_scanner, set_boundaries: :boundaries
|
||||
|
||||
# @api private
|
||||
# @return [#parse] parser object for pattern
|
||||
# @!visibility private
|
||||
def self.parser
|
||||
return Parser if self == AST::Pattern
|
||||
const_set :Parser, Class.new(superclass.parser) unless const_defined? :Parser, false
|
||||
const_get :Parser
|
||||
end
|
||||
|
||||
# @api private
|
||||
# @return [#compile] compiler object for pattern
|
||||
# @!visibility private
|
||||
def self.compiler
|
||||
Compiler
|
||||
end
|
||||
|
||||
# @api private
|
||||
# @return [#set_boundaries] translator making sure start and stop is set on all nodes
|
||||
# @!visibility private
|
||||
def self.boundaries
|
||||
Boundaries
|
||||
end
|
||||
|
||||
# @api private
|
||||
# @return [#transform] transformer object for pattern
|
||||
# @!visibility private
|
||||
def self.transformer
|
||||
Transformer
|
||||
end
|
||||
|
||||
# @api private
|
||||
# @return [#validate] validation object for pattern
|
||||
# @!visibility private
|
||||
def self.validation
|
||||
Validation
|
||||
end
|
||||
|
||||
# @api private
|
||||
# @return [#generate_templates] generates URI templates for pattern
|
||||
# @!visibility private
|
||||
def self.template_generator
|
||||
TemplateGenerator
|
||||
end
|
||||
|
||||
# @api private
|
||||
# @return [#scan_params] param scanner for pattern
|
||||
# @!visibility private
|
||||
def self.param_scanner
|
||||
ParamScanner
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def compile(**options)
|
||||
options[:except] &&= parse options[:except]
|
||||
compiler.compile(to_ast, **options)
|
||||
rescue CompileError => error
|
||||
error.message << ": %p" % @string
|
||||
raise error
|
||||
end
|
||||
|
||||
# Internal AST representation of pattern.
|
||||
# @!visibility private
|
||||
def to_ast
|
||||
@ast_cache ||= EqualityMap.new
|
||||
@ast_cache.fetch(@string) do
|
||||
ast = parse(@string, pattern: self)
|
||||
ast &&= transform(ast)
|
||||
ast &&= set_boundaries(ast, string: @string)
|
||||
validate(ast)
|
||||
end
|
||||
end
|
||||
|
||||
# All AST-based pattern implementations support expanding.
|
||||
#
|
||||
# @example (see Mustermann::Pattern#expand)
|
||||
# @param (see Mustermann::Pattern#expand)
|
||||
# @return (see Mustermann::Pattern#expand)
|
||||
# @raise (see Mustermann::Pattern#expand)
|
||||
# @see Mustermann::Pattern#expand
|
||||
# @see Mustermann::Expander
|
||||
def expand(behavior = nil, values = {})
|
||||
@expander ||= Mustermann::Expander.new(self)
|
||||
@expander.expand(behavior, values)
|
||||
end
|
||||
|
||||
# All AST-based pattern implementations support generating templates.
|
||||
#
|
||||
# @example (see Mustermann::Pattern#to_templates)
|
||||
# @param (see Mustermann::Pattern#to_templates)
|
||||
# @return (see Mustermann::Pattern#to_templates)
|
||||
# @see Mustermann::Pattern#to_templates
|
||||
def to_templates
|
||||
@to_templates ||= generate_templates(to_ast)
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
# @see Mustermann::Pattern#map_param
|
||||
def map_param(key, value)
|
||||
return super unless param_converters.include? key
|
||||
param_converters[key][super]
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def param_converters
|
||||
@param_converters ||= scan_params(to_ast)
|
||||
end
|
||||
|
||||
private :compile, :parse, :transform, :validate, :generate_templates, :param_converters, :scan_params, :set_boundaries
|
||||
end
|
||||
end
|
||||
end
|
28
vendor/gems/mustermann/lib/mustermann/ast/template_generator.rb
vendored
Normal file
28
vendor/gems/mustermann/lib/mustermann/ast/template_generator.rb
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
require 'mustermann/ast/translator'
|
||||
|
||||
module Mustermann
|
||||
module AST
|
||||
# Turns an AST into an Array of URI templates representing the AST.
|
||||
# @!visibility private
|
||||
# @see Mustermann::AST::Pattern#to_templates
|
||||
class TemplateGenerator < Translator
|
||||
# @!visibility private
|
||||
def self.generate_templates(ast)
|
||||
new.translate(ast).uniq
|
||||
end
|
||||
|
||||
# translate(:expression) is not needed, since template patterns simply call to_s
|
||||
translate(:root, :group) { t(payload) || [""] }
|
||||
translate(:separator, :char) { t.escape(payload) }
|
||||
translate(:capture) { "{#{name}}" }
|
||||
translate(:optional) { [t(payload), ""] }
|
||||
translate(:named_splat, :splat) { "{+#{name}}" }
|
||||
translate(:with_look_ahead) { t([head, payload]) }
|
||||
translate(:union) { payload.flat_map { |e| t(e) } }
|
||||
|
||||
translate(Array) do
|
||||
map { |e| Array(t(e)) }.inject { |first, second| first.product(second).map(&:join) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
178
vendor/gems/mustermann/lib/mustermann/ast/transformer.rb
vendored
Normal file
178
vendor/gems/mustermann/lib/mustermann/ast/transformer.rb
vendored
Normal file
@ -0,0 +1,178 @@
|
||||
require 'mustermann/ast/translator'
|
||||
|
||||
module Mustermann
|
||||
module AST
|
||||
# Takes a tree, turns it into an even better tree.
|
||||
# @!visibility private
|
||||
class Transformer < Translator
|
||||
|
||||
# Transforms a tree.
|
||||
# @note might mutate handed in tree instead of creating a new one
|
||||
# @param [Mustermann::AST::Node] tree to be transformed
|
||||
# @return [Mustermann::AST::Node] transformed tree
|
||||
# @!visibility private
|
||||
def self.transform(tree)
|
||||
new.translate(tree)
|
||||
end
|
||||
|
||||
# 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
|
||||
payload.flatten! if payload.is_a?(Array)
|
||||
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
|
||||
# @!visibility private
|
||||
class ExpressionTransform < NodeTranslator
|
||||
register :expression
|
||||
|
||||
# @!visibility private
|
||||
Operator ||= Struct.new(:separator, :allow_reserved, :prefix, :parametric)
|
||||
|
||||
# Operators available for expressions.
|
||||
# @!visibility private
|
||||
OPERATORS ||= {
|
||||
nil => Operator.new(?,, false, false, false), ?+ => Operator.new(?,, true, false, false),
|
||||
?# => Operator.new(?,, true, ?#, false), ?. => Operator.new(?., false, ?., false),
|
||||
?/ => Operator.new(?/, false, ?/, false), ?; => Operator.new(?;, false, ?;, true),
|
||||
?? => Operator.new(?&, false, ??, true), ?& => Operator.new(?&, false, ?&, true)
|
||||
}
|
||||
|
||||
# Sets operator and inserts separators in between variables.
|
||||
# @!visibility private
|
||||
def translate
|
||||
self.operator = OPERATORS.fetch(operator) { raise CompileError, "#{operator} operator not supported" }
|
||||
separator = Node[:separator].new(operator.separator)
|
||||
prefix = Node[:separator].new(operator.prefix)
|
||||
self.payload = Array(payload.inject { |list, element| Array(list) << t(separator.dup) << t(element) })
|
||||
payload.unshift(prefix) if operator.prefix
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
# Inserts with_look_ahead nodes wherever appropriate
|
||||
# @!visibility private
|
||||
class ArrayTransform < NodeTranslator
|
||||
register Array
|
||||
|
||||
# the new array
|
||||
# @!visibility private
|
||||
def payload
|
||||
@payload ||= []
|
||||
end
|
||||
|
||||
# buffer for potential look ahead
|
||||
# @!visibility private
|
||||
def lookahead_buffer
|
||||
@lookahead_buffer ||= []
|
||||
end
|
||||
|
||||
# transform the array
|
||||
# @!visibility private
|
||||
def translate
|
||||
each { |e| track t(e) }
|
||||
payload.concat create_lookahead(lookahead_buffer, true)
|
||||
end
|
||||
|
||||
# handle a single element from the array
|
||||
# @!visibility private
|
||||
def track(element)
|
||||
return list_for(element) << element if lookahead_buffer.empty?
|
||||
return lookahead_buffer << element if lookahead? element
|
||||
|
||||
lookahead = lookahead_buffer.dup
|
||||
lookahead = create_lookahead(lookahead, false) if element.is_a? Node[:separator]
|
||||
lookahead_buffer.clear
|
||||
|
||||
payload.concat(lookahead) << element
|
||||
end
|
||||
|
||||
# turn look ahead buffer into look ahead node
|
||||
# @!visibility private
|
||||
def create_lookahead(elements, *args)
|
||||
return elements unless elements.size > 1
|
||||
[Node[:with_look_ahead].new(elements, *args, start: elements.first.start, stop: elements.last.stop)]
|
||||
end
|
||||
|
||||
# can the given element be used in a look-ahead?
|
||||
# @!visibility private
|
||||
def lookahead?(element, in_lookahead = false)
|
||||
case element
|
||||
when Node[:char] then in_lookahead
|
||||
when Node[:group] then lookahead_payload?(element.payload, in_lookahead)
|
||||
when Node[:optional] then lookahead?(element.payload, true) or expect_lookahead?(element.payload)
|
||||
end
|
||||
end
|
||||
|
||||
# does the list of elements look look-ahead-ish to you?
|
||||
# @!visibility private
|
||||
def lookahead_payload?(payload, in_lookahead)
|
||||
return unless payload[0..-2].all? { |e| lookahead?(e, in_lookahead) }
|
||||
expect_lookahead?(payload.last) or lookahead?(payload.last, in_lookahead)
|
||||
end
|
||||
|
||||
# can the current element deal with a look-ahead?
|
||||
# @!visibility private
|
||||
def expect_lookahead?(element)
|
||||
return element.class == Node[:capture] unless element.is_a? Node[:group]
|
||||
element.payload.all? { |e| expect_lookahead?(e) }
|
||||
end
|
||||
|
||||
# helper method for deciding where to put an element for now
|
||||
# @!visibility private
|
||||
def list_for(element)
|
||||
expect_lookahead?(element) ? lookahead_buffer : payload
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
125
vendor/gems/mustermann/lib/mustermann/ast/translator.rb
vendored
Normal file
125
vendor/gems/mustermann/lib/mustermann/ast/translator.rb
vendored
Normal file
@ -0,0 +1,125 @@
|
||||
require 'mustermann/ast/node'
|
||||
require 'mustermann/error'
|
||||
require 'delegate'
|
||||
|
||||
module Mustermann
|
||||
module AST
|
||||
# Implements translator pattern
|
||||
#
|
||||
# @abstract
|
||||
# @!visibility private
|
||||
class Translator
|
||||
# Encapsulates a single node translation
|
||||
# @!visibility private
|
||||
class NodeTranslator < DelegateClass(Node)
|
||||
# @param [Array<Symbol, Class>] types list of types to register for.
|
||||
# @!visibility private
|
||||
def self.register(*types)
|
||||
types.each do |type|
|
||||
type = Node.constant_name(type) if type.is_a? Symbol
|
||||
translator.dispatch_table[type.to_s] = self
|
||||
end
|
||||
end
|
||||
|
||||
# @param node [Mustermann::AST::Node, Object]
|
||||
# @param translator [Mustermann::AST::Translator]
|
||||
#
|
||||
# @!visibility private
|
||||
def initialize(node, translator)
|
||||
@translator = translator
|
||||
super(node)
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
attr_reader :translator
|
||||
|
||||
# shorthand for translating a nested object
|
||||
# @!visibility private
|
||||
def t(*args, &block)
|
||||
return translator unless args.any?
|
||||
translator.translate(*args, &block)
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
alias_method :node, :__getobj__
|
||||
end
|
||||
|
||||
# maps types to translations
|
||||
# @!visibility private
|
||||
def self.dispatch_table
|
||||
@dispatch_table ||= {}
|
||||
end
|
||||
|
||||
# some magic sauce so {NodeTranslator}s know whom to talk to for {#register}
|
||||
# @!visibility private
|
||||
def self.inherited(subclass)
|
||||
node_translator = Class.new(NodeTranslator)
|
||||
node_translator.define_singleton_method(:translator) { subclass }
|
||||
subclass.const_set(:NodeTranslator, node_translator)
|
||||
super
|
||||
end
|
||||
|
||||
# DSL-ish method for specifying the exception class to use.
|
||||
# @!visibility private
|
||||
def self.raises(error)
|
||||
define_method(:error_class) { error }
|
||||
end
|
||||
|
||||
# DSL method for defining single method translations.
|
||||
# @!visibility private
|
||||
def self.translate(*types, &block)
|
||||
Class.new(const_get(:NodeTranslator)) do
|
||||
register(*types)
|
||||
define_method(:translate, &block)
|
||||
end
|
||||
end
|
||||
|
||||
# Enables quick creation of a translator object.
|
||||
#
|
||||
# @example
|
||||
# require 'mustermann'
|
||||
# require 'mustermann/ast/translator'
|
||||
#
|
||||
# translator = Mustermann::AST::Translator.create do
|
||||
# translate(:node) { [type, *t(payload)].flatten.compact }
|
||||
# translate(Array) { map { |e| t(e) } }
|
||||
# translate(Object) { }
|
||||
# end
|
||||
#
|
||||
# ast = Mustermann.new('/:name').to_ast
|
||||
# translator.translate(ast) # => [:root, :separator, :capture]
|
||||
#
|
||||
# @!visibility private
|
||||
def self.create(&block)
|
||||
Class.new(self, &block).new
|
||||
end
|
||||
|
||||
raises Mustermann::Error
|
||||
|
||||
# @param [Mustermann::AST::Node, Object] node to translate
|
||||
# @return decorator encapsulating translation
|
||||
#
|
||||
# @!visibility private
|
||||
def decorator_for(node)
|
||||
factory = node.class.ancestors.inject(nil) { |d,a| d || self.class.dispatch_table[a.name] }
|
||||
raise error_class, "#{self.class}: Cannot translate #{node.class}" unless factory
|
||||
factory.new(node, self)
|
||||
end
|
||||
|
||||
# Start the translation dance for a (sub)tree.
|
||||
# @!visibility private
|
||||
def translate(node, *args, &block)
|
||||
result = decorator_for(node).translate(*args, &block)
|
||||
result = result.node while result.is_a? NodeTranslator
|
||||
result
|
||||
end
|
||||
|
||||
# @return [String] escaped character
|
||||
# @!visibility private
|
||||
def escape(char, parser: URI::DEFAULT_PARSER, escape: parser.regexp[:UNSAFE], also_escape: nil)
|
||||
escape = Regexp.union(also_escape, escape) if also_escape
|
||||
char =~ escape ? parser.escape(char, Regexp.union(*escape)) : char
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
44
vendor/gems/mustermann/lib/mustermann/ast/validation.rb
vendored
Normal file
44
vendor/gems/mustermann/lib/mustermann/ast/validation.rb
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
require 'mustermann/ast/translator'
|
||||
|
||||
module Mustermann
|
||||
module AST
|
||||
# Checks the AST for certain validations, like correct capture names.
|
||||
#
|
||||
# Internally a poor man's visitor (abusing translator to not have to implement a visitor).
|
||||
# @!visibility private
|
||||
class Validation < Translator
|
||||
# Runs validations.
|
||||
#
|
||||
# @param [Mustermann::AST::Node] ast to be validated
|
||||
# @return [Mustermann::AST::Node] the validated ast
|
||||
# @raise [Mustermann::AST::CompileError] if validation fails
|
||||
# @!visibility private
|
||||
def self.validate(ast)
|
||||
new.translate(ast)
|
||||
ast
|
||||
end
|
||||
|
||||
translate(Object, :splat) {}
|
||||
translate(:node) { t(payload) }
|
||||
translate(Array) { each { |p| t(p)} }
|
||||
translate(:capture) { t.check_name(name, forbidden: ['captures', 'splat'])}
|
||||
translate(:variable, :named_splat) { t.check_name(name, forbidden: 'captures')}
|
||||
|
||||
# @raise [Mustermann::CompileError] if name is not acceptable
|
||||
# @!visibility private
|
||||
def check_name(name, forbidden: [])
|
||||
raise CompileError, "capture name can't be empty" if name.nil? or name.empty?
|
||||
raise CompileError, "capture name must start with underscore or lower case letter" unless name =~ /^[a-z_]/
|
||||
raise CompileError, "capture name can't be #{name}" if Array(forbidden).include? name
|
||||
raise CompileError, "can't use the same capture name twice" if names.include? name
|
||||
names << name
|
||||
end
|
||||
|
||||
# @return [Array<String>] list of capture names in tree
|
||||
# @!visibility private
|
||||
def names
|
||||
@names ||= []
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
108
vendor/gems/mustermann/lib/mustermann/caster.rb
vendored
Normal file
108
vendor/gems/mustermann/lib/mustermann/caster.rb
vendored
Normal file
@ -0,0 +1,108 @@
|
||||
require 'delegate'
|
||||
|
||||
module Mustermann
|
||||
# Class for defining and running simple Hash transformations.
|
||||
#
|
||||
# @example
|
||||
# caster = Mustermann::Caster.new
|
||||
# caster.register(:foo) { |value| { bar: value.upcase } }
|
||||
# caster.cast(foo: "hello", baz: "world") # => { bar: "HELLO", baz: "world" }
|
||||
#
|
||||
# @see Mustermann::Expander#cast
|
||||
#
|
||||
# @!visibility private
|
||||
class Caster < DelegateClass(Array)
|
||||
# @param (see #register)
|
||||
# @!visibility private
|
||||
def initialize(*types, &block)
|
||||
super([])
|
||||
register(*types, &block)
|
||||
end
|
||||
|
||||
# @param [Array<Symbol, Regexp, #cast, #===>] types identifier for cast type (some need block)
|
||||
# @!visibility private
|
||||
def register(*types, &block)
|
||||
return if types.empty? and block.nil?
|
||||
types << Any.new(&block) if types.empty?
|
||||
types.each { |type| self << caster_for(type, &block) }
|
||||
end
|
||||
|
||||
# @param [Symbol, Regexp, #cast, #===] type identifier for cast type (some need block)
|
||||
# @return [#cast] specific cast operation
|
||||
# @!visibility private
|
||||
def caster_for(type, &block)
|
||||
case type
|
||||
when Symbol, Regexp then Key.new(type, &block)
|
||||
else type.respond_to?(:cast) ? type : Value.new(type, &block)
|
||||
end
|
||||
end
|
||||
|
||||
# Transforms a Hash.
|
||||
# @param [Hash] hash pre-transform Hash
|
||||
# @return [Hash] post-transform Hash
|
||||
# @!visibility private
|
||||
def cast(hash)
|
||||
return hash if empty?
|
||||
merge = {}
|
||||
hash.delete_if do |key, value|
|
||||
next unless casted = lazy.map { |e| e.cast(key, value) }.detect { |e| e }
|
||||
casted = { key => casted } unless casted.respond_to? :to_hash
|
||||
merge.update(casted.to_hash)
|
||||
end
|
||||
hash.update(merge)
|
||||
end
|
||||
|
||||
# Class for block based casts that are triggered for every key/value pair.
|
||||
# @!visibility private
|
||||
class Any
|
||||
# @!visibility private
|
||||
def initialize(&block)
|
||||
@block = block
|
||||
end
|
||||
|
||||
# @see Mustermann::Caster#cast
|
||||
# @!visibility private
|
||||
def cast(key, value)
|
||||
case @block.arity
|
||||
when 0 then @block.call
|
||||
when 1 then @block.call(value)
|
||||
else @block.call(key, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Class for block based casts that are triggered for key/value pairs with a matching value.
|
||||
# @!visibility private
|
||||
class Value < Any
|
||||
# @param [#===] type used for matching values
|
||||
# @!visibility private
|
||||
def initialize(type, &block)
|
||||
@type = type
|
||||
super(&block)
|
||||
end
|
||||
|
||||
# @see Mustermann::Caster#cast
|
||||
# @!visibility private
|
||||
def cast(key, value)
|
||||
super if @type === value
|
||||
end
|
||||
end
|
||||
|
||||
# Class for block based casts that are triggered for key/value pairs with a matching key.
|
||||
# @!visibility private
|
||||
class Key < Any
|
||||
# @param [#===] type used for matching keys
|
||||
# @!visibility private
|
||||
def initialize(type, &block)
|
||||
@type = type
|
||||
super(&block)
|
||||
end
|
||||
|
||||
# @see Mustermann::Caster#cast
|
||||
# @!visibility private
|
||||
def cast(key, value)
|
||||
super if @type === key
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
111
vendor/gems/mustermann/lib/mustermann/composite.rb
vendored
Normal file
111
vendor/gems/mustermann/lib/mustermann/composite.rb
vendored
Normal file
@ -0,0 +1,111 @@
|
||||
module Mustermann
|
||||
# Class for pattern objects composed of multiple patterns using binary logic.
|
||||
# @see Mustermann::Pattern#&
|
||||
# @see Mustermann::Pattern#|
|
||||
# @see Mustermann::Pattern#^
|
||||
class Composite < Pattern
|
||||
attr_reader :patterns, :operator
|
||||
supported_options :operator, :type
|
||||
|
||||
# @see Mustermann::Pattern.supported?
|
||||
def self.supported?(option, type: nil, **options)
|
||||
return true if super
|
||||
Mustermann[type || Mustermann::DEFAULT_TYPE].supported?(option, **options)
|
||||
end
|
||||
|
||||
# @return [Mustermann::Pattern] a new composite pattern
|
||||
def self.new(*patterns, **options)
|
||||
patterns = patterns.flatten
|
||||
case patterns.size
|
||||
when 0 then raise ArgumentError, 'cannot create empty composite pattern'
|
||||
when 1 then patterns.first
|
||||
else super(patterns, **options)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(patterns, operator: :|, **options)
|
||||
@operator = operator.to_sym
|
||||
@patterns = patterns.flat_map { |p| patterns_from(p, **options) }
|
||||
end
|
||||
|
||||
# @see Mustermann::Pattern#==
|
||||
def ==(pattern)
|
||||
patterns == patterns_from(pattern)
|
||||
end
|
||||
|
||||
# @see Mustermann::Pattern#eql?
|
||||
def eql?(pattern)
|
||||
patterns.eql? patterns_from(pattern)
|
||||
end
|
||||
|
||||
# @see Mustermann::Pattern#hash
|
||||
def hash
|
||||
patterns.hash | operator.hash
|
||||
end
|
||||
|
||||
# @see Mustermann::Pattern#===
|
||||
def ===(string)
|
||||
patterns.map { |p| p === string }.inject(operator)
|
||||
end
|
||||
|
||||
# @see Mustermann::Pattern#params
|
||||
def params(string)
|
||||
with_matching(string, :params)
|
||||
end
|
||||
|
||||
# @see Mustermann::Pattern#match
|
||||
def match(string)
|
||||
with_matching(string, :match)
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def respond_to_special?(method)
|
||||
return false unless operator == :|
|
||||
patterns.all? { |p| p.respond_to?(method) }
|
||||
end
|
||||
|
||||
# (see Mustermann::Pattern#expand)
|
||||
def expand(behavior = nil, values = {})
|
||||
raise NotImplementedError, 'expanding not supported' unless respond_to? :expand
|
||||
@expander ||= Mustermann::Expander.new(*patterns)
|
||||
@expander.expand(behavior, values)
|
||||
end
|
||||
|
||||
# (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
|
||||
end
|
||||
|
||||
# @return [String] the string representation of the pattern
|
||||
def to_s
|
||||
simple_inspect
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def inspect
|
||||
"#<%p:%s>" % [self.class, simple_inspect]
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def simple_inspect
|
||||
pattern_strings = patterns.map { |p| p.simple_inspect }
|
||||
"(#{pattern_strings.join(" #{operator} ")})"
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def with_matching(string, method)
|
||||
return unless self === string
|
||||
pattern = patterns.detect { |p| p === string }
|
||||
pattern.public_send(method, string) if pattern
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
def patterns_from(pattern, options = nil)
|
||||
return pattern.patterns if pattern.is_a? Composite and pattern.operator == self.operator
|
||||
[options ? Mustermann.new(pattern, **options) : pattern]
|
||||
end
|
||||
|
||||
private :with_matching, :patterns_from
|
||||
end
|
||||
end
|
124
vendor/gems/mustermann/lib/mustermann/concat.rb
vendored
Normal file
124
vendor/gems/mustermann/lib/mustermann/concat.rb
vendored
Normal file
@ -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
|
60
vendor/gems/mustermann/lib/mustermann/equality_map.rb
vendored
Normal file
60
vendor/gems/mustermann/lib/mustermann/equality_map.rb
vendored
Normal file
@ -0,0 +1,60 @@
|
||||
module Mustermann
|
||||
# A simple wrapper around ObjectSpace::WeakMap that allows matching keys by equality rather than identity.
|
||||
# Used for caching. Note that `fetch` is not guaranteed to return the object, even if it has not been
|
||||
# garbage collected yet, especially when used concurrently. Therefore, the block passed to `fetch` has to
|
||||
# be idempotent.
|
||||
#
|
||||
# @example
|
||||
# class ExpensiveComputation
|
||||
# @map = Mustermann::EqualityMap.new
|
||||
#
|
||||
# def self.new(*args)
|
||||
# @map.fetch(*args) { super }
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# @see #fetch
|
||||
class EqualityMap
|
||||
attr_reader :map
|
||||
|
||||
def self.new
|
||||
defined?(ObjectSpace::WeakMap) ? super : {}
|
||||
end
|
||||
|
||||
def initialize
|
||||
@keys = {}
|
||||
@map = ObjectSpace::WeakMap.new
|
||||
end
|
||||
|
||||
# @param [Array<#hash>] key for caching
|
||||
# @yield block that will be called to populate entry if missing (has to be idempotent)
|
||||
# @return value stored in map or result of block
|
||||
def fetch(*key)
|
||||
identity = @keys[key.hash]
|
||||
key = identity == key ? identity : key
|
||||
|
||||
# it is ok that this is not thread-safe, worst case it has double cost in
|
||||
# generating, object equality is not guaranteed anyways
|
||||
@map[key] ||= track(key, yield)
|
||||
end
|
||||
|
||||
# @param [#hash] key for identifying the object
|
||||
# @param [Object] object to be stored
|
||||
# @return [Object] same as the second parameter
|
||||
def track(key, object)
|
||||
ObjectSpace.define_finalizer(object, finalizer(key.hash))
|
||||
@keys[key.hash] = key
|
||||
object
|
||||
end
|
||||
|
||||
# Finalizer proc needs to be generated in different scope so it doesn't keep a reference to the object.
|
||||
#
|
||||
# @param [Fixnum] hash for key
|
||||
# @return [Proc] finalizer callback
|
||||
def finalizer(hash)
|
||||
proc { @keys.delete(hash) }
|
||||
end
|
||||
|
||||
private :track, :finalizer
|
||||
end
|
||||
end
|
6
vendor/gems/mustermann/lib/mustermann/error.rb
vendored
Normal file
6
vendor/gems/mustermann/lib/mustermann/error.rb
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
module Mustermann
|
||||
Error ||= Class.new(StandardError) # Raised if anything goes wrong while generating a {Pattern}.
|
||||
CompileError ||= Class.new(Error) # Raised if anything goes wrong while compiling a {Pattern}.
|
||||
ParseError ||= Class.new(Error) # Raised if anything goes wrong while parsing a {Pattern}.
|
||||
ExpandError ||= Class.new(Error) # Raised if anything goes wrong while expanding a {Pattern}.
|
||||
end
|
208
vendor/gems/mustermann/lib/mustermann/expander.rb
vendored
Normal file
208
vendor/gems/mustermann/lib/mustermann/expander.rb
vendored
Normal file
@ -0,0 +1,208 @@
|
||||
require 'mustermann/ast/expander'
|
||||
require 'mustermann/caster'
|
||||
require 'mustermann'
|
||||
|
||||
module Mustermann
|
||||
# Allows fine-grained control over pattern expansion.
|
||||
#
|
||||
# @example
|
||||
# expander = Mustermann::Expander.new(additional_values: :append)
|
||||
# expander << "/users/:user_id"
|
||||
# expander << "/pages/:page_id"
|
||||
#
|
||||
# expander.expand(page_id: 58, format: :html5) # => "/pages/58?format=html5"
|
||||
class Expander
|
||||
attr_reader :patterns, :additional_values, :caster
|
||||
|
||||
# @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, &block)
|
||||
unless additional_values == :raise or additional_values == :ignore or additional_values == :append
|
||||
raise ArgumentError, "Illegal value %p for additional_values" % additional_values
|
||||
end
|
||||
|
||||
@patterns = []
|
||||
@api_expander = AST::Expander.new
|
||||
@additional_values = additional_values
|
||||
@options = options
|
||||
@caster = Caster.new
|
||||
add(*patterns, &block)
|
||||
end
|
||||
|
||||
# Add patterns to expand.
|
||||
#
|
||||
# @example
|
||||
# expander = Mustermann::Expander.new
|
||||
# expander.add("/:a.jpg", "/:b.png")
|
||||
# expander.expand(a: "pony") # => "/pony.jpg"
|
||||
#
|
||||
# @param [Array<#to_str, Mustermann::Pattern>] patterns list of to add for expansion, Strings will be compiled to patterns.
|
||||
# @return [Mustermann::Expander] the expander
|
||||
def add(*patterns)
|
||||
patterns.each do |pattern|
|
||||
pattern = Mustermann.new(pattern, **@options)
|
||||
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
|
||||
end
|
||||
|
||||
alias_method :<<, :add
|
||||
|
||||
# Register a block as simple hash transformation that runs before expanding the pattern.
|
||||
# @return [Mustermann::Expander] the expander
|
||||
#
|
||||
# @overload cast
|
||||
# Register a block as simple hash transformation that runs before expanding the pattern for all entries.
|
||||
#
|
||||
# @example casting everything that implements to_param to param
|
||||
# expander.cast { |o| o.to_param if o.respond_to? :to_param }
|
||||
#
|
||||
# @yield every key/value pair
|
||||
# @yieldparam key [Symbol] omitted if block takes less than 2
|
||||
# @yieldparam value [Object] omitted if block takes no arguments
|
||||
# @yieldreturn [Hash{Symbol: Object}] will replace key/value pair with returned hash
|
||||
# @yieldreturn [nil, false] will keep key/value pair in hash
|
||||
# @yieldreturn [Object] will replace value with returned object
|
||||
#
|
||||
# @overload cast(*type_matchers)
|
||||
# Register a block as simple hash transformation that runs before expanding the pattern for certain entries.
|
||||
#
|
||||
# @example convert user to user_id
|
||||
# expander = Mustermann::Expander.new('/users/:user_id')
|
||||
# expand.cast(:user) { |user| { user_id: user.id } }
|
||||
#
|
||||
# expand.expand(user: User.current) # => "/users/42"
|
||||
#
|
||||
# @example convert user, page, image to user_id, page_id, image_id
|
||||
# expander = Mustermann::Expander.new('/users/:user_id', '/pages/:page_id', '/:image_id.jpg')
|
||||
# expand.cast(:user, :page, :image) { |key, value| { "#{key}_id".to_sym => value.id } }
|
||||
#
|
||||
# expand.expand(user: User.current) # => "/users/42"
|
||||
#
|
||||
# @example casting to multiple key/value pairs
|
||||
# expander = Mustermann::Expander.new('/users/:user_id/:image_id.:format')
|
||||
# expander.cast(:image) { |i| { user_id: i.owner.id, image_id: i.id, format: i.format } }
|
||||
#
|
||||
# expander.expander(image: User.current.avatar) # => "/users/42/avatar.jpg"
|
||||
#
|
||||
# @example casting all ActiveRecord objects to param
|
||||
# expander.cast(ActiveRecord::Base, &:to_param)
|
||||
#
|
||||
# @param [Array<Symbol, Regexp, #===>] type_matchers
|
||||
# To identify key/value pairs to match against.
|
||||
# Regexps and Symbols match against key, everything else matches against value.
|
||||
#
|
||||
# @yield every key/value pair
|
||||
# @yieldparam key [Symbol] omitted if block takes less than 2
|
||||
# @yieldparam value [Object] omitted if block takes no arguments
|
||||
# @yieldreturn [Hash{Symbol: Object}] will replace key/value pair with returned hash
|
||||
# @yieldreturn [nil, false] will keep key/value pair in hash
|
||||
# @yieldreturn [Object] will replace value with returned object
|
||||
#
|
||||
# @overload cast(*cast_objects)
|
||||
#
|
||||
# @param [Array<#cast>] cast_objects
|
||||
# Before expanding, will call #cast on these objects for each key/value pair.
|
||||
# Return value will be treated same as block return values described above.
|
||||
def cast(*types, &block)
|
||||
caster.register(*types, &block)
|
||||
self
|
||||
end
|
||||
|
||||
# @example Expanding a pattern
|
||||
# pattern = Mustermann::Expander.new('/:name', '/:name.:ext')
|
||||
# pattern.expand(name: 'hello') # => "/hello"
|
||||
# pattern.expand(name: 'hello', ext: 'png') # => "/hello.png"
|
||||
#
|
||||
# @example Handling additional values
|
||||
# pattern = Mustermann::Expander.new('/:name', '/:name.:ext')
|
||||
# pattern.expand(:ignore, name: 'hello', ext: 'png', scale: '2x') # => "/hello.png"
|
||||
# pattern.expand(:append, name: 'hello', ext: 'png', scale: '2x') # => "/hello.png?scale=2x"
|
||||
# pattern.expand(:raise, name: 'hello', ext: 'png', scale: '2x') # raises Mustermann::ExpandError
|
||||
#
|
||||
# @example Setting additional values behavior for the expander object
|
||||
# pattern = Mustermann::Expander.new('/:name', '/:name.:ext', additional_values: :append)
|
||||
# pattern.expand(name: 'hello', ext: 'png', scale: '2x') # => "/hello.png?scale=2x"
|
||||
#
|
||||
# @param [Symbol] behavior
|
||||
# What to do with additional key/value pairs not present in the values hash.
|
||||
# Possible options: :raise, :ignore, :append.
|
||||
#
|
||||
# @param [Hash{Symbol: #to_s, Array<#to_s>}] values
|
||||
# Values to use for expansion.
|
||||
#
|
||||
# @return [String] expanded string
|
||||
# @raise [NotImplementedError] raised if expand is not supported.
|
||||
# @raise [Mustermann::ExpandError] raised if a value is missing or unknown
|
||||
def expand(behavior = nil, values = {})
|
||||
behavior, values = nil, behavior if behavior.is_a? Hash
|
||||
values = map_values(values)
|
||||
|
||||
case behavior || additional_values
|
||||
when :raise then @api_expander.expand(values)
|
||||
when :ignore then with_rest(values) { |uri, rest| uri }
|
||||
when :append then with_rest(values) { |uri, rest| append(uri, rest) }
|
||||
else raise ArgumentError, "unknown behavior %p" % behavior
|
||||
end
|
||||
end
|
||||
|
||||
# @see Object#==
|
||||
def ==(other)
|
||||
return false unless other.class == self.class
|
||||
other.patterns == patterns and other.additional_values == additional_values
|
||||
end
|
||||
|
||||
# @see Object#eql?
|
||||
def eql?(other)
|
||||
return false unless other.class == self.class
|
||||
other.patterns.eql? patterns and other.additional_values.eql? additional_values
|
||||
end
|
||||
|
||||
# @see Object#hash
|
||||
def hash
|
||||
patterns.hash + additional_values.hash
|
||||
end
|
||||
|
||||
def expandable?(values)
|
||||
return false unless values
|
||||
expandable, _ = split_values(map_values(values))
|
||||
@api_expander.expandable? expandable
|
||||
end
|
||||
|
||||
def with_rest(values)
|
||||
expandable, non_expandable = split_values(values)
|
||||
yield expand(:raise, slice(values, expandable)), slice(values, non_expandable)
|
||||
end
|
||||
|
||||
def split_values(values)
|
||||
expandable = @api_expander.expandable_keys(values.keys)
|
||||
non_expandable = values.keys - expandable
|
||||
[expandable, non_expandable]
|
||||
end
|
||||
|
||||
def slice(hash, keys)
|
||||
Hash[keys.map { |k| [k, hash[k]] }]
|
||||
end
|
||||
|
||||
def append(uri, values)
|
||||
return uri unless values and values.any?
|
||||
entries = values.map { |pair| pair.map { |e| @api_expander.escape(e, also_escape: /[\/\?#\&\=%]/) }.join(?=) }
|
||||
"#{ uri }#{ uri[??]??&:?? }#{ entries.join(?&) }"
|
||||
end
|
||||
|
||||
def map_values(values)
|
||||
values = values.dup
|
||||
@api_expander.keys.each { |key| values[key] ||= values.delete(key.to_s) if values.include? key.to_s }
|
||||
caster.cast(values).delete_if { |k, v| v.nil? }
|
||||
end
|
||||
|
||||
private :with_rest, :slice, :append, :caster, :map_values, :split_values
|
||||
end
|
||||
end
|
49
vendor/gems/mustermann/lib/mustermann/extension.rb
vendored
Normal file
49
vendor/gems/mustermann/lib/mustermann/extension.rb
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
require 'sinatra/version'
|
||||
fail "no need to load the Mustermann extension for #{::Sinatra::VERSION}" if ::Sinatra::VERSION >= '2.0.0'
|
||||
|
||||
require 'mustermann'
|
||||
|
||||
module Mustermann
|
||||
# Sinatra 1.x extension switching default pattern parsing over to Mustermann.
|
||||
#
|
||||
# @example With classic Sinatra application
|
||||
# require 'sinatra'
|
||||
# require 'mustermann'
|
||||
#
|
||||
# register Mustermann
|
||||
# get('/:id', capture: /\d+/) { ... }
|
||||
#
|
||||
# @example With modular Sinatra application
|
||||
# require 'sinatra/base'
|
||||
# require 'mustermann'
|
||||
#
|
||||
# class MyApp < Sinatra::Base
|
||||
# register Mustermann
|
||||
# get('/:id', capture: /\d+/) { ... }
|
||||
# end
|
||||
#
|
||||
# @see file:README.md#Sinatra_Integration "Sinatra Integration" in the README
|
||||
module Extension
|
||||
def compile!(verb, path, block, except: nil, capture: nil, pattern: { }, **options)
|
||||
if path.respond_to? :to_str
|
||||
pattern[:except] = except if except
|
||||
pattern[:capture] = capture if capture
|
||||
|
||||
if settings.respond_to? :pattern and settings.pattern?
|
||||
pattern.merge! settings.pattern do |key, local, global|
|
||||
next local unless local.is_a? Hash
|
||||
next global.merge(local) if global.is_a? Hash
|
||||
Hash.new(global).merge! local
|
||||
end
|
||||
end
|
||||
|
||||
path = Mustermann.new(path, **pattern)
|
||||
condition { params.merge! path.params(captures: Array(params[:captures]), offset: -1) }
|
||||
end
|
||||
|
||||
super(verb, path, block, options)
|
||||
end
|
||||
|
||||
private :compile!
|
||||
end
|
||||
end
|
76
vendor/gems/mustermann/lib/mustermann/identity.rb
vendored
Normal file
76
vendor/gems/mustermann/lib/mustermann/identity.rb
vendored
Normal file
@ -0,0 +1,76 @@
|
||||
require 'mustermann'
|
||||
require 'mustermann/pattern'
|
||||
require 'mustermann/ast/node'
|
||||
|
||||
module Mustermann
|
||||
# Matches strings that are identical to the pattern.
|
||||
#
|
||||
# @example
|
||||
# Mustermann.new('/:foo', type: :identity) === '/bar' # => false
|
||||
#
|
||||
# @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#===)
|
||||
# @return (see Mustermann::Pattern#===)
|
||||
# @see (see Mustermann::Pattern#===)
|
||||
def ===(string)
|
||||
unescape(string) == @string
|
||||
end
|
||||
|
||||
# @param (see Mustermann::Pattern#peek_size)
|
||||
# @return (see Mustermann::Pattern#peek_size)
|
||||
# @see (see Mustermann::Pattern#peek_size)
|
||||
def peek_size(string)
|
||||
return unless unescape(string).start_with? @string
|
||||
return @string.size if string.start_with? @string # optimization
|
||||
@string.each_char.with_index.inject(0) do |count, (char, index)|
|
||||
char_size = 1
|
||||
escaped = @@uri.escape(char, /./)
|
||||
char_size = escaped.size if string[index, escaped.size].downcase == escaped.downcase
|
||||
count + char_size
|
||||
end
|
||||
end
|
||||
|
||||
# URI templates support generating templates (the logic is quite complex, though).
|
||||
#
|
||||
# @example (see Mustermann::Pattern#to_templates)
|
||||
# @param (see Mustermann::Pattern#to_templates)
|
||||
# @return (see Mustermann::Pattern#to_templates)
|
||||
# @see Mustermann::Pattern#to_templates
|
||||
def to_templates
|
||||
[@@uri.escape(to_s)]
|
||||
end
|
||||
|
||||
# Generates an AST so it's compatible with {Mustermann::AST::Pattern}.
|
||||
# Not used internally by {Mustermann::Identity}.
|
||||
# @!visibility private
|
||||
def to_ast
|
||||
payload = @string.each_char.with_index.map { |c, i| AST::Node[c == ?/ ? :separator : :char].new(c, start: i, stop: i+1) }
|
||||
AST::Node[:root].new(payload, pattern: @string, start: 0, stop: @string.length)
|
||||
end
|
||||
|
||||
# Identity patterns support expanding.
|
||||
#
|
||||
# This implementation does not use {Mustermann::Expander} internally to save memory and
|
||||
# compilation time.
|
||||
#
|
||||
# @example (see Mustermann::Pattern#expand)
|
||||
# @param (see Mustermann::Pattern#expand)
|
||||
# @return (see Mustermann::Pattern#expand)
|
||||
# @raise (see Mustermann::Pattern#expand)
|
||||
# @see Mustermann::Pattern#expand
|
||||
# @see Mustermann::Expander
|
||||
def expand(behavior = nil, values = {})
|
||||
return to_s if values.empty? or behavior == :ignore
|
||||
raise ExpandError, "cannot expand with keys %p" % values.keys.sort if behavior == :raise
|
||||
raise ArgumentError, "unknown behavior %p" % behavior if behavior != :append
|
||||
params = values.map { |key, value| @@uri.escape(key.to_s) + "=" + @@uri.escape(value.to_s, /[^\w]/) }
|
||||
separator = @string.include?(??) ? ?& : ??
|
||||
@string + separator + params.join(?&)
|
||||
end
|
||||
end
|
||||
end
|
94
vendor/gems/mustermann/lib/mustermann/mapper.rb
vendored
Normal file
94
vendor/gems/mustermann/lib/mustermann/mapper.rb
vendored
Normal file
@ -0,0 +1,94 @@
|
||||
require 'mustermann'
|
||||
require 'mustermann/expander'
|
||||
|
||||
module Mustermann
|
||||
# A mapper allows mapping one string to another based on pattern parsing and expanding.
|
||||
#
|
||||
# @example
|
||||
# require 'mustermann/mapper'
|
||||
# mapper = Mustermann::Mapper.new("/:foo" => "/:foo.html")
|
||||
# mapper['/example'] # => "/example.html"
|
||||
class Mapper
|
||||
# Creates a new mapper.
|
||||
#
|
||||
# @overload initialize(**options)
|
||||
# @param options [Hash] options The options hash
|
||||
# @yield block for generating mappings as a hash
|
||||
# @yieldreturn [Hash] see {#update}
|
||||
#
|
||||
# @example
|
||||
# require 'mustermann/mapper'
|
||||
# Mustermann::Mapper.new(type: :rails) {{
|
||||
# "/:foo" => ["/:foo.html", "/:foo.:format"]
|
||||
# }}
|
||||
#
|
||||
# @overload initialize(**options)
|
||||
# @param options [Hash] options The options hash
|
||||
# @yield block for generating mappings as a hash
|
||||
# @yieldparam mapper [Mustermann::Mapper] the mapper instance
|
||||
#
|
||||
# @example
|
||||
# require 'mustermann/mapper'
|
||||
# Mustermann::Mapper.new(type: :rails) do |mapper|
|
||||
# mapper["/:foo"] = ["/:foo.html", "/:foo.:format"]
|
||||
# end
|
||||
#
|
||||
# @overload initialize(map = {}, **options)
|
||||
# @param map [Hash] see {#update}
|
||||
# @param [Hash] options The options hash
|
||||
#
|
||||
# @example map before options
|
||||
# require 'mustermann/mapper'
|
||||
# Mustermann::Mapper.new("/:foo" => "/:foo.html", type: :rails)
|
||||
#
|
||||
# @example map after options
|
||||
# require 'mustermann/mapper'
|
||||
# Mustermann::Mapper.new(type: :rails, "/:foo" => "/:foo.html")
|
||||
def initialize(map = {}, additional_values: :ignore, **options, &block)
|
||||
@map = []
|
||||
@options = options
|
||||
@additional_values = additional_values
|
||||
block.arity == 0 ? update(yield) : yield(self) if block
|
||||
update(map) if map
|
||||
end
|
||||
|
||||
# Add multiple mappings.
|
||||
#
|
||||
# @param map [Hash{String, Pattern: String, Pattern, Arry<String, Pattern>, Expander}] the mapping
|
||||
def update(map)
|
||||
map.to_h.each_pair do |input, output|
|
||||
input = Mustermann.new(input, **@options)
|
||||
output = Expander.new(*output, additional_values: @additional_values, **@options) unless output.is_a? Expander
|
||||
@map << [input, output]
|
||||
end
|
||||
end
|
||||
|
||||
# @return [Hash{Patttern: Expander}] Hash version of the mapper.
|
||||
def to_h
|
||||
Hash[@map]
|
||||
end
|
||||
|
||||
# Convert a string according to mappings. You can pass in additional params.
|
||||
#
|
||||
# @example mapping with and without additional parameters
|
||||
# mapper = Mustermann::Mapper.new("/:example" => "(/:prefix)?/:example.html")
|
||||
#
|
||||
def convert(input, values = {})
|
||||
@map.inject(input) do |current, (pattern, expander)|
|
||||
params = pattern.params(current)
|
||||
params &&= Hash[values.merge(params).map { |k,v| [k.to_s, v] }]
|
||||
expander.expandable?(params) ? expander.expand(params) : current
|
||||
end
|
||||
end
|
||||
|
||||
# Add a single mapping.
|
||||
#
|
||||
# @param key [String, Pattern] format of the input string
|
||||
# @param value [String, Pattern, Arry<String, Pattern>, Expander] format of the output string
|
||||
def []=(key, value)
|
||||
update key => value
|
||||
end
|
||||
|
||||
alias_method :[], :convert
|
||||
end
|
||||
end
|
397
vendor/gems/mustermann/lib/mustermann/pattern.rb
vendored
Normal file
397
vendor/gems/mustermann/lib/mustermann/pattern.rb
vendored
Normal file
@ -0,0 +1,397 @@
|
||||
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<Symbol>] 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<Symbol>] 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") # => #<MatchData "/Frank" name:"Frank">
|
||||
#
|
||||
# @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<Hash, Integer>, 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<Integer>}] 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<String>] 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<String>}, 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<String>] 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
|
49
vendor/gems/mustermann/lib/mustermann/pattern_cache.rb
vendored
Normal file
49
vendor/gems/mustermann/lib/mustermann/pattern_cache.rb
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
require 'set'
|
||||
require 'thread'
|
||||
require 'mustermann'
|
||||
|
||||
module Mustermann
|
||||
# A simple, persistent cache for creating repositories.
|
||||
#
|
||||
# @example
|
||||
# require 'mustermann/pattern_cache'
|
||||
# cache = Mustermann::PatternCache.new
|
||||
#
|
||||
# # use this instead of Mustermann.new
|
||||
# pattern = cache.create_pattern("/:name", type: :rails)
|
||||
#
|
||||
# @note
|
||||
# {Mustermann::Pattern.new} (which is used by {Mustermann.new}) will reuse instances that have
|
||||
# not yet been garbage collected. You only need an extra cache if you do not keep a reference to
|
||||
# the patterns around.
|
||||
#
|
||||
# @api private
|
||||
class PatternCache
|
||||
# @param [Hash] pattern_options default options used for {#create_pattern}
|
||||
def initialize(**pattern_options)
|
||||
@cached = Set.new
|
||||
@mutex = Mutex.new
|
||||
@pattern_options = pattern_options
|
||||
end
|
||||
|
||||
# @param (see Mustermann.new)
|
||||
# @return (see Mustermann.new)
|
||||
# @raise (see Mustermann.new)
|
||||
# @see Mustermann.new
|
||||
def create_pattern(string, **pattern_options)
|
||||
pattern = Mustermann.new(string, **pattern_options, **@pattern_options)
|
||||
@mutex.synchronize { @cached.add(pattern) } unless @cached.include? pattern
|
||||
pattern
|
||||
end
|
||||
|
||||
# Removes all pattern instances from the cache.
|
||||
def clear
|
||||
@mutex.synchronize { @cached.clear }
|
||||
end
|
||||
|
||||
# @return [Integer] number of currently cached patterns
|
||||
def size
|
||||
@mutex.synchronize { @cached.size }
|
||||
end
|
||||
end
|
||||
end
|
1
vendor/gems/mustermann/lib/mustermann/regexp.rb
vendored
Normal file
1
vendor/gems/mustermann/lib/mustermann/regexp.rb
vendored
Normal file
@ -0,0 +1 @@
|
||||
require 'mustermann/regular'
|
47
vendor/gems/mustermann/lib/mustermann/regexp_based.rb
vendored
Normal file
47
vendor/gems/mustermann/lib/mustermann/regexp_based.rb
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
require 'mustermann/pattern'
|
||||
require 'forwardable'
|
||||
|
||||
module Mustermann
|
||||
# Superclass for patterns that internally compile to a regular expression.
|
||||
# @see Mustermann::Pattern
|
||||
# @abstract
|
||||
class RegexpBased < Pattern
|
||||
# @return [Regexp] regular expression equivalent to the pattern.
|
||||
attr_reader :regexp
|
||||
alias_method :to_regexp, :regexp
|
||||
|
||||
# @param (see Mustermann::Pattern#initialize)
|
||||
# @return (see Mustermann::Pattern#initialize)
|
||||
# @see (see Mustermann::Pattern#initialize)
|
||||
def initialize(string, **options)
|
||||
super
|
||||
regexp = compile(**options)
|
||||
@peek_regexp = /\A#{regexp}/
|
||||
@regexp = /\A#{regexp}\Z/
|
||||
end
|
||||
|
||||
# @param (see Mustermann::Pattern#peek_size)
|
||||
# @return (see Mustermann::Pattern#peek_size)
|
||||
# @see (see Mustermann::Pattern#peek_size)
|
||||
def peek_size(string)
|
||||
return unless match = peek_match(string)
|
||||
match.to_s.size
|
||||
end
|
||||
|
||||
# @param (see Mustermann::Pattern#peek_match)
|
||||
# @return (see Mustermann::Pattern#peek_match)
|
||||
# @see (see Mustermann::Pattern#peek_match)
|
||||
def peek_match(string)
|
||||
@peek_regexp.match(string)
|
||||
end
|
||||
|
||||
extend Forwardable
|
||||
def_delegators :regexp, :===, :=~, :match, :names, :named_captures
|
||||
|
||||
def compile(**options)
|
||||
raise NotImplementedError, 'subclass responsibility'
|
||||
end
|
||||
|
||||
private :compile
|
||||
end
|
||||
end
|
44
vendor/gems/mustermann/lib/mustermann/regular.rb
vendored
Normal file
44
vendor/gems/mustermann/lib/mustermann/regular.rb
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
require 'mustermann'
|
||||
require 'mustermann/regexp_based'
|
||||
require 'strscan'
|
||||
|
||||
module Mustermann
|
||||
# Regexp pattern implementation.
|
||||
#
|
||||
# @example
|
||||
# Mustermann.new('/.*', type: :regexp) === '/bar' # => true
|
||||
#
|
||||
# @see Mustermann::Pattern
|
||||
# @see file:README.md#simple Syntax description in the README
|
||||
class Regular < RegexpBased
|
||||
include Concat::Native
|
||||
register :regexp, :regular
|
||||
supported_options :check_anchors
|
||||
|
||||
# @param (see Mustermann::Pattern#initialize)
|
||||
# @return (see Mustermann::Pattern#initialize)
|
||||
# @see (see Mustermann::Pattern#initialize)
|
||||
def initialize(string, check_anchors: true, **options)
|
||||
string = $1 if string.to_s =~ /\A\(\?\-mix\:(.*)\)\Z/ && string.inspect == "/#$1/"
|
||||
@check_anchors = check_anchors
|
||||
super(string, **options)
|
||||
end
|
||||
|
||||
def compile(**options)
|
||||
if @check_anchors
|
||||
scanner = ::StringScanner.new(@string)
|
||||
check_anchors(scanner) until scanner.eos?
|
||||
end
|
||||
|
||||
/#{@string}/
|
||||
end
|
||||
|
||||
def check_anchors(scanner)
|
||||
return scanner.scan_until(/\]/) if scanner.scan(/\[/)
|
||||
return scanner.scan(/\\?./) unless illegal = scanner.scan(/\\[AzZ]|[\^\$]/)
|
||||
raise CompileError, "regular expression should not contain %s: %p" % [illegal.to_s, @string]
|
||||
end
|
||||
|
||||
private :compile, :check_anchors
|
||||
end
|
||||
end
|
48
vendor/gems/mustermann/lib/mustermann/simple_match.rb
vendored
Normal file
48
vendor/gems/mustermann/lib/mustermann/simple_match.rb
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
module Mustermann
|
||||
# Fakes MatchData for patterns that do not support capturing.
|
||||
# @see http://ruby-doc.org/core-2.0/MatchData.html MatchData
|
||||
class SimpleMatch
|
||||
# @api private
|
||||
def initialize(string = "", names: [], captures: [])
|
||||
@string = string.dup
|
||||
@names = names
|
||||
@captures = captures
|
||||
end
|
||||
|
||||
# @return [String] the string that was matched against
|
||||
def to_s
|
||||
@string.dup
|
||||
end
|
||||
|
||||
# @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)
|
||||
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
|
||||
def inspect
|
||||
"#<%p %p>" % [self.class, @string]
|
||||
end
|
||||
end
|
||||
end
|
87
vendor/gems/mustermann/lib/mustermann/sinatra.rb
vendored
Normal file
87
vendor/gems/mustermann/lib/mustermann/sinatra.rb
vendored
Normal file
@ -0,0 +1,87 @@
|
||||
require 'mustermann'
|
||||
require 'mustermann/identity'
|
||||
require 'mustermann/ast/pattern'
|
||||
require 'mustermann/sinatra/parser'
|
||||
require 'mustermann/sinatra/safe_renderer'
|
||||
require 'mustermann/sinatra/try_convert'
|
||||
|
||||
module Mustermann
|
||||
# Sinatra 2.0 style pattern implementation.
|
||||
#
|
||||
# @example
|
||||
# Mustermann.new('/:foo') === '/bar' # => true
|
||||
#
|
||||
# @see Mustermann::Pattern
|
||||
# @see file:README.md#sinatra Syntax description in the README
|
||||
class Sinatra < AST::Pattern
|
||||
include Concat::Native
|
||||
register :sinatra
|
||||
|
||||
# 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)
|
||||
TryConvert.convert(input, **options)
|
||||
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(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
|
45
vendor/gems/mustermann/lib/mustermann/sinatra/parser.rb
vendored
Normal file
45
vendor/gems/mustermann/lib/mustermann/sinatra/parser.rb
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
module Mustermann
|
||||
class Sinatra < AST::Pattern
|
||||
# Sinatra syntax definition.
|
||||
# @!visibility private
|
||||
class Parser < AST::Parser
|
||||
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(?() { |c| node(:group) { read unless scan(?)) } }
|
||||
on(?|) { |c| node(:or) }
|
||||
|
||||
on ?{ do |char|
|
||||
current_pos = buffer.pos
|
||||
type = scan(?+) ? :named_splat : :capture
|
||||
name = expect(/[\w\.]+/)
|
||||
if type == :capture && scan(?|)
|
||||
buffer.pos = current_pos
|
||||
capture = proc do
|
||||
start = pos
|
||||
match = expect(/(?<capture>[^\|}]+)/)
|
||||
node(:capture, match[:capture], start: start)
|
||||
end
|
||||
grouped_captures = node(:group, [capture[]]) do
|
||||
if scan(?|)
|
||||
[min_size(pos - 1, pos, node(:or)), capture[]]
|
||||
end
|
||||
end
|
||||
grouped_captures if expect(?})
|
||||
else
|
||||
type = :splat if type == :named_splat and name == 'splat'
|
||||
expect(?})
|
||||
node(type, name)
|
||||
end
|
||||
end
|
||||
|
||||
suffix ?? do |char, element|
|
||||
node(:optional, element)
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Parser
|
||||
end
|
||||
end
|
26
vendor/gems/mustermann/lib/mustermann/sinatra/safe_renderer.rb
vendored
Normal file
26
vendor/gems/mustermann/lib/mustermann/sinatra/safe_renderer.rb
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
module Mustermann
|
||||
class Sinatra < AST::Pattern
|
||||
# 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
|
||||
|
||||
private_constant :SafeRenderer
|
||||
end
|
||||
end
|
48
vendor/gems/mustermann/lib/mustermann/sinatra/try_convert.rb
vendored
Normal file
48
vendor/gems/mustermann/lib/mustermann/sinatra/try_convert.rb
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
module Mustermann
|
||||
class Sinatra < AST::Pattern
|
||||
# Tries to translate objects to Sinatra patterns.
|
||||
# @!visibility private
|
||||
class TryConvert < AST::Translator
|
||||
# @return [Mustermann::Sinatra, nil]
|
||||
# @!visibility private
|
||||
def self.convert(input, **options)
|
||||
new(options).translate(input)
|
||||
end
|
||||
|
||||
# Expected options for the resulting pattern.
|
||||
# @!visibility private
|
||||
attr_reader :options
|
||||
|
||||
# @!visibility private
|
||||
def initialize(options)
|
||||
@options = options
|
||||
end
|
||||
|
||||
# @return [Mustermann::Sinatra]
|
||||
# @!visibility private
|
||||
def new(input, escape = false)
|
||||
input = Mustermann::Sinatra.escape(input) if escape
|
||||
Mustermann::Sinatra.new(input, **options)
|
||||
end
|
||||
|
||||
# @return [true, false] whether or not expected pattern should have uri_decode option set
|
||||
# @!visibility private
|
||||
def uri_decode
|
||||
options.fetch(:uri_decode, true)
|
||||
end
|
||||
|
||||
translate(Object) { nil }
|
||||
translate(String) { t.new(self, true) }
|
||||
|
||||
translate(Identity) { t.new(self, true) if uri_decode == t.uri_decode }
|
||||
translate(Sinatra) { node if options == t.options }
|
||||
|
||||
translate AST::Pattern do
|
||||
next unless options == t.options
|
||||
t.new(SafeRenderer.translate(to_ast)) rescue nil
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :TryConvert
|
||||
end
|
||||
end
|
50
vendor/gems/mustermann/lib/mustermann/to_pattern.rb
vendored
Normal file
50
vendor/gems/mustermann/lib/mustermann/to_pattern.rb
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
require 'mustermann'
|
||||
|
||||
module Mustermann
|
||||
# Mixin for adding {#to_pattern} ducktyping to objects.
|
||||
#
|
||||
# @example
|
||||
# require 'mustermann/to_pattern'
|
||||
#
|
||||
# class Foo
|
||||
# include Mustermann::ToPattern
|
||||
#
|
||||
# def to_s
|
||||
# ":foo/:bar"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Foo.new.to_pattern # => #<Mustermann::Sinatra:":foo/:bar">
|
||||
#
|
||||
# By default included into String, Symbol, Regexp, Array and {Mustermann::Pattern}.
|
||||
module ToPattern
|
||||
PRIMITIVES = [String, Symbol, Array, Regexp, Mustermann::Pattern]
|
||||
private_constant :PRIMITIVES
|
||||
|
||||
# Converts the object into a {Mustermann::Pattern}.
|
||||
#
|
||||
# @example converting a string
|
||||
# ":name.png".to_pattern # => #<Mustermann::Sinatra:":name.png">
|
||||
#
|
||||
# @example converting a string with options
|
||||
# "/*path".to_pattern(type: :rails) # => #<Mustermann::Rails:"/*path">
|
||||
#
|
||||
# @example converting a regexp
|
||||
# /.*/.to_pattern # => #<Mustermann::Regular:".*">
|
||||
#
|
||||
# @example converting a pattern
|
||||
# Mustermann.new("foo").to_pattern # => #<Mustermann::Sinatra:"foo">
|
||||
#
|
||||
# @param [Hash] options The options hash.
|
||||
# @return [Mustermann::Pattern] pattern corresponding to object.
|
||||
def to_pattern(**options)
|
||||
input = self if PRIMITIVES.any? { |p| self.is_a? p }
|
||||
input ||= __getobj__ if respond_to?(:__getobj__)
|
||||
Mustermann.new(input || to_s, **options)
|
||||
end
|
||||
|
||||
PRIMITIVES.each do |klass|
|
||||
append_features(klass)
|
||||
end
|
||||
end
|
||||
end
|
3
vendor/gems/mustermann/lib/mustermann/version.rb
vendored
Normal file
3
vendor/gems/mustermann/lib/mustermann/version.rb
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
module Mustermann
|
||||
VERSION ||= '0.4.0'
|
||||
end
|
1
vendor/gems/net-http-server
vendored
Submodule
1
vendor/gems/net-http-server
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit d2c76b6e9742ec0832c599b4115ca58893d655dd
|
1
vendor/gems/parslet
vendored
Submodule
1
vendor/gems/parslet
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 9d59363f14b0292676187287bee25d7b5ad06aa0
|
1
vendor/gems/rack
vendored
Submodule
1
vendor/gems/rack
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 25a549883b85fb33970b4a1530a365c0c9e51f95
|
1
vendor/gems/rack-protection
vendored
Submodule
1
vendor/gems/rack-protection
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit f8d8ee2eb72f707d17b07c48bc55a8906978ea5d
|
1
vendor/gems/sinatra
vendored
Submodule
1
vendor/gems/sinatra
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 03c03d7e169954185b008253f8bf2f9068198ca3
|
1
vendor/gems/tilt
vendored
Submodule
1
vendor/gems/tilt
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 2eb0e355745010af8c2a52338722d0961a1114fb
|
1
vendor/gems/websocket-ruby
vendored
Submodule
1
vendor/gems/websocket-ruby
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 79a67622eeb1f0c11a31910e4bb1d3b292569898
|
Loading…
Reference in New Issue
Block a user