RFC Web Link
RFC Web Link implements RFC 8288: Web Linking. This allows you to process links via the HTTP link header when processing HTTP responses. For example, the following demonstrates a HTTP link header which links to a previous and next article:
link: <https://demo.io/articles?page=1>; rel="previous"; title="Previous",
<https://demo.io/articles?page=3>; rel="next"; title="Next"
The above parses each line, delimited by a comma (,), and then each pair (attribute) in the line as delimited by a semicolon (;). This is not something you want to reinvent for each application you maintain so this gem handles the parsing (and creation) of HTTP link headers so you can stay focused on your own business logic.
- Features
- Requirements
- Setup
- Usage
- Root URI
- Delimiters
- Attributes
- anchor
- hreflang
- media
- rel
- title
- type
- Extensions
- Models
- List
- Link
- Pair
- Error Handling
- Development
- Tests
- License
- Security
- Code of Conduct
- Contributions
- Developer Certificate of Origin
- Versions
- Community
- Credits
Features
-
Implements RFC 8288.
-
Parses web link headers into whole value objects for processing and/or inspection.
-
Provides explicit or implicit casting of whole value objects into a web links.
Requirements
-
Ruby.
Setup
To install with security, run:
# 💡 Skip this line if you already have the public certificate installed.
gem cert --add <(curl --compressed --location https://alchemists.io/gems.pem)
gem install rfc-web-link --trust-policy HighSecurityTo install without security, run:
gem install rfc-web-linkYou can also add the gem directly to your project:
bundle add rfc-web-linkOnce the gem is installed, you only need to require it:
require "rfc/web/link"Usage
The fastest way to get started is to create a new instance of the parser that you can reuse upon each HTTP request. Example:
parser = RFC::Web::Link.new "https://demo.io"Then you can use the parser instance to parse HTTP headers and build lists you can interact with:
parser.call({"link" => "</articles>; rel=index"})The above will yield the following set of links (in this case, only with a single link):
#<data RFC::Web::Link::Models::List:0x00000900
links = [
#<data RFC::Web::Link::Models::Link:0x00000920
pairs = [ #<data RFC::Web::Link::Models::Pair:0x00000940
delimiter = "=",
encoding = nil,
key = "rel",
language = nil,
value = "index"
>
],
uri = "https://demo.io/articles"
>
]
>As you can see, you get an immutable Data (whole value) object for which you can directly access all attributes.
Root URI
A root URI (root_uri) must be supplied in order to resolve all relative URIs as absolute URIs. Generally, the root_uri is your host (i.e. primary domain). There are a few ways you can supply the root URI:
Initialization
The following sets the root URI globally so you only have to define it once:
parser = RFC::Web::Link.new "https://demo.io"Call
The following sets the root URI per call.
parser = RFC::Web::Link.new
parser.call({"link" => "</articles>; rel=index"}, root_uri: "https://demo.io")Call (override)
The following sets the root URI globally but also allows you to override the global value with a custom value per call.
parser = RFC::Web::Link.new "https://demo.io"
parser.call({"link" => "</articles>; rel=index"}, root_uri: "https://alt.io")Delimiters
Each delimiter provides the following behavior:
-
Semicolon (
;): Delimits each pair associated with a line. -
Comma (
,): Delimits each line of a link.
They must be quoted when not used as delimiters.
Attributes
There are six RFC 8288 registered target attributes:
-
anchor
-
hreflang
-
media
-
rel
-
title
-
type
Each is described in detail below.
anchor
Optional but, when supplied, can be a single string, quoted with special characters, a relative URI, or an absolute URI. Here’s a few examples:
-
Fragment:
#footer -
Quoted (special characters):
"#overview,body,footer" -
Relative URI:
/policies -
Absolute URI:
https://demo.io/about
Relative URIs will automatically be expanded into an absolute URIs when parsed. This means if the root_uri is https://demo.io and the anchor’s value is /policies (relative), then the resulting URI will be: https://demo.io/polices. Examples:
Fragment
parser = RFC::Web::Link.new "https://demo.io"
list = parser.call({"link" => "</articles>; anchor=#footer"})
# #<data RFC::Web::Link::Models::List:0x000016f0
# links = [
# #<data RFC::Web::Link::Models::Link:0x00001710
# pairs = [
# #<data RFC::Web::Link::Models::Pair:0x00001730
# delimiter = "=",
# encoding = nil,
# key = "anchor",
# language = nil,
# value = "#footer"
# >
# ],
# uri = "https://demo.io/articles"
# >
# ]
>
list.to_s
# "<https://demo.io/articles>; anchor=#footer"Quoted
parser = RFC::Web::Link.new "https://demo.io"
list = parser.call({"link" => %(</articles>; anchor="#overview,body,footer")})
# #<data RFC::Web::Link::Models::List:0x00001660
# links = [
# #<data RFC::Web::Link::Models::Link:0x00001680
# pairs = [
# #<data RFC::Web::Link::Models::Pair:0x000016a0
# delimiter = "=",
# encoding = nil,
# key = "anchor",
# language = nil,
# value = "\"#overview,body,footer\""
# >
# ],
# uri = "https://demo.io/articles"
# >
# ]
# >
list.to_s
# "<https://demo.io/articles>; anchor=\"#overview,body,footer\""Relative URI
parser = RFC::Web::Link.new "https://demo.io"
list = parser.call({"link" => "</articles>; anchor=/policies"})
# #<data RFC::Web::Link::Models::List:0x000017c0
# links = [
# #<data RFC::Web::Link::Models::Link:0x000017e0
# pairs = [
# #<data RFC::Web::Link::Models::Pair:0x00001800
# delimiter = "=",
# encoding = nil,
# key = "anchor",
# language = nil,
# value = "https://demo.io/policies"
# >
# ],
# uri = "https://demo.io/articles"
# >
# ]
# >
list.to_s
# "<https://demo.io/articles>; anchor=https://demo.io/policies"Absolute URI
parser = RFC::Web::Link.new "https://demo.io"
list = parser.call({"link" => "</articles>; anchor=https://demo.io/about"})
# #<data RFC::Web::Link::Models::List:0x00001890
# links = [
# #<data RFC::Web::Link::Models::Link:0x000018b0
# pairs = [
# #<data RFC::Web::Link::Models::Pair:0x000018d0
# delimiter = "=",
# encoding = nil,
# key = "anchor",
# language = nil,
# value = "https://demo.io/about"
# >
# ],
# uri = "https://demo.io/articles"
# >
# ]
# >
list.to_s
# "<https://demo.io/articles>; anchor=https://demo.io/about"hreflang
Optional but, when supplied, is a hint indicating the language of the associated link. This does not override the value of the content-language header. Example:
Single
parser = RFC::Web::Link.new "https://demo.io"
list = parser.call({"link" => "</articles>; hreflang=en"})
# #<data RFC::Web::Link::Models::List:0x00000890
# links = [
# #<data RFC::Web::Link::Models::Link:0x000008b0
# pairs = [
# #<data RFC::Web::Link::Models::Pair:0x000008d0
# delimiter = "=",
# encoding = nil,
# key = "hreflang",
# language = nil,
# value = "en"
# >
# ],
# uri = "https://demo.io/articles"
# >
# ]
# >
list.to_s
# "<https://demo.io/articles>; hreflang=en"Multiple
parser = RFC::Web::Link.new "https://demo.io"
list = parser.call({"link" => "</articles>; hreflang=en; hreflang=de"})
# #<data RFC::Web::Link::Models::List:0x000016c0
# links = [
# #<data RFC::Web::Link::Models::Link:0x000016e0
# pairs = [
# #<data RFC::Web::Link::Models::Pair:0x00001700
# delimiter = "=",
# encoding = nil,
# key = "hreflang",
# language = nil,
# value = "en"
# >,
# #<data RFC::Web::Link::Models::Pair:0x00001740
# delimiter = "=",
# encoding = nil,
# key = "hreflang",
# language = nil,
# value = "de"
# >
# ],
# uri = "https://demo.io/articles"
# >
# ]
# >
list.to_s
# "<https://demo.io/articles>; hreflang=en; hreflang=de"media
Optional but, when supplied, is a hint indicating the kind of media associated with the link. Example:
parser = RFC::Web::Link.new "https://demo.io"
list = parser.call({"link" => "</articles>; media=print"})
# #<data RFC::Web::Link::Models::List:0x000017d0
# links = [
# #<data RFC::Web::Link::Models::Link:0x000017f0
# pairs = [
# #<data RFC::Web::Link::Models::Pair:0x00001810
# delimiter = "=",
# encoding = nil,
# key = "media",
# language = nil,
# value = "print"
# >
# ],
# uri = "https://demo.io/articles"
# >
# ]
# >
list.to_s
# "<https://demo.io/articles>; media=print"rel
Required and must be first defined. Example:
Single
parser = RFC::Web::Link.new "https://demo.io"
list = parser.call({"link" => "</articles>; rel=index"})
# #<data RFC::Web::Link::Models::List:0x00000890
# links = [
# #<data RFC::Web::Link::Models::Link:0x000008b0
# pairs = [
# #<data RFC::Web::Link::Models::Pair:0x000008d0
# delimiter = "=",
# encoding = nil,
# key = "rel",
# language = nil,
# value = "index"
# >
# ],
# uri = "https://demo.io/articles"
# >
# ]
# >
list.to_s
# "<https://demo.io/articles>; rel=index"Shorthand
Short hand, for multiple relations, is supported as well. To use, each relation must be delimited by a space within a single quoted string. Example:
parser = RFC::Web::Link.new "https://demo.io"
list = parser.call({"link" => %(</articles>; rel="one two three")})
# #<data RFC::Web::Link::Models::List:0x00000960
# links = [
# #<data RFC::Web::Link::Models::Link:0x00000980
# pairs = [
# #<data RFC::Web::Link::Models::Pair:0x000009a0
# delimiter = "=",
# encoding = nil,
# key = "rel",
# language = nil,
# value = "one"
# >
# ],
# uri = "https://demo.io/articles"
# >,
# #<data RFC::Web::Link::Models::Link:0x000009f0
# pairs = [
# #<data RFC::Web::Link::Models::Pair:0x00000a10
# delimiter = "=",
# encoding = nil,
# key = "rel",
# language = nil,
# value = "two"
# >
# ],
# uri = "https://demo.io/articles"
# >,
# #<data RFC::Web::Link::Models::Link:0x00000a60
# pairs = [
# #<data RFC::Web::Link::Models::Pair:0x00000a80
# delimiter = "=",
# encoding = nil,
# key = "rel",
# language = nil,
# value = "three"
# >
# ],
# uri = "https://demo.io/articles"
# >
# ]
# >
list.to_s
# "<https://demo.io/articles>; rel=one, <https://demo.io/articles>; rel=two, <https://demo.io/articles>; rel=three"Notice that the original relation short hand of "one two three" produced three unique links for each relation which allows you to type less while still producing three distinct links.
title
Optional but, when supplied, allows you to provide a human readable label for the associated link. This includes being able to encode and decode the value based on the delimiter used. For example, notice the difference in delimiters used below:
-
Plain (
=):title=Demo(example) -
Encoded (
*=):title*=UTF-8'en'd%C3%A9j%C3%A0%20vu(example)
The title must not appear more than once. If multiple occurrences are detected then only the first is honored. Also, if = and *= is used then *= takes precedence. Examples:
Without Encoding
parser = RFC::Web::Link.new "https://demo.io"
list = parser.call({"link" => "</articles>; title=Demo"})
# #<data RFC::Web::Link::Models::List:0x00000890
# links = [
# #<data RFC::Web::Link::Models::Link:0x000008b0
# pairs = [
# #<data RFC::Web::Link::Models::Pair:0x000008d0
# delimiter = "=",
# encoding = nil,
# key = "title",
# language = nil,
# value = "Demo"
# >
# ],
# uri = "https://demo.io/articles"
# >
# ]
# >
list.to_s
# "<https://demo.io/articles>; title=Demo"With Encoding
parser = RFC::Web::Link.new "https://demo.io"
list = parser.call({"link" => "</articles>; title*=UTF-8'en'd%C3%A9j%C3%A0%20vu"})
# #<data RFC::Web::Link::Models::List:0x00000890
# links = [
# #<data RFC::Web::Link::Models::Link:0x000008b0
# pairs = [
# #<data RFC::Web::Link::Models::Pair:0x000008d0
# delimiter = "*=",
# encoding = "UTF-8",
# key = "title",
# language = "en",
# value = "déjà vu"
# >
# ],
# uri = "https://demo.io/articles"
# >
# ]
# >
list.to_s
# "<https://demo.io/articles>; title*=UTF-8'en'd%C3%A9j%C3%A0%20vu"type
Optional but, when supplied, is a hint indicating the MIME Type of the associated link. This does not override the value of the content-type header. Example:
parser = RFC::Web::Link.new "https://demo.io"
list = parser.call({"link" => "</articles>; type=text/html"})
# #<data RFC::Web::Link::Models::List:0x00000890
# links = [
# #<data RFC::Web::Link::Models::Link:0x000008b0
# pairs = [
# #<data RFC::Web::Link::Models::Pair:0x000008d0
# delimiter = "=",
# encoding = nil,
# key = "type",
# language = nil,
# value = "text/html"
# >
# ],
# uri = "https://demo.io/articles"
# >
# ]
# >
list.to_s
# "<https://demo.io/articles>; type=text/html"Extensions
Extensions allow you to extend your implementation beyond the registered target attributes, described above, with your own custom attributes. For example, maybe you want to use the following attributes which are specific to your application: hint, errata, copyright. You can do this as follows:
parser = RFC::Web::Link.new "https://demo.io"
list = parser.call(
{"link" => %(</articles>; hint="A demo."; errata="For demonstration only."; copyright=2025)}
)
# #<data RFC::Web::Link::Models::List:0x00000980
# links = [
# #<data RFC::Web::Link::Models::Link:0x000009a0
# pairs = [
# #<data RFC::Web::Link::Models::Pair:0x000009c0
# delimiter = "=",
# encoding = nil,
# key = "hint",
# language = nil,
# value = "\"A demo.\""
# >,
# #<data RFC::Web::Link::Models::Pair:0x00000a00
# delimiter = "=",
# encoding = nil,
# key = "errata",
# language = nil,
# value = "\"For demonstration only.\""
# >,
# #<data RFC::Web::Link::Models::Pair:0x00000a40
# delimiter = "=",
# encoding = nil,
# key = "copyright",
# language = nil,
# value = "2025"
# >
# ],
# uri = "https://demo.io/articles"
# >
# ]
# >
list.to_s
# "<https://demo.io/articles>; hint=\"A demo.\"; errata=\"For demonstration only.\"; copyright=2025"You can also encode your extensions by using the *= delimiter. Example:
parser = RFC::Web::Link.new "https://demo.io"
list = parser.call({"link" => "</articles>; demo*=UTF-8'en'd%C3%A9j%C3%A0%20vu"})
# #<data RFC::Web::Link::Models::List:0x00000ad0
# links = [
# #<data RFC::Web::Link::Models::Link:0x00000af0
# pairs = [
# #<data RFC::Web::Link::Models::Pair:0x00000b10
# delimiter = "*=",
# encoding = "UTF-8",
# key = "demo",
# language = "en",
# value = "déjà vu"
# >
# ],
# uri = "https://demo.io/articles"
# >
# ]
# >
list.to_s
# "<https://demo.io/articles>; demo*=UTF-8'en'd%C3%A9j%C3%A0%20vu"Models
You’ve already seen all of the models (whole value objects) used in the examples above but this section details what you can do with each model individually.
List
This model has both Data and Enumerable behavior because it encapsulates the list of links parsed from an HTTP link header or created by you. A list can be created multiple ways:
All At Once
list = RFC::Web::Link::Models::List[
links: Set[
RFC::Web::Link::Models::Link[
uri: "https://demo.io",
pairs: Set[
RFC::Web::Link::Models::Pair[key: :title, value: "Demo"]
]
]
]
]💡 Ensure you use a Set when adding your links and pairs.
Separately
pair = RFC::Web::Link::Models::Pair[key: :title, value: "Demo"]
list = RFC::Web::Link::Models::Link[uri: "https://demo.io"].add(pair)
list = RFC::Web::Link::Models::List.new.add(link)Once you have a list, you can send the following messages including Data and Enumerable messages:
list.add line
list.clear
list.empty?
list.size
list.to_s
list.to_strYou’ll notice you can explicitly and implicitly cast your list to a string. This makes the following quite handy:
"link: #{list}"
# link: <https://demo.io>; title=DemoLink
This model encapsulates a single HTTP link header. A link can be created multiple ways:
All At Once
link = RFC::Web::Link::Models::Link[
uri: "https://demo.io",
pairs: Set[
RFC::Web::Link::Models::Pair[key: :title, value: "Demo"]
]
]💡 Ensure you use a Set when adding your pairs.
Separately
pair = RFC::Web::Link::Models::Pair[key: :title, value: "Demo"]
link = RFC::Web::Link::Models::Link[uri: "https://demo.io"].add(pair)Once you have a link, you can then send the following messages (including Data messages):
link.empty?
link.include? pair
link.add pair
link.append :relation, "index"
link.append :title,
"déjà vu",
delimiter: "*=",
encoding: "UTF-8",
language: "en"
link.find_pair key: "title", value: "Demo"
link.find_pair key: "title", value: /dem/i
link.find_pair key: "title", delimiter: /\*=/, value: "Demo"
link.pair? key: "title"
link.pair? key: "title", value: "Demo"
list.to_s
list.to_strYou’ll notice you can explicitly and implicitly cast your link to a string. This makes the following quite handy:
"link: #{link}"
# link: <https://demo.io>; title=Demo"Pair
This models encapsulates a single HTTP link header pair (i.e. key/value). A pair can be created as follows:
Basic
pair = RFC::Web::Link::Models::Pair[key: :title, value: "Demo"]
# #<data RFC::Web::Link::Models::Pair:0x00001e90
# delimiter = "=",
# encoding = nil,
# key = "title",
# language = nil,
# value = "Demo"
# >
pair.encoded? # falseEncoded
pair = RFC::Web::Link::Models::Pair[
key: :title,
delimiter: "*=",
value: "Demo",
encoding: "UTF-8",
language: "en"
]
# #<data RFC::Web::Link::Models::Pair:0x00001ec0
# delimiter = "*=",
# encoding = "UTF-8",
# key = "title",
# language = "en",
# value = "Demo"
# >
pair.encoded? # trueThe key can be either a symbol or a string but is always stored as a string, internally. When using = as your delimiter (default), #encoded? will be false but when using *= as your delimiter, #encoded? will be true.
As with List and Link, you have full access to all Data messages and can explicitly and implicitly cast to a string. Example:
"Pair: #{pair}"
# Pair: title=DemoError Handling
As per RFC 8288, this implementation is designed to safely ignore malformed web links. To illustrate, we’ll start with an initialized parser:
parser = RFC::Web::Link.new "https://demo.io"The following examples show the result of parsing different malformed URIs:
With Nil Value
parser.call({"link" => nil})
# #<data RFC::Web::Link::Models::List:0x00000880
# links = []
# >With Empty Value
parser.call({"link" => ""})
# #<data RFC::Web::Link::Models::List:0x00000880
# links = []
# >With No Leading Less Than Sign For URI
parser.call({"link" => "/articles; rel=index"})
# #<data RFC::Web::Link::Models::List:0x00000880
# links = []
# >Without Attributes
parser.call({"link" => "</articles>"})
# #<data RFC::Web::Link::Models::List:0x00000880
# links = [
# #<data RFC::Web::Link::Models::Link:0x000008b0
# pairs = [],
# uri = "https://demo.io/articles"
# >
# ]
# >With Missing Attribute Value
parser.call({"link" => "</articles>; rel"})
# #<data RFC::Web::Link::Models::List:0x000008a0
# links = [
# #<data RFC::Web::Link::Models::Link:0x00000910
# pairs = [
# #<data RFC::Web::Link::Models::Pair:0x00000930
# delimiter = nil,
# encoding = nil,
# key = "rel",
# language = nil,
# value = nil
# >
# ],
# uri = "https://demo.io/articles"
# >
# ]
# >Development
To contribute, run:
git clone https://github.com/bkuhlmann/rfc-web-link
cd rfc-web-link
bin/setupYou can also use the IRB console for direct access to all objects:
bin/consoleTests
To test, run:
bin/rakeCredits
-
Built with Gemsmith.
-
Engineered by Brooke Kuhlmann.