Working with References
This guide explains how to use class Protocol::URL::Reference for managing URLs with query parameters and fragments.
Overview
class Protocol::URL::Reference extends class Protocol::URL::Relative with support for query strings and fragments. References are ideal when you need to work with URLs that include query parameters (like ?page=2&sort=name) or fragments (like #section-3).
Creating References
You can create references in several ways:
Parsing External URLs (Untrusted Data)
Use Protocol::URL.parse or Protocol::URL.[] to parse URL strings from external sources (user input, APIs, web pages). These methods reject whitespace and control characters, preserve the encoded path structure, and expose decoded path components:
# Parse a reference with query and fragment:
reference = Protocol::URL["/api/users?active=true&role=admin#list"]
reference.path.to_s # => "/api/users"
reference.query # => "active=true&role=admin"
reference.fragment # => "list"
Constructing from Known Values (Trusted Data)
Use Protocol::URL::Reference.new when you have known good values from
your code. This method does not validate its input. Path strings contain encoded
URL syntax, query strings are already formatted, and fragments are decoded:
require "protocol/url"
# Reference with path only:
reference = Protocol::URL::Reference.new("/api/users")
reference.to_s # => "/api/users"
# Reference with query string:
reference = Protocol::URL::Reference.new("/search", "q=ruby&page=2")
reference.to_s # => "/search?q=ruby&page=2"
# Reference with all components:
reference = Protocol::URL::Reference.new("/api/users", "status=active", "results")
reference.to_s # => "/api/users?status=active#results"
# Using parameters (recommended for query strings):
reference = Protocol::URL::Reference.new("/search", nil, nil, {q: "ruby", page: 2})
reference.to_s # => "/search?q=ruby&page=2"
# Construct a path from decoded application values explicitly:
path = Protocol::URL::Path.for(["", "users", "Samuel Williams"])
reference = Protocol::URL::Reference.new(path)
reference.to_s # => "/users/Samuel%20Williams"
Understanding Encoding
References retain their path as a class Protocol::URL::Path. Encoded and decoded
path values therefore use the same explicit boundaries as Path itself.
With parse() - Preserves Encoded Input and Decodes Components
parse() expects already-encoded URLs. It returns a class Protocol::URL::Path
which preserves the exact encoded segments and decodes components only when
requested:
ref = Protocol::URL::Reference.parse("path%20with%20spaces?foo=bar#frag%20ment")
ref.path.to_s # => "path%20with%20spaces" (encoded URL path)
ref.path.segments # => ["path%20with%20spaces"] (encoded segments)
ref.path.components # => ["path with spaces"] (decoded components)
ref.fragment # => "frag ment" (decoded)
ref.to_s # => "path%20with%20spaces?foo=bar#frag%20ment" (re-encoded)
With new() - Expects an Encoded Path
new() treats a path string as encoded URL syntax. The fragment remains a
decoded value and is encoded during output:
ref = Protocol::URL::Reference.new("path%20with%20spaces", "foo=bar", "frag ment")
ref.path.components # => ["path with spaces"]
ref.fragment # => "frag ment"
ref.to_s # => "path%20with%20spaces?foo=bar#frag%20ment"
Use Protocol::URL::Path.for when starting with decoded application data:
path = Protocol::URL::Path.for(["I", "❤️", "UNICODE"])
ref = Protocol::URL::Reference.new(path)
ref.to_s # => "I/%E2%9D%A4%EF%B8%8F/UNICODE"
Accessing Components
References provide accessors for all URL components:
reference = Protocol::URL["/api/v1/users?page=2&limit=50#results"]
# Path component:
reference.path.to_s # => "/api/v1/users"
# Query string (unparsed):
reference.query # => "page=2&limit=50"
# Fragment (decoded):
reference.fragment # => "results"
Updating References
The Protocol::URL::Reference#with method creates a new reference with modified components. This follows an immutable pattern - the original reference is unchanged.
Modifying the Path
base = Protocol::URL::Reference.new("/api/v1/users")
# Append to path with relative reference:
detail = base.with(path: "123")
detail.to_s # => "/api/v1/users/123"
# Navigate with relative paths:
sibling = detail.with(path: "../groups")
sibling.to_s # => "/api/v1/groups"
# Replace with absolute path:
root = base.with(path: "/status")
root.to_s # => "/status"
The path resolution follows RFC 3986 rules, using Protocol::URL::Path#join internally.
Updating Query Parameters
base = Protocol::URL::Reference.new("/search", "q=ruby")
# Replace query string:
filtered = base.with(query: "q=ruby&lang=en")
filtered.to_s # => "/search?q=ruby&lang=en"
# Remove query string:
no_query = base.with(query: nil)
no_query.to_s # => "/search"
Updating Fragments
doc = Protocol::URL::Reference.new("/docs/guide")
# Add fragment:
section = doc.with(fragment: "installation")
section.to_s # => "/docs/guide#installation"
# Change fragment:
different = section.with(fragment: "usage")
different.to_s # => "/docs/guide#usage"
# Remove fragment:
no_fragment = section.with(fragment: nil)
no_fragment.to_s # => "/docs/guide"
Updating Multiple Components
You can update multiple components at once:
base = Protocol::URL::Reference.new("/api/users")
modified = base.with(
path: "posts",
query: "author=john&status=published",
fragment: "top"
)
modified.to_s # => "/api/posts?author=john&status=published#top"
Combining with Absolute URLs
References can be combined with absolute URLs to create complete URLs:
# Base absolute URL:
base = Protocol::URL["https://api.example.com/v1"]
# Relative reference:
reference = Protocol::URL::Reference.new("users", "active=true", "list")
# Combine them:
result = base + reference
result.to_s # => "https://api.example.com/v1/users?active=true#list"
Fragment Encoding
Fragments are automatically decoded when parsing and encoded when converting to strings:
# Parsing decodes percent-encoded fragments:
reference = Protocol::URL["/docs#hello%20world"]
reference.fragment # => "hello world" (decoded)
reference.to_s # => "/docs#hello%20world" (re-encoded)
# Special characters are preserved:
reference = Protocol::URL["/page#section/1.2?note"]
reference.fragment # => "section/1.2?note"
# Characters like / and ? are allowed in fragments per RFC 3986
Practical Examples
Building Paginated API Requests
# Start with base endpoint:
endpoint = Protocol::URL::Reference.new("/api/users", "page=1&limit=20")
# Parse query string into parameters:
endpoint.parse_query!
endpoint.parameters # => {"page" => "1", "limit" => "20"}
# Update page number:
next_page = endpoint.with(parameters: {"page" => "2"})
next_page.to_s # => "/api/users?page=2&limit=20"
# Add filtering (merge with existing parameters):
filtered = endpoint.with(parameters: {"status" => "active"})
filtered.to_s # => "/api/users?page=1&limit=20&status=active"
Documentation Links with Anchors
# Base documentation path:
doc = Protocol::URL::Reference.new("/docs/api")
# Link to specific section:
intro = doc.with(fragment: "introduction")
intro.to_s # => "/docs/api#introduction"
# Different section in same document:
methods = doc.with(fragment: "methods")
methods.to_s # => "/docs/api#methods"
# Navigate to related document:
tutorial = doc.with(path: "/docs/tutorial", fragment: "step-1")
tutorial.to_s # => "/docs/tutorial#step-1"
Search Results with Filters
# Initial search:
search = Protocol::URL::Reference.new("/search", "q=ruby")
# Add language filter:
filtered = search.with(query: "q=ruby&lang=en")
filtered.to_s # => "/search?q=ruby&lang=en"
# Jump to specific result:
result = filtered.with(fragment: "result-5")
result.to_s # => "/search?q=ruby&lang=en#result-5"
Best Practices
When to Use References
- Use
class Protocol::URL::Referencewhen working with query parameters or fragments - Use
class Protocol::URL::Relativefor simple path-only URLs - Use
class Protocol::URL::Absolutefor complete URLs with scheme and host
parse() vs new()
Choose the right method based on your data source:
- Use
parse()or[]for external/untrusted data (user input, URLs from web pages, API responses). These methods reject whitespace and control characters while preserving encoded path boundaries. - Use
new()for known encoded paths or aPathconstructed explicitly from decoded components. It skips whole-reference parsing and validation.
# External data - use parse():
user_input = "/search?q=ruby%20gems"
reference = Protocol::URL[user_input] # Validates and decodes
# Internal decoded data - make the encoding boundary explicit:
path = Protocol::URL::Path.for(["", "api", "users"])
reference = Protocol::URL::Reference.new(path, "status=active")
Query String Management
The library provides built-in parameter handling through the parameters attribute:
# Create reference with query string:
reference = Protocol::URL::Reference.new("/search", "q=ruby&page=2")
# Parse query string into parameters hash:
reference.parse_query!
reference.parameters # => {"q" => "ruby", "page" => "2"}
reference.query # => nil (cleared after parsing)
# Update with new parameters (merged):
updated = reference.with(parameters: {"lang" => "en"})
updated.to_s # => "/search?q=ruby&page=2&lang=en"
# Replace parameters completely (merge: false):
replaced = reference.with(parameters: {"q" => "python"}, merge: false)
replaced.to_s # => "/search?q=python"
Alternatively, you can provide parameters directly when creating a reference:
# Create with parameters directly:
reference = Protocol::URL::Reference.new("/search", nil, nil, {"q" => "ruby", "page" => "2"})
reference.to_s # => "/search?q=ruby&page=2"
Immutability
References are immutable - with always returns a new instance:
original = Protocol::URL::Reference.new("/api/users")
modified = original.with(query: "active=true")
original.to_s # => "/api/users" (unchanged)
modified.to_s # => "/api/users?active=true" (new instance)