Protocol::URLGuidesWorking with References

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"

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

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)