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 validate and decode the input:
# Parse a reference with query and fragment:
reference = Protocol::URL["/api/users?active=true&role=admin#list"]
reference.path # => "/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 doesn't validate and expects unencoded values:
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"
Understanding Encoding
References use different encoding strategies depending on how they're constructed:
With parse() - Decodes Input
parse() expects already-encoded URLs and decodes them for internal storage:
ref = Protocol::URL::Reference.parse("path%20with%20spaces?foo=bar#frag%20ment")
ref.path # => "path with spaces" (decoded)
ref.fragment # => "frag ment" (decoded)
ref.to_s # => "path%20with%20spaces?foo=bar#frag%20ment" (re-encoded)
With new() - Expects Unencoded Input
new() expects raw, unencoded values and encodes them during output:
ref = Protocol::URL::Reference.new("path with spaces", "foo=bar", "frag ment")
ref.path # => "path with spaces"
ref.fragment # => "frag ment"
ref.to_s # => "path%20with%20spaces?foo=bar#frag%20ment"
Warning: Passing encoded values to new() causes double-encoding:
# Wrong - will double-encode:
ref = Protocol::URL::Reference.new("path%20with%20spaces")
ref.to_s # => "path%2520with%2520spaces" (double-encoded!)
# Correct - use parse() for encoded input:
ref = Protocol::URL::Reference.parse("path%20with%20spaces")
ref.to_s # => "path%20with%20spaces"
Unicode and special characters are handled automatically:
ref = Protocol::URL::Reference.new("I/❤️/UNICODE")
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 # => "/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.expand 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 validate and decode the URL. - Use
new()for known good values from your code. This is more efficient since it skips validation and expects unencoded values.
# External data - use parse():
user_input = "/search?q=ruby%20gems"
reference = Protocol::URL[user_input] # Validates and decodes
# Internal data - use new():
reference = Protocol::URL::Reference.new("/api/users", "status=active") # Direct construction
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)