class Reference
Represents a "Hypertext Reference", which may include a path, query string, fragment, and user parameters.
This class is designed to be easy to manipulate and combine URL references, following the rules specified in RFC2396, while supporting standard URL encoded parameters.
Use Protocol::URL::Reference.parse for external/untrusted data, and new for constructing references from known good values.
Definitions
def self.[](value, parameters = nil)
Coerce a value into a class Protocol::URL::Reference instance.
This method provides flexible conversion from various types into a class Protocol::URL::Reference.
When given a String, it parses the URL-encoded path, query, and fragment components
and unescapes them for internal storage. When given a class Protocol::URL::Relative, it converts the
encoded values to unescaped form suitable for class Protocol::URL::Reference instances.
Example: Coerce a string with path, query, and fragment.
reference = Reference["/search?q=ruby#results"]
reference.path # => "/search"
reference.query # => "q=ruby"
reference.fragment # => "results"
Example: Coerce with additional parameters.
reference = Reference["/search", {"limit" => "10"}]
reference.to_s # => "/search?limit=10"
Example: Coerce a Relative instance.
relative = Relative.new("/path%20with%20spaces", nil, "top")
reference = Reference[relative]
reference.path # => "/path with spaces"
Signature
-
parameter
valueString | Relative | Nil The value to coerce.
-
parameter
parametersHash | Nil Optional user-supplied parameters to append to the query string.
-
returns
Reference | Nil A new reference instance, or
nilif the input isnil.-
raises
ArgumentError If the string contains whitespace or control characters.
-
raises
ArgumentError If the value cannot be coerced to a
class Protocol::URL::Reference.
Implementation
def self.[](value, parameters = nil)
case value
when String
if match = value.match(PATTERN)
path = match[:path]
query = match[:query]
fragment = match[:fragment]
# Unescape path and fragment for user-friendly internal storage:
# Query strings are kept as-is since they contain = and & syntax
path = Encoding.unescape(path) if path && !path.empty?
fragment = Encoding.unescape(fragment) if fragment
self.new(path, query, fragment, parameters)
else
raise ArgumentError, "Invalid URL (contains whitespace or control characters): #{value.inspect}"
end
when Relative
# Relative stores encoded values, so we need to unescape them for Reference:
path = value.path
fragment = value.fragment
path = Encoding.unescape(path) if path && !path.empty?
fragment = Encoding.unescape(fragment) if fragment
self.new(path, value.query, fragment, parameters)
when nil
nil
else
raise ArgumentError, "Cannot coerce #{value.inspect} to Reference!"
end
end # Generate a reference from a path and user parameters. The path may contain a `#fragment` or `?query=parameters`.
def self.parse(value = "/", parameters = nil)
Example: Parse a path with query and fragment.
reference = Reference.parse("/search?query=ruby#results")
reference.path # => "/search"
reference.query # => "query=ruby"
reference.fragment # => "results"
Signature
Implementation
def self.parse(value = "/", parameters = nil)
self.[](value, parameters)
end
def initialize(path = "/", query = nil, fragment = nil, parameters = nil)
Initialize the reference with raw, unescaped values.
Example: Create a reference with parameters.
reference = Reference.new("/search", nil, nil, {"query" => "ruby", "limit" => "10"})
reference.to_s # => "/search?query=ruby&limit=10"
Signature
-
parameter
pathString The unescaped path.
-
parameter
queryString | Nil An already-formatted query string.
-
parameter
fragmentString | Nil The unescaped fragment.
-
parameter
parametersHash | Nil User supplied parameters that will be safely encoded.
Implementation
def initialize(path = "/", query = nil, fragment = nil, parameters = nil)
super(path, query, fragment)
@parameters = parameters
end
attr :parameters
Signature
-
attribute
Hash User supplied parameters that will be appended to the query part.
def freeze
Freeze the reference.
Signature
-
returns
Reference The frozen reference.
Implementation
def freeze
return self if frozen?
@parameters.freeze
super
end
def to_ary
Implicit conversion to an array.
Signature
-
returns
Array The reference as an array,
[path, query, fragment, parameters].
Implementation
def to_ary
[@path, @query, @fragment, @parameters]
end
def parameters?
Signature
-
returns
Boolean Whether the reference has parameters.
Implementation
def parameters?
@parameters and !@parameters.empty?
end
def parse_query!(encoding = Encoding)
Parse the query string into parameters and merge with existing parameters.
Afterwards, the query attribute will be cleared.
Signature
-
returns
Hash The merged parameters.
Implementation
def parse_query!(encoding = Encoding)
if @query and !@query.empty?
parsed = encoding.decode(@query)
if @parameters
@parameters = @parameters.merge(parsed)
else
@parameters = parsed
end
@query = nil
end
return @parameters
end
def query?
Signature
-
returns
Boolean Whether the reference has a query string.
Implementation
def query?
@query and !@query.empty?
end
def fragment?
Signature
-
returns
Boolean Whether the reference has a fragment.
Implementation
def fragment?
@fragment and !@fragment.empty?
end
def append(buffer = String.new)
Append the reference to the given buffer. Encodes the path and fragment which are stored unescaped internally. Query strings are passed through as-is (they contain = and & which are valid syntax).
Implementation
def append(buffer = String.new)
buffer << Encoding.escape_path(@path)
if @query and !@query.empty?
buffer << "?" << @query
buffer << "&" << Encoding.encode(@parameters) if parameters?
elsif parameters?
buffer << "?" << Encoding.encode(@parameters)
end
if @fragment and !@fragment.empty?
buffer << "#" << Encoding.escape_fragment(@fragment)
end
return buffer
end
def +(other)
Merges two references as specified by RFC2396, similar to URI.join.
Implementation
def + other
other = self.class[other]
self.class.new(
Path.expand(self.path, other.path, true),
other.query,
other.fragment,
other.parameters,
)
end
def base
Just the base path, without any query string, parameters or fragment.
Implementation
def base
self.class.new(@path, nil, nil, nil)
end
def with(path: nil, query: false, fragment: @fragment, parameters: false, pop: false, merge: true)
Update the reference with the given path, query, fragment, and parameters.
Example: Merge parameters.
reference = Reference.new("/search", nil, nil, {"query" => "ruby"})
updated = reference.with(parameters: {"limit" => "10"})
updated.to_s # => "/search?query=ruby&limit=10"
Example: Replace parameters.
reference = Reference.new("/search", nil, nil, {"query" => "ruby"})
updated = reference.with(parameters: {"query" => "python"}, merge: false)
updated.to_s # => "/search?query=python"
Signature
-
parameter
pathString Append the string to this reference similar to
File.join.-
parameter
queryString | Nil Replace the query string. Defaults to keeping the existing query if not specified.
-
parameter
fragmentString | Nil Replace the fragment. Defaults to keeping the existing fragment if not specified.
-
parameter
parametersHash | false Parameters to merge or replace. Pass
false(default) to keep existing parameters.-
parameter
popBoolean If the path contains a trailing filename, pop the last component of the path before appending the new path.
-
parameter
mergeBoolean Controls how parameters are handled. When
true(default), new parameters are merged with existing ones and query is kept. Whenfalseand new parameters are provided, parameters replace existing ones and query is cleared. Explicitly passingquery:always overrides this behavior.
Implementation
def with(path: nil, query: false, fragment: @fragment, parameters: false, pop: false, merge: true)
if merge
# If merging, we keep existing query unless explicitly overridden:
if query == false
query = @query
end
# Merge mode: combine new parameters with existing, keep query:
# parameters = (@parameters || {}).merge(parameters || {})
if @parameters
if parameters
parameters = @parameters.merge(parameters)
else
parameters = @parameters
end
elsif !parameters
parameters = @parameters
end
else
# Replace mode: use new parameters if provided, clear query when replacing:
if parameters == false
# No new parameters provided, keep existing:
parameters = @parameters
# Also keep query if not explicitly specified:
if query == false
query = @query
end
else
# New parameters provided, clear query unless explicitly specified:
if query == false
query = nil
end
end
end
path = Path.expand(@path, path, pop)
self.class.new(path, query, fragment, parameters)
end