mirror of
https://github.com/moparisthebest/mailcatcher
synced 2024-08-13 17:03:45 -04: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
|
gemspec
|
||||||
|
|
||||||
|
gem "sqlite3"
|
||||||
|
|
||||||
# mime-types 3+, required by mail, requires ruby 2.0+
|
# mime-types 3+, required by mail, requires ruby 2.0+
|
||||||
gem "mime-types", "< 3" if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2")
|
gem "mime-types", "< 3" if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2")
|
||||||
|
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
#!/usr/bin/env ruby
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
begin
|
# Make sure we can require vendored gems
|
||||||
require 'mail'
|
$:.unshift(*Dir.glob(File.expand_path("../../vendor/gems/*/lib", __FILE__)))
|
||||||
rescue LoadError
|
|
||||||
require 'rubygems'
|
|
||||||
require 'mail'
|
|
||||||
end
|
|
||||||
|
|
||||||
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|
|
OptionParser.new do |parser|
|
||||||
parser.banner = <<-BANNER.gsub /^ +/, ""
|
parser.banner = <<-BANNER.gsub /^ +/, ""
|
||||||
@ -17,34 +15,34 @@ OptionParser.new do |parser|
|
|||||||
sendmail-like interface to forward mail to MailCatcher.
|
sendmail-like interface to forward mail to MailCatcher.
|
||||||
BANNER
|
BANNER
|
||||||
|
|
||||||
parser.on('--ip IP') do |ip|
|
parser.on("--ip IP") do |ip|
|
||||||
options[:smtp_ip] = ip
|
options[:smtp_ip] = ip
|
||||||
end
|
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
|
options[:smtp_ip] = ip
|
||||||
end
|
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
|
options[:smtp_port] = port
|
||||||
end
|
end
|
||||||
|
|
||||||
parser.on('-f FROM', 'Set the sending address') do |from|
|
parser.on("-f FROM", "Set the sending address") do |from|
|
||||||
options[:from] = from
|
options[:from] = from
|
||||||
end
|
end
|
||||||
|
|
||||||
parser.on('-oi', 'Ignored option -oi') do |ignored|
|
parser.on("-oi", "Ignored option -oi") do |ignored|
|
||||||
end
|
end
|
||||||
parser.on('-t', 'Ignored option -t') do |ignored|
|
parser.on("-t", "Ignored option -t") do |ignored|
|
||||||
end
|
end
|
||||||
parser.on('-q', 'Ignored option -q') do |ignored|
|
parser.on("-q", "Ignored option -q") do |ignored|
|
||||||
end
|
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
|
options[:no_exit] = true
|
||||||
end
|
end
|
||||||
|
|
||||||
parser.on('-h', '--help', 'Display this help information') do
|
parser.on("-h", "--help", "Display this help information") do
|
||||||
puts parser
|
puts parser
|
||||||
exit!
|
exit!
|
||||||
end
|
end
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
#!/usr/bin/env ruby
|
#!/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!
|
MailCatcher.run!
|
||||||
|
@ -1,32 +1,10 @@
|
|||||||
# Apparently rubygems won't activate these on its own, so here we go. Let's
|
require "logger"
|
||||||
# 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 "open3"
|
require "open3"
|
||||||
require "optparse"
|
require "optparse"
|
||||||
require "rbconfig"
|
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/smtp"
|
||||||
require "mail_catcher/web"
|
require "mail_catcher/http"
|
||||||
require "mail_catcher/version"
|
require "mail_catcher/version"
|
||||||
|
|
||||||
module MailCatcher extend self
|
module MailCatcher extend self
|
||||||
@ -140,6 +118,12 @@ module MailCatcher extend self
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def logger
|
||||||
|
@logger ||= Logger.new(STDOUT).tap do |logger|
|
||||||
|
logger.level = Logger::INFO
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def run! options=nil
|
def run! options=nil
|
||||||
# If we are passed options, fill in the blanks
|
# If we are passed options, fill in the blanks
|
||||||
options &&= options.reverse_merge @@defaults
|
options &&= options.reverse_merge @@defaults
|
||||||
@ -156,46 +140,56 @@ module MailCatcher extend self
|
|||||||
|
|
||||||
puts "Starting MailCatcher"
|
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...
|
# Start up an HTTP server
|
||||||
EventMachine.run do
|
@http_server = MailCatcher::HTTP.new(host: options[:http_ip], port: options[:http_port], logger: logger)
|
||||||
# Set up an SMTP server to run within EventMachine
|
@http_server.start
|
||||||
rescue_port options[:smtp_port] do
|
|
||||||
EventMachine.start_server options[:smtp_ip], options[:smtp_port], Smtp
|
# Set up some signal traps to gracefully quit
|
||||||
|
#Signal.trap("INT") { quit! }
|
||||||
|
#Signal.trap("TERM") { quit! }
|
||||||
|
|
||||||
|
# Tell her about it
|
||||||
puts "==> #{smtp_url}"
|
puts "==> #{smtp_url}"
|
||||||
end
|
|
||||||
|
|
||||||
# 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}"
|
puts "==> #{http_url}"
|
||||||
end
|
|
||||||
|
|
||||||
# Open the web browser before detatching console
|
# Open a browser if we were asked to
|
||||||
if options[:browse]
|
if options[:browse]
|
||||||
EventMachine.next_tick do
|
|
||||||
browse http_url
|
browse http_url
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
# Daemonize, if we should, but only after the servers have started.
|
# Daemonize, if we should, but only after the servers have started.
|
||||||
if options[:daemon]
|
if options[:daemon]
|
||||||
EventMachine.next_tick do
|
|
||||||
if quittable?
|
if quittable?
|
||||||
puts "*** MailCatcher runs as a daemon by default. Go to the web interface to quit."
|
puts "*** MailCatcher runs as a daemon by default. Go to the web interface to quit."
|
||||||
else
|
else
|
||||||
puts "*** MailCatcher is now running as a daemon that cannot be quit."
|
puts "*** MailCatcher is now running as a daemon that cannot be quit."
|
||||||
end
|
end
|
||||||
|
|
||||||
Process.daemon
|
Process.daemon
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
# Now wait for shutdown
|
||||||
|
@smtp_server.join
|
||||||
|
@http_server.join
|
||||||
|
|
||||||
|
logger.info "Bye! 👋"
|
||||||
end
|
end
|
||||||
|
|
||||||
def quit!
|
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
|
end
|
||||||
|
|
||||||
protected
|
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 "json"
|
||||||
require "mail"
|
require "mail"
|
||||||
require "sqlite3"
|
require "sqlite3"
|
||||||
@ -37,11 +36,11 @@ module MailCatcher::Mail extend self
|
|||||||
end
|
end
|
||||||
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'))")
|
@add_message_query ||= db.prepare("INSERT INTO message (sender, recipients, subject, source, type, size, created_at) VALUES (?, ?, ?, ?, ?, ?, datetime('now'))")
|
||||||
|
|
||||||
mail = Mail.new(message[:source])
|
mail = Mail.new(data)
|
||||||
@add_message_query.execute(message[:sender], JSON.generate(message[:recipients]), mail.subject, message[:source], mail.mime_type || "text/plain", message[:source].length)
|
@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
|
message_id = db.last_insert_row_id
|
||||||
parts = mail.all_parts
|
parts = mail.all_parts
|
||||||
parts = [mail] if parts.empty?
|
parts = [mail] if parts.empty?
|
||||||
@ -51,11 +50,6 @@ module MailCatcher::Mail extend self
|
|||||||
cid = part.cid if part.respond_to? :cid
|
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)
|
add_message_part(message_id, cid, part.mime_type || "text/plain", part.attachment? ? 1 : 0, part.filename, part.charset, body, body.length)
|
||||||
end
|
end
|
||||||
|
|
||||||
EventMachine.next_tick do
|
|
||||||
message = MailCatcher::Mail.message message_id
|
|
||||||
MailCatcher::Events::MessageAdded.push message
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_message_part(*args)
|
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"
|
require "mail_catcher/mail"
|
||||||
|
|
||||||
class MailCatcher::Smtp < EventMachine::Protocols::SmtpServer
|
class MailCatcher::SMTP < MidiSmtpServer::Smtpd
|
||||||
# We override EM's mail from processing to allow multiple mail-from commands
|
public :start
|
||||||
# per [RFC 2821](http://tools.ietf.org/html/rfc2821#section-4.1.1.2)
|
|
||||||
def process_mail_from sender
|
def initialize(host:, port:, logger: nil, **options)
|
||||||
if @state.include? :mail_from
|
super(port, host, 256, do_dns_reverse_lookup: false, logger: logger)
|
||||||
@state -= [:mail_from, :rcpt, :data]
|
|
||||||
receive_reset
|
|
||||||
end
|
end
|
||||||
|
|
||||||
super
|
def on_message_data_event(envelope:, message:, **context)
|
||||||
end
|
MailCatcher::Mail.add_message(from: envelope[:from], to: envelope[:to], data: message[:data])
|
||||||
|
|
||||||
def current_message
|
puts "==> SMTP: Received message from '#{envelope[:from]}' (#{message[:data].bytesize} bytes)"
|
||||||
@current_message ||= {}
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
|
||||||
rescue
|
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 " Exception: #{$!}"
|
||||||
puts " Backtrace:"
|
puts " Backtrace:"
|
||||||
$!.backtrace.each do |line|
|
$!.backtrace.each do |line|
|
||||||
puts " #{line}"
|
puts " #{line}"
|
||||||
end
|
end
|
||||||
puts " Please submit this as an issue at http://github.com/sj26/mailcatcher/issues"
|
puts " Please submit this as an issue at http://github.com/sj26/mailcatcher/issues"
|
||||||
false
|
|
||||||
ensure
|
raise MidiSmtpServer::Smtpd451Exception.new("Error receiving message, see MailCatcher log for details")
|
||||||
@current_message = nil
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
module MailCatcher
|
module MailCatcher
|
||||||
VERSION = "0.6.4"
|
VERSION = "2.0.0.alpha"
|
||||||
end
|
end
|
||||||
|
@ -3,39 +3,15 @@ require "net/http"
|
|||||||
require "uri"
|
require "uri"
|
||||||
|
|
||||||
require "sinatra"
|
require "sinatra"
|
||||||
require "skinny"
|
|
||||||
|
|
||||||
require "mail_catcher/events"
|
|
||||||
require "mail_catcher/mail"
|
require "mail_catcher/mail"
|
||||||
|
|
||||||
class Sinatra::Request
|
|
||||||
include Skinny::Helpers
|
|
||||||
end
|
|
||||||
|
|
||||||
module MailCatcher
|
module MailCatcher
|
||||||
module Web
|
module Web
|
||||||
class Application < Sinatra::Base
|
class Application < Sinatra::Base
|
||||||
set :development, ENV["MAILCATCHER_ENV"] == "development"
|
set :development, ENV["MAILCATCHER_ENV"] == "development"
|
||||||
set :root, File.expand_path("#{__FILE__}/../../../..")
|
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
|
|
||||||
end
|
|
||||||
|
|
||||||
helpers do
|
|
||||||
include Sprockets::Helpers
|
|
||||||
end
|
|
||||||
else
|
|
||||||
helpers do
|
helpers do
|
||||||
def javascript_tag(name)
|
def javascript_tag(name)
|
||||||
%{<script src="/assets/#{name}.js"></script>}
|
%{<script src="/assets/#{name}.js"></script>}
|
||||||
@ -45,7 +21,6 @@ module MailCatcher
|
|||||||
%{<link rel="stylesheet" href="/assets/#{name}.css">}
|
%{<link rel="stylesheet" href="/assets/#{name}.css">}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
get "/" do
|
get "/" do
|
||||||
erb :index
|
erb :index
|
||||||
@ -61,19 +36,9 @@ module MailCatcher
|
|||||||
end
|
end
|
||||||
|
|
||||||
get "/messages" do
|
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
|
content_type :json
|
||||||
JSON.generate(Mail.messages)
|
JSON.generate(Mail.messages)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
delete "/messages" do
|
delete "/messages" do
|
||||||
Mail.delete!
|
Mail.delete!
|
||||||
|
@ -23,6 +23,7 @@ Gem::Specification.new do |s|
|
|||||||
"lib/**/*.rb",
|
"lib/**/*.rb",
|
||||||
"public/**/*",
|
"public/**/*",
|
||||||
"views/**/*",
|
"views/**/*",
|
||||||
|
"vendor/gems/*/lib/**/*.rb",
|
||||||
] - Dir["lib/mail_catcher/web/assets.rb"]
|
] - Dir["lib/mail_catcher/web/assets.rb"]
|
||||||
s.require_paths = ["lib"]
|
s.require_paths = ["lib"]
|
||||||
s.executables = ["mailcatcher", "catchmail"]
|
s.executables = ["mailcatcher", "catchmail"]
|
||||||
@ -30,14 +31,6 @@ Gem::Specification.new do |s|
|
|||||||
|
|
||||||
s.required_ruby_version = ">= 1.9.3"
|
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 "coffee-script"
|
||||||
s.add_development_dependency "compass"
|
s.add_development_dependency "compass"
|
||||||
s.add_development_dependency "minitest", "~> 5.0"
|
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