1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
|
# frozen_string_literal: true
require "set"
module Roadie
# @api private
# Class that handles URL generation
#
# URL generation is all about converting relative URLs into absolute URLS
# according to the given options. It is written such as absolute URLs will
# get passed right through, so all URLs could be passed through here.
class UrlGenerator
attr_reader :url_options
# Create a new instance with the given URL options.
#
# Initializing without a host setting raises an error, as do unknown keys.
#
# @param [Hash] url_options
# @option url_options [String] :host (required)
# @option url_options [String, Integer] :port
# @option url_options [String] :path root path
# @option url_options [String] :scheme URL scheme ("http" is default)
# @option url_options [String] :protocol alias for :scheme
def initialize(url_options)
unless url_options
raise ArgumentError, "No URL options were specified"
end
unless url_options[:host]
raise ArgumentError,
"No :host was specified; options were: #{url_options.inspect}"
end
validate_options url_options
@url_options = url_options
@scheme = normalize_scheme(url_options[:scheme] || url_options[:protocol])
@root_uri = build_root_uri
end
# Generate an absolute URL from a relative URL.
#
# If the passed path is already an absolute URL or just an anchor
# reference, it will be returned as-is.
# If passed a blank path, the "root URL" will be returned. The root URL is
# the URL that the {#url_options} would generate by themselves.
#
# An optional base can be specified. The base is another relative path from
# the root that specifies an "offset" from which the path was found in. A
# common use-case is to convert a relative path found in a stylesheet which
# resides in a subdirectory.
#
# @example Normal conversions
# generator = Roadie::UrlGenerator.new host: "foo.com", scheme: "https"
# generator.generate_url("bar.html") # => "https://foo.com/bar.html"
# generator.generate_url("/bar.html") # => "https://foo.com/bar.html"
# generator.generate_url("") # => "https://foo.com"
#
# @example Conversions with a base
# generator = Roadie::UrlGenerator.new host: "foo.com", scheme: "https"
# generator.generate_url("../images/logo.png", "/css") # => "https://foo.com/images/logo.png"
# generator.generate_url("../images/logo.png", "/assets/css") # => "https://foo.com/assets/images/logo.png"
#
# @param [String] base The base which the relative path comes from
# @return [String] an absolute URL
def generate_url(path, base = "/")
return root_uri.to_s if path.nil? || path.empty?
return path if path_is_anchor?(path)
return add_scheme(path) if path_is_schemeless?(path)
return path if Utils.path_is_absolute?(path)
combine_segments(root_uri, base, path).to_s
end
protected
attr_reader :root_uri, :scheme
private
def build_root_uri
path = make_absolute url_options[:path]
port = parse_port url_options[:port]
URI::Generic.build(
scheme: scheme,
host: url_options[:host],
port: port,
path: path
)
end
def add_scheme(path)
[scheme, path].join(":")
end
def combine_segments(root, base, path)
new_path = apply_base(base, path)
if root.path
new_path = File.join(root.path, new_path)
end
root.merge(new_path)
end
def apply_base(base, path)
if path[0] == "/"
path
else
File.join(base, path)
end
end
# Strip :// from any scheme, if present
def normalize_scheme(scheme)
return "http" unless scheme
scheme.to_s[/^\w+/]
end
def parse_port(port)
(port ? port.to_i : port)
end
def make_absolute(path)
if path.nil? || path[0] == "/"
path
else
"/#{path}"
end
end
def path_is_schemeless?(path)
path =~ %r{^//\w}
end
def path_is_anchor?(path)
path.start_with? "#"
end
VALID_OPTIONS = Set[:host, :port, :path, :protocol, :scheme].freeze
def validate_options(options)
keys = Set.new(options.keys)
unless keys.subset? VALID_OPTIONS
raise ArgumentError,
"Passed invalid options: #{(keys - VALID_OPTIONS).to_a}, " \
"valid options are: #{VALID_OPTIONS.to_a}"
end
end
end
end
|