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
|
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2025, by Samuel Williams.
require_relative "relative"
module Protocol
module URL
# Represents an absolute URL with scheme and/or authority.
# Examples: "https://example.com/path", "//cdn.example.com/lib.js", "http://localhost/"
class Absolute < Relative
def initialize(scheme, authority, path = "/", query = nil, fragment = nil)
@scheme = scheme
@authority = authority
# Initialize the parent Relative class with the path component
super(path, query, fragment)
end
attr :scheme
attr :authority
def scheme?
@scheme and !@scheme.empty?
end
def authority?
@authority and !@authority.empty?
end
# Combine this absolute URL with a relative reference according to RFC 3986 Section 5.
#
# @parameter other [String, Relative, Reference, Absolute] The reference to resolve.
# @returns [Absolute, String] The resolved absolute URL.
#
# @example Resolve a relative path.
# base = Absolute.new("https", "example.com", "/documents/reports/")
# relative = Relative.new("summary.pdf")
# result = base + relative
# result.to_s # => "https://example.com/documents/reports/summary.pdf"
#
# @example Navigate to parent directory.
# base = Absolute.new("https", "example.com", "/documents/reports/2024/")
# relative = Relative.new("../../archive/")
# result = base + relative
# result.to_s # => "https://example.com/documents/archive/"
def +(other)
case other
when Absolute
# If other is already absolute with a scheme, return it as-is:
return other if other.scheme
# Protocol-relative URL: inherit scheme from base:
return Absolute.new(@scheme, other.authority, other.path, other.query, other.fragment)
when Relative
# Already a Relative, use directly.
when String
other = URL[other]
# If parsing resulted in an Absolute URL, handle it:
if other.is_a?(Absolute)
return other if other.scheme
# Protocol-relative URL: inherit scheme from base:
return Absolute.new(@scheme, other.authority, other.path, other.query, other.fragment)
end
else
raise ArgumentError, "Cannot combine Absolute URL with #{other.class}"
end
# RFC 3986 Section 5.3: Component Recomposition
# At this point, other is a Relative URL
# Check for special cases first:
if other.path.empty?
# Empty path - could be query-only or fragment-only reference:
if other.query
# Query replacement: use base path with new query:
Absolute.new(@scheme, @authority, @path, other.query, other.fragment)
else
# Fragment-only: keep everything from base, just change fragment:
Absolute.new(@scheme, @authority, @path, @query, other.fragment || @fragment)
end
else
# Relative path: merge with base path:
path = Path.expand(@path, other.path)
Absolute.new(@scheme, @authority, path, other.query, other.fragment)
end
end
# Append the absolute URL to the given buffer.
def append(buffer = String.new)
buffer << @scheme << ":" if @scheme
buffer << "//" << @authority if @authority
super(buffer)
end
UNSPECIFIED = Object.new
# Create a new Absolute URL with modified components.
#
# @parameter scheme [String, nil] The scheme to use (nil to remove scheme).
# @parameter authority [String, nil] The authority to use (nil to remove authority).
# @parameter path [String, nil] The path to merge with the current path.
# @parameter query [String, nil] The query string to use.
# @parameter fragment [String, nil] The fragment to use.
# @parameter pop [Boolean] Whether to pop the last path component before merging.
# @returns [Absolute] A new Absolute URL with the modified components.
#
# @example Change the scheme.
# url = Absolute.new("http", "example.com", "/page")
# secure = url.with(scheme: "https")
# secure.to_s # => "https://example.com/page"
#
# @example Update the query string.
# url = Absolute.new("https", "example.com", "/search", "query=ruby")
# updated = url.with(query: "query=python")
# updated.to_s # => "https://example.com/search?query=python"
def with(scheme: @scheme, authority: @authority, path: nil, query: @query, fragment: @fragment, pop: true)
self.class.new(scheme, authority, Path.expand(@path, path, pop), query, fragment)
end
def to_ary
[@scheme, @authority, @path, @query, @fragment]
end
def <=>(other)
to_ary <=> other.to_ary
end
def to_s
append
end
end
end
end
|