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 preserves the path as a class Protocol::URL::Path. When given a class Protocol::URL::Relative, it preserves the
existing path and its component boundaries.
Example: Coerce a string with path, query, and fragment.
reference = Reference["/search?q=ruby#results"]
reference.path.to_s # => "/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.components # => ["", "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]
# Paths retain their encoded structure while exposing decoded components.
path = Path[path]
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 an encoded path; preserve its component boundaries.
path = value.path
fragment = value.fragment
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
def self.parse(value = "/", parameters = nil)
Generate a reference from a path and user parameters. The path may contain a #fragment or ?query=parameters.
Example: Parse a path with query and fragment.
reference = Reference.parse("/search?query=ruby#results")
reference.path.to_s # => "/search"
reference.query # => "query=ruby"
reference.fragment # => "results"
Implementation
def self.parse(value = "/", parameters = nil)
self.[](value, parameters)
end
def initialize(path = "/", query = nil, fragment = nil, parameters = nil)
Initialize the reference from an encoded path and reference 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 | Path The encoded path string, or an existing 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_accessor :parameters
Signature
-
attribute
Hash | Nil 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, explicit: false)
Append the reference to the given buffer. Encodes the fragment; the path already retains its encoded structure. Query strings are passed through as-is (they contain = and & which are valid syntax).
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
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.join(other.path),
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 | Path Append the encoded path 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
if path.nil?
path = @path
else
path = @path.join(path, pop: pop)
end
self.class.new(path, query, fragment, parameters)
end