Protocol::URLSourceProtocolURLReference

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 value String | Relative | Nil

The value to coerce.

parameter parameters Hash | Nil

Optional user-supplied parameters to append to the query string.

returns Reference | Nil

A new reference instance, or nil if the input is nil.

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 path String

The unescaped path.

parameter query String | Nil

An already-formatted query string.

parameter fragment String | Nil

The unescaped fragment.

parameter parameters Hash | 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 path String

Append the string to this reference similar to File.join.

parameter query String | Nil

Replace the query string. Defaults to keeping the existing query if not specified.

parameter fragment String | Nil

Replace the fragment. Defaults to keeping the existing fragment if not specified.

parameter parameters Hash | false

Parameters to merge or replace. Pass false (default) to keep existing parameters.

parameter pop Boolean

If the path contains a trailing filename, pop the last component of the path before appending the new path.

parameter merge Boolean

Controls how parameters are handled. When true (default), new parameters are merged with existing ones and query is kept. When false and new parameters are provided, parameters replace existing ones and query is cleared. Explicitly passing query: 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