class Relative
Represents a relative URL, which does not include a scheme or authority.
Definitions
def initialize(path, query = nil, fragment = nil)
Initialize a new relative URL.
Signature
-
parameter
pathString | Path The encoded path component.
-
parameter
queryString | Nil The query string.
-
parameter
fragmentString | Nil The fragment identifier.
Implementation
def initialize(path, query = nil, fragment = nil)
@path = Path[path]
@query = query
@fragment = fragment
end
def freeze
Freeze the URL and its direct components.
Signature
-
returns
Relative The frozen URL.
Implementation
def freeze
return self if frozen?
@path.freeze
@query.freeze
@fragment.freeze
return super
end
attr :path
Signature
-
attribute
Path The path component of the URL.
def path=(path)
Replace the path component of this URL.
Signature
-
parameter
pathString | Path The encoded path component.
-
returns
Path The assigned path component.
Implementation
def path=(path)
@path = Path[path]
end
attr_accessor :query
Signature
-
attribute
String | Nil The query string component.
attr_accessor :fragment
Signature
-
attribute
String | Nil The fragment identifier.
def local_path(root)
Resolve the URL path beneath a local filesystem root.
Signature
-
parameter
rootString The filesystem root beneath which to resolve the URL path.
-
returns
String The expanded local filesystem path.
-
raises
ArgumentError If a URL segment is invalid or the path escapes the specified root.
Implementation
def local_path(root)
@path.local_path(root)
end
def query?
Signature
-
returns
Boolean If there is a query string.
Implementation
def query?
@query and !@query.empty?
end
def fragment?
Signature
-
returns
Boolean If there is a fragment.
Implementation
def fragment?
@fragment and !@fragment.empty?
end
def +(other)
Combine this relative URL with another URL or path.
Example: Combine two relative paths.
base = Relative.new("/documents/reports/")
other = Relative.new("invoices/2024.pdf")
result = base + other
result.path.to_s # => "/documents/reports/invoices/2024.pdf"
Example: Navigate to parent directory.
base = Relative.new("/documents/reports/archive/")
other = Relative.new("../../summary.pdf")
result = base + other
result.path.to_s # => "/documents/summary.pdf"
Signature
-
parameter
otherString, Absolute, Relative The URL or path to combine.
-
returns
Absolute, Relative The combined URL.
Implementation
def +(other)
case other
when Absolute
# Relative + Absolute: the absolute URL takes precedence
# You can't apply relative navigation to an absolute URL
other
when Relative
# Relative + Relative: merge paths directly
self.class.new(
@path.join(other.path),
other.query,
other.fragment
)
when String
# Relative + String: parse and combine
self + URL[other]
else
raise ArgumentError, "Cannot combine Relative URL with #{other.class}"
end
end
def with(path: nil, query: @query, fragment: @fragment, pop: true)
Create a new Relative URL with modified components.
Example: Update the query string.
url = Relative.new("/search", "query=ruby")
updated = url.with(query: "query=python")
updated.to_s # => "/search?query=python"
Example: Append to the path.
url = Relative.new("/documents/")
updated = url.with(path: "report.pdf", pop: false)
updated.to_s # => "/documents/report.pdf"
Signature
-
parameter
pathString | Nil The path to merge with the current path.
-
parameter
queryString | Nil The query string to use.
-
parameter
fragmentString | Nil The fragment to use.
-
parameter
popBoolean Whether to pop the last path component before merging.
-
returns
Relative A new Relative URL with the modified components.
Implementation
def with(path: nil, query: @query, fragment: @fragment, pop: true)
path = @path.join(path, pop: pop) unless path.nil?
self.class.new(path || @path, query, fragment)
end
def relative_to(base)
Express this URL relative to the given base path.
Already-relative paths are returned unchanged. Query and fragment components are preserved when converting a root-relative path.
Signature
-
parameter
baseRelative | Path | String The base URL or path.
-
returns
Relative The relative URL.
Implementation
def relative_to(base)
return self unless @path.absolute?
if base.is_a?(Relative)
base = base.path
end
return self.class.new(@path.relative(base), @query, @fragment)
end
def normalize!
Normalize the encoded path and simplify its structure.
This modifies the URL in-place by normalizing and simplifying the path component:
- Decodes percent-encoded unreserved characters
- Uses uppercase hexadecimal digits for retained percent escapes
- Removes "." segments (current directory)
- Resolves ".." segments (parent directory)
- Collapses empty path segments represented by consecutive slashes
Normalization is intentionally lossy. Callers that need to preserve the original path structure should retain the parsed URL and avoid this method.
Example: Basic normalization
url = Relative.new("/foo//bar/./baz/../qux")
url.normalize!
url.path.to_s # => "/foo/bar/qux"
Signature
-
returns
self The normalized URL.
Implementation
def normalize!
@path = @path.normalize.simplify
return self
end
def append(buffer = String.new, explicit: false)
Append the relative URL to the given buffer. The path, query, and fragment are expected to already be properly encoded.
Signature
-
parameter
explicitBoolean Whether the result should be lexically identifiable as a URL in a mixed grammar.
Implementation
def append(buffer = String.new, explicit: false)
append_path(buffer, explicit: explicit)
if @query and !@query.empty?
buffer << "?" << @query
end
if @fragment and !@fragment.empty?
buffer << "#" << @fragment
end
return buffer
end
def to_ary
Convert the URL to an array representation.
Signature
-
returns
Array An array of
[path, query, fragment].
Implementation
def to_ary
[@path, @query, @fragment]
end
def hash
Compute a hash value for the URL based on its components.
Signature
-
returns
Integer The hash value.
Implementation
def hash
to_ary.hash
end
def equal?(other)
Check if this URL is equal to another URL by comparing components.
Signature
-
parameter
otherRelative The URL to compare with.
-
returns
Boolean True if the URLs have identical components.
Implementation
def equal?(other)
to_ary == other.to_ary
end
def <=>(other)
Compare this URL with another for sorting purposes.
Signature
-
parameter
otherRelative The URL to compare with.
-
returns
Integer -1, 0, or 1 based on component-wise comparison.
Implementation
def <=>(other)
to_ary <=> other.to_ary
end
def ==(other)
Check structural equality by comparing components.
Signature
-
parameter
otherRelative The URL to compare with.
-
returns
Boolean True if the URLs have identical components.
Implementation
def ==(other)
to_ary == other.to_ary
end
def ===(other)
Check string equality, useful for case statements.
Signature
-
parameter
otherString, Relative The value to compare with.
-
returns
Boolean True if the string representations match.
Implementation
def ===(other)
to_s === other
end
def to_s(explicit: false)
Convert the URL to its string representation.
When explicit, same-directory references start with ./ so they can be
distinguished from non-URL values in a mixed grammar.
Signature
-
parameter
explicitBoolean Whether the result should be lexically identifiable as a URL in a mixed grammar.
-
returns
String The formatted URL string.
Implementation
def to_s(explicit: false)
append(explicit: explicit)
end
def as_json(...)
Convert the URL to a JSON-compatible representation.
Signature
-
returns
String The URL as a string.
Implementation
def as_json(...)
to_s
end
def to_json(...)
Convert the URL to JSON.
Signature
-
returns
String The JSON-encoded URL.
Implementation
def to_json(...)
as_json.to_json(...)
end
def inspect
Generate a human-readable representation for debugging.
Signature
-
returns
String A string like
#<Protocol::URL::Relative /path?query#fragment>.
Implementation
def inspect
"#<#{self.class} #{to_s}>"
end