rfc-web-link 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/LICENSE.adoc +134 -0
- data/README.adoc +948 -0
- data/lib/rfc/web/link/decoder.rb +42 -0
- data/lib/rfc/web/link/encoder.rb +30 -0
- data/lib/rfc/web/link/models/link.rb +38 -0
- data/lib/rfc/web/link/models/list.rb +44 -0
- data/lib/rfc/web/link/models/pair.rb +32 -0
- data/lib/rfc/web/link/parsers/header.rb +26 -0
- data/lib/rfc/web/link/parsers/line.rb +54 -0
- data/lib/rfc/web/link/parsers/list.rb +85 -0
- data/lib/rfc/web/link/parsers/pair.rb +36 -0
- data/lib/rfc/web/link.rb +20 -0
- data/rfc-web-link.gemspec +29 -0
- data.tar.gz.sig +0 -0
- metadata +87 -0
- metadata.gz.sig +0 -0
data/README.adoc
ADDED
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
:toc: macro
|
|
2
|
+
:toclevels: 5
|
|
3
|
+
:figure-caption!:
|
|
4
|
+
|
|
5
|
+
:data_link: link:https://alchemists.io/articles/ruby_data[Data]
|
|
6
|
+
:rfc_8288_link: link:https://www.rfc-editor.org/info/rfc8288[RFC 8288]
|
|
7
|
+
|
|
8
|
+
= RFC Web Link
|
|
9
|
+
|
|
10
|
+
RFC Web Link implements {rfc_8288_link}: 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:
|
|
11
|
+
|
|
12
|
+
----
|
|
13
|
+
link: <https://demo.io/articles?page=1>; rel="previous"; title="Previous",
|
|
14
|
+
<https://demo.io/articles?page=3>; rel="next"; title="Next"
|
|
15
|
+
----
|
|
16
|
+
|
|
17
|
+
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.
|
|
18
|
+
|
|
19
|
+
toc::[]
|
|
20
|
+
|
|
21
|
+
== Features
|
|
22
|
+
|
|
23
|
+
* Implements {rfc_8288_link}.
|
|
24
|
+
* Parses web link headers into whole value objects for processing and/or inspection.
|
|
25
|
+
* Allows explicit or implicit casting of whole value objects into a web links.
|
|
26
|
+
|
|
27
|
+
== Requirements
|
|
28
|
+
|
|
29
|
+
. link:https://www.ruby-lang.org[Ruby].
|
|
30
|
+
|
|
31
|
+
== Setup
|
|
32
|
+
|
|
33
|
+
To install _with_ security, run:
|
|
34
|
+
|
|
35
|
+
[source,bash]
|
|
36
|
+
----
|
|
37
|
+
# 💡 Skip this line if you already have the public certificate installed.
|
|
38
|
+
gem cert --add <(curl --compressed --location https://alchemists.io/gems.pem)
|
|
39
|
+
gem install rfc-web-link --trust-policy HighSecurity
|
|
40
|
+
----
|
|
41
|
+
|
|
42
|
+
To install _without_ security, run:
|
|
43
|
+
|
|
44
|
+
[source,bash]
|
|
45
|
+
----
|
|
46
|
+
gem install rfc-web-link
|
|
47
|
+
----
|
|
48
|
+
|
|
49
|
+
You can also add the gem directly to your project:
|
|
50
|
+
|
|
51
|
+
[source,bash]
|
|
52
|
+
----
|
|
53
|
+
bundle add rfc-web-link
|
|
54
|
+
----
|
|
55
|
+
|
|
56
|
+
Once the gem is installed, you only need to require it:
|
|
57
|
+
|
|
58
|
+
[source,ruby]
|
|
59
|
+
----
|
|
60
|
+
require "rfc/web/link"
|
|
61
|
+
----
|
|
62
|
+
|
|
63
|
+
== Usage
|
|
64
|
+
|
|
65
|
+
The fastest way to get started is to create a new instance of the parser that you can reuse upon each HTTP request. Example:
|
|
66
|
+
|
|
67
|
+
[source,ruby]
|
|
68
|
+
----
|
|
69
|
+
parser = RFC::Web::Link.new
|
|
70
|
+
----
|
|
71
|
+
|
|
72
|
+
Then you can use the `parser` instance to parse HTTP headers and build lists you can interact with:
|
|
73
|
+
|
|
74
|
+
[source,ruby]
|
|
75
|
+
----
|
|
76
|
+
parser.call({"link" => "</articles>; rel=index"}, root_uri: "https://demo.io")
|
|
77
|
+
----
|
|
78
|
+
|
|
79
|
+
The above will yield the following set of links (in this case, only with a single link):
|
|
80
|
+
|
|
81
|
+
[source,ruby]
|
|
82
|
+
----
|
|
83
|
+
#<data RFC::Web::Link::Models::List:0x00000900
|
|
84
|
+
links = [
|
|
85
|
+
#<data RFC::Web::Link::Models::Link:0x00000920
|
|
86
|
+
pairs = [ #<data RFC::Web::Link::Models::Pair:0x00000940
|
|
87
|
+
delimiter = "=",
|
|
88
|
+
encoding = nil,
|
|
89
|
+
key = "rel",
|
|
90
|
+
language = nil,
|
|
91
|
+
value = "index"
|
|
92
|
+
>
|
|
93
|
+
],
|
|
94
|
+
uri = "https://demo.io/articles"
|
|
95
|
+
>
|
|
96
|
+
]
|
|
97
|
+
>
|
|
98
|
+
----
|
|
99
|
+
|
|
100
|
+
As you can see, you get an immutable {data_link} whole value object for which you can directly access all attributes.
|
|
101
|
+
|
|
102
|
+
=== Root URI
|
|
103
|
+
|
|
104
|
+
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).
|
|
105
|
+
|
|
106
|
+
=== Delimiters
|
|
107
|
+
|
|
108
|
+
When not used for delimiting link links and/or pairs (attributes), these need to in quotes when used as values for your attributes. Here's the breakdown:
|
|
109
|
+
|
|
110
|
+
* *Semicolon* (`;`): Used to delimit each pair associated with a line.
|
|
111
|
+
* *Comma* (`,`): Used to delimit each line of a link.
|
|
112
|
+
|
|
113
|
+
=== Attributes
|
|
114
|
+
|
|
115
|
+
There are _six_ {rfc_8288_link} registered target attributes: anchor, hreflang, media, rel, title, and type. The key map between the specification and this implementation's `RFC::Web::Link::Models::Pair` model is shown below:
|
|
116
|
+
|
|
117
|
+
[options="header"]
|
|
118
|
+
|===
|
|
119
|
+
| Specification | Implementation
|
|
120
|
+
| anchor | anchor
|
|
121
|
+
| hreflang | language
|
|
122
|
+
| media | media
|
|
123
|
+
| rel | relation
|
|
124
|
+
| title | title
|
|
125
|
+
| type | type
|
|
126
|
+
|===
|
|
127
|
+
|
|
128
|
+
You can think of `language` as an alias to `hreflang` and `relation` as an alias to `rel` for improved readability. Both work but the later are preferred. Each attribute is described in further detail below.
|
|
129
|
+
|
|
130
|
+
==== Anchor
|
|
131
|
+
|
|
132
|
+
An anchor is 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:
|
|
133
|
+
|
|
134
|
+
* *Fragment*: `#footer`
|
|
135
|
+
* *Quoted (special characters)*: `"#overview,body,footer"`
|
|
136
|
+
* *Relative URI*: `/policies`
|
|
137
|
+
* *Absolute URI*: `https://demo.io/about`
|
|
138
|
+
|
|
139
|
+
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:
|
|
140
|
+
|
|
141
|
+
*Fragment*
|
|
142
|
+
|
|
143
|
+
[source,ruby]
|
|
144
|
+
----
|
|
145
|
+
list = RFC::Web::Link.new.call(
|
|
146
|
+
{"link" => "</articles>; anchor=#footer"},
|
|
147
|
+
root_uri: "https://demo.io"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# #<data RFC::Web::Link::Models::List:0x000016f0
|
|
151
|
+
# links = [
|
|
152
|
+
# #<data RFC::Web::Link::Models::Link:0x00001710
|
|
153
|
+
# pairs = [
|
|
154
|
+
# #<data RFC::Web::Link::Models::Pair:0x00001730
|
|
155
|
+
# delimiter = "=",
|
|
156
|
+
# encoding = nil,
|
|
157
|
+
# key = "anchor",
|
|
158
|
+
# language = nil,
|
|
159
|
+
# value = "#footer"
|
|
160
|
+
# >
|
|
161
|
+
# ],
|
|
162
|
+
# uri = "https://demo.io/articles"
|
|
163
|
+
# >
|
|
164
|
+
# ]
|
|
165
|
+
>
|
|
166
|
+
|
|
167
|
+
list.to_s
|
|
168
|
+
|
|
169
|
+
# "<https://demo.io/articles>; anchor=#footer"
|
|
170
|
+
----
|
|
171
|
+
|
|
172
|
+
*Quoted*
|
|
173
|
+
|
|
174
|
+
[source,ruby]
|
|
175
|
+
----
|
|
176
|
+
list = RFC::Web::Link.new.call(
|
|
177
|
+
{"link" => %(</articles>; anchor="#overview,body,footer")},
|
|
178
|
+
root_uri: "https://demo.io"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# #<data RFC::Web::Link::Models::List:0x00001660
|
|
182
|
+
# links = [
|
|
183
|
+
# #<data RFC::Web::Link::Models::Link:0x00001680
|
|
184
|
+
# pairs = [
|
|
185
|
+
# #<data RFC::Web::Link::Models::Pair:0x000016a0
|
|
186
|
+
# delimiter = "=",
|
|
187
|
+
# encoding = nil,
|
|
188
|
+
# key = "anchor",
|
|
189
|
+
# language = nil,
|
|
190
|
+
# value = "\"#overview,body,footer\""
|
|
191
|
+
# >
|
|
192
|
+
# ],
|
|
193
|
+
# uri = "https://demo.io/articles"
|
|
194
|
+
# >
|
|
195
|
+
# ]
|
|
196
|
+
# >
|
|
197
|
+
|
|
198
|
+
list.to_s
|
|
199
|
+
|
|
200
|
+
# "<https://demo.io/articles>; anchor=\"#overview,body,footer\""
|
|
201
|
+
----
|
|
202
|
+
|
|
203
|
+
*Relative URI*
|
|
204
|
+
|
|
205
|
+
[source,ruby]
|
|
206
|
+
----
|
|
207
|
+
list = RFC::Web::Link.new.call(
|
|
208
|
+
{"link" => "</articles>; anchor=/policies"},
|
|
209
|
+
root_uri: "https://demo.io"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# #<data RFC::Web::Link::Models::List:0x000017c0
|
|
213
|
+
# links = [
|
|
214
|
+
# #<data RFC::Web::Link::Models::Link:0x000017e0
|
|
215
|
+
# pairs = [
|
|
216
|
+
# #<data RFC::Web::Link::Models::Pair:0x00001800
|
|
217
|
+
# delimiter = "=",
|
|
218
|
+
# encoding = nil,
|
|
219
|
+
# key = "anchor",
|
|
220
|
+
# language = nil,
|
|
221
|
+
# value = "https://demo.io/policies"
|
|
222
|
+
# >
|
|
223
|
+
# ],
|
|
224
|
+
# uri = "https://demo.io/articles"
|
|
225
|
+
# >
|
|
226
|
+
# ]
|
|
227
|
+
# >
|
|
228
|
+
|
|
229
|
+
list.to_s
|
|
230
|
+
|
|
231
|
+
# "<https://demo.io/articles>; anchor=https://demo.io/policies"
|
|
232
|
+
----
|
|
233
|
+
|
|
234
|
+
*Absolute URI*
|
|
235
|
+
|
|
236
|
+
[source,ruby]
|
|
237
|
+
----
|
|
238
|
+
list = RFC::Web::Link.new.call(
|
|
239
|
+
{"link" => "</articles>; anchor=https://demo.io/about"},
|
|
240
|
+
root_uri: "https://demo.io"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# #<data RFC::Web::Link::Models::List:0x00001890
|
|
244
|
+
# links = [
|
|
245
|
+
# #<data RFC::Web::Link::Models::Link:0x000018b0
|
|
246
|
+
# pairs = [
|
|
247
|
+
# #<data RFC::Web::Link::Models::Pair:0x000018d0
|
|
248
|
+
# delimiter = "=",
|
|
249
|
+
# encoding = nil,
|
|
250
|
+
# key = "anchor",
|
|
251
|
+
# language = nil,
|
|
252
|
+
# value = "https://demo.io/about"
|
|
253
|
+
# >
|
|
254
|
+
# ],
|
|
255
|
+
# uri = "https://demo.io/articles"
|
|
256
|
+
# >
|
|
257
|
+
# ]
|
|
258
|
+
# >
|
|
259
|
+
|
|
260
|
+
list.to_s
|
|
261
|
+
|
|
262
|
+
# "<https://demo.io/articles>; anchor=https://demo.io/about"
|
|
263
|
+
----
|
|
264
|
+
|
|
265
|
+
==== Language
|
|
266
|
+
|
|
267
|
+
Language is 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:
|
|
268
|
+
|
|
269
|
+
*Single*
|
|
270
|
+
|
|
271
|
+
[source,ruby]
|
|
272
|
+
----
|
|
273
|
+
list = RFC::Web::Link.new.call(
|
|
274
|
+
{"link" => "</articles>; hreflang=en"},
|
|
275
|
+
root_uri: "https://demo.io"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# #<data RFC::Web::Link::Models::List:0x00000890
|
|
279
|
+
# links = [
|
|
280
|
+
# #<data RFC::Web::Link::Models::Link:0x000008b0
|
|
281
|
+
# pairs = [
|
|
282
|
+
# #<data RFC::Web::Link::Models::Pair:0x000008d0
|
|
283
|
+
# delimiter = "=",
|
|
284
|
+
# encoding = nil,
|
|
285
|
+
# key = "hreflang",
|
|
286
|
+
# language = nil,
|
|
287
|
+
# value = "en"
|
|
288
|
+
# >
|
|
289
|
+
# ],
|
|
290
|
+
# uri = "https://demo.io/articles"
|
|
291
|
+
# >
|
|
292
|
+
# ]
|
|
293
|
+
# >
|
|
294
|
+
|
|
295
|
+
list.to_s
|
|
296
|
+
|
|
297
|
+
# "<https://demo.io/articles>; hreflang=en"
|
|
298
|
+
----
|
|
299
|
+
|
|
300
|
+
*Multiple*
|
|
301
|
+
|
|
302
|
+
[source,ruby]
|
|
303
|
+
----
|
|
304
|
+
list = RFC::Web::Link.new.call(
|
|
305
|
+
{"link" => "</articles>; hreflang=en; hreflang=de"},
|
|
306
|
+
root_uri: "https://demo.io"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# #<data RFC::Web::Link::Models::List:0x000016c0
|
|
310
|
+
# links = [
|
|
311
|
+
# #<data RFC::Web::Link::Models::Link:0x000016e0
|
|
312
|
+
# pairs = [
|
|
313
|
+
# #<data RFC::Web::Link::Models::Pair:0x00001700
|
|
314
|
+
# delimiter = "=",
|
|
315
|
+
# encoding = nil,
|
|
316
|
+
# key = "hreflang",
|
|
317
|
+
# language = nil,
|
|
318
|
+
# value = "en"
|
|
319
|
+
# >,
|
|
320
|
+
# #<data RFC::Web::Link::Models::Pair:0x00001740
|
|
321
|
+
# delimiter = "=",
|
|
322
|
+
# encoding = nil,
|
|
323
|
+
# key = "hreflang",
|
|
324
|
+
# language = nil,
|
|
325
|
+
# value = "de"
|
|
326
|
+
# >
|
|
327
|
+
# ],
|
|
328
|
+
# uri = "https://demo.io/articles"
|
|
329
|
+
# >
|
|
330
|
+
# ]
|
|
331
|
+
# >
|
|
332
|
+
|
|
333
|
+
list.to_s
|
|
334
|
+
|
|
335
|
+
# "<https://demo.io/articles>; hreflang=en; hreflang=de"
|
|
336
|
+
----
|
|
337
|
+
|
|
338
|
+
==== Media
|
|
339
|
+
|
|
340
|
+
Media is optional but, when supplied, is a hint indicating the kind of media associated with the link. Example:
|
|
341
|
+
|
|
342
|
+
[source,ruby]
|
|
343
|
+
----
|
|
344
|
+
list = RFC::Web::Link.new.call(
|
|
345
|
+
{"link" => "</articles>; media=print"},
|
|
346
|
+
root_uri: "https://demo.io"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# #<data RFC::Web::Link::Models::List:0x000017d0
|
|
350
|
+
# links = [
|
|
351
|
+
# #<data RFC::Web::Link::Models::Link:0x000017f0
|
|
352
|
+
# pairs = [
|
|
353
|
+
# #<data RFC::Web::Link::Models::Pair:0x00001810
|
|
354
|
+
# delimiter = "=",
|
|
355
|
+
# encoding = nil,
|
|
356
|
+
# key = "media",
|
|
357
|
+
# language = nil,
|
|
358
|
+
# value = "print"
|
|
359
|
+
# >
|
|
360
|
+
# ],
|
|
361
|
+
# uri = "https://demo.io/articles"
|
|
362
|
+
# >
|
|
363
|
+
# ]
|
|
364
|
+
# >
|
|
365
|
+
|
|
366
|
+
list.to_s
|
|
367
|
+
|
|
368
|
+
# "<https://demo.io/articles>; media=print"
|
|
369
|
+
----
|
|
370
|
+
|
|
371
|
+
==== Relation
|
|
372
|
+
|
|
373
|
+
A relation is the only attribute that is required by the specification and should be the first one defined. Example:
|
|
374
|
+
|
|
375
|
+
*Single*
|
|
376
|
+
|
|
377
|
+
[source,ruby]
|
|
378
|
+
----
|
|
379
|
+
list = RFC::Web::Link.new.call(
|
|
380
|
+
{"link" => "</articles>; relation=index"},
|
|
381
|
+
root_uri: "https://demo.io"
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# #<data RFC::Web::Link::Models::List:0x00000890
|
|
385
|
+
# links = [
|
|
386
|
+
# #<data RFC::Web::Link::Models::Link:0x000008b0
|
|
387
|
+
# pairs = [
|
|
388
|
+
# #<data RFC::Web::Link::Models::Pair:0x000008d0
|
|
389
|
+
# delimiter = "=",
|
|
390
|
+
# encoding = nil,
|
|
391
|
+
# key = "relation",
|
|
392
|
+
# language = nil,
|
|
393
|
+
# value = "index"
|
|
394
|
+
# >
|
|
395
|
+
# ],
|
|
396
|
+
# uri = "https://demo.io/articles"
|
|
397
|
+
# >
|
|
398
|
+
# ]
|
|
399
|
+
# >
|
|
400
|
+
|
|
401
|
+
list.to_s
|
|
402
|
+
|
|
403
|
+
# "<https://demo.io/articles>; rel=index"
|
|
404
|
+
----
|
|
405
|
+
|
|
406
|
+
*Shorthand*
|
|
407
|
+
|
|
408
|
+
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:
|
|
409
|
+
|
|
410
|
+
[source,ruby]
|
|
411
|
+
----
|
|
412
|
+
list = RFC::Web::Link.new.call(
|
|
413
|
+
{"link" => %(</articles>; rel="one two three")},
|
|
414
|
+
root_uri: "https://demo.io"
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
# #<data RFC::Web::Link::Models::List:0x00000960
|
|
418
|
+
# links = [
|
|
419
|
+
# #<data RFC::Web::Link::Models::Link:0x00000980
|
|
420
|
+
# pairs = [
|
|
421
|
+
# #<data RFC::Web::Link::Models::Pair:0x000009a0
|
|
422
|
+
# delimiter = "=",
|
|
423
|
+
# encoding = nil,
|
|
424
|
+
# key = "rel",
|
|
425
|
+
# language = nil,
|
|
426
|
+
# value = "one"
|
|
427
|
+
# >
|
|
428
|
+
# ],
|
|
429
|
+
# uri = "https://demo.io/articles"
|
|
430
|
+
# >,
|
|
431
|
+
# #<data RFC::Web::Link::Models::Link:0x000009f0
|
|
432
|
+
# pairs = [
|
|
433
|
+
# #<data RFC::Web::Link::Models::Pair:0x00000a10
|
|
434
|
+
# delimiter = "=",
|
|
435
|
+
# encoding = nil,
|
|
436
|
+
# key = "rel",
|
|
437
|
+
# language = nil,
|
|
438
|
+
# value = "two"
|
|
439
|
+
# >
|
|
440
|
+
# ],
|
|
441
|
+
# uri = "https://demo.io/articles"
|
|
442
|
+
# >,
|
|
443
|
+
# #<data RFC::Web::Link::Models::Link:0x00000a60
|
|
444
|
+
# pairs = [
|
|
445
|
+
# #<data RFC::Web::Link::Models::Pair:0x00000a80
|
|
446
|
+
# delimiter = "=",
|
|
447
|
+
# encoding = nil,
|
|
448
|
+
# key = "rel",
|
|
449
|
+
# language = nil,
|
|
450
|
+
# value = "three"
|
|
451
|
+
# >
|
|
452
|
+
# ],
|
|
453
|
+
# uri = "https://demo.io/articles"
|
|
454
|
+
# >
|
|
455
|
+
# ]
|
|
456
|
+
# >
|
|
457
|
+
|
|
458
|
+
list.to_s
|
|
459
|
+
|
|
460
|
+
# "<https://demo.io/articles>; rel=one, <https://demo.io/articles>; rel=two, <https://demo.io/articles>; rel=three"
|
|
461
|
+
----
|
|
462
|
+
|
|
463
|
+
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.
|
|
464
|
+
|
|
465
|
+
==== Title
|
|
466
|
+
|
|
467
|
+
A title is 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:
|
|
468
|
+
|
|
469
|
+
* *Plain* (`=`): `title=Demo` (example)
|
|
470
|
+
* *Encoded* (`*=`): `+title*=UTF-8'en'd%C3%A9j%C3%A0%20vu+` (example)
|
|
471
|
+
|
|
472
|
+
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:
|
|
473
|
+
|
|
474
|
+
*Without Encoding*
|
|
475
|
+
|
|
476
|
+
[source,ruby]
|
|
477
|
+
----
|
|
478
|
+
list = RFC::Web::Link.new.call(
|
|
479
|
+
{"link" => "</articles>; title=Demo"},
|
|
480
|
+
root_uri: "https://demo.io"
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
# #<data RFC::Web::Link::Models::List:0x00000890
|
|
484
|
+
# links = [
|
|
485
|
+
# #<data RFC::Web::Link::Models::Link:0x000008b0
|
|
486
|
+
# pairs = [
|
|
487
|
+
# #<data RFC::Web::Link::Models::Pair:0x000008d0
|
|
488
|
+
# delimiter = "=",
|
|
489
|
+
# encoding = nil,
|
|
490
|
+
# key = "title",
|
|
491
|
+
# language = nil,
|
|
492
|
+
# value = "Demo"
|
|
493
|
+
# >
|
|
494
|
+
# ],
|
|
495
|
+
# uri = "https://demo.io/articles"
|
|
496
|
+
# >
|
|
497
|
+
# ]
|
|
498
|
+
# >
|
|
499
|
+
|
|
500
|
+
list.to_s
|
|
501
|
+
|
|
502
|
+
# "<https://demo.io/articles>; title=Demo"
|
|
503
|
+
----
|
|
504
|
+
|
|
505
|
+
*With Encoding*
|
|
506
|
+
|
|
507
|
+
[source,ruby]
|
|
508
|
+
----
|
|
509
|
+
list = RFC::Web::Link.new.call(
|
|
510
|
+
{"link" => "</articles>; title*=UTF-8'en'd%C3%A9j%C3%A0%20vu"},
|
|
511
|
+
root_uri: "https://demo.io"
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
# #<data RFC::Web::Link::Models::List:0x00000890
|
|
515
|
+
# links = [
|
|
516
|
+
# #<data RFC::Web::Link::Models::Link:0x000008b0
|
|
517
|
+
# pairs = [
|
|
518
|
+
# #<data RFC::Web::Link::Models::Pair:0x000008d0
|
|
519
|
+
# delimiter = "*=",
|
|
520
|
+
# encoding = "UTF-8",
|
|
521
|
+
# key = "title",
|
|
522
|
+
# language = "en",
|
|
523
|
+
# value = "déjà vu"
|
|
524
|
+
# >
|
|
525
|
+
# ],
|
|
526
|
+
# uri = "https://demo.io/articles"
|
|
527
|
+
# >
|
|
528
|
+
# ]
|
|
529
|
+
# >
|
|
530
|
+
|
|
531
|
+
list.to_s
|
|
532
|
+
|
|
533
|
+
# "<https://demo.io/articles>; title*=UTF-8'en'd%C3%A9j%C3%A0%20vu"
|
|
534
|
+
----
|
|
535
|
+
|
|
536
|
+
==== Type
|
|
537
|
+
|
|
538
|
+
Type is 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:
|
|
539
|
+
|
|
540
|
+
[source,ruby]
|
|
541
|
+
----
|
|
542
|
+
list = RFC::Web::Link.new.call(
|
|
543
|
+
{"link" => "</articles>; type=text/html"},
|
|
544
|
+
root_uri: "https://demo.io"
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
# #<data RFC::Web::Link::Models::List:0x00000890
|
|
548
|
+
# links = [
|
|
549
|
+
# #<data RFC::Web::Link::Models::Link:0x000008b0
|
|
550
|
+
# pairs = [
|
|
551
|
+
# #<data RFC::Web::Link::Models::Pair:0x000008d0
|
|
552
|
+
# delimiter = "=",
|
|
553
|
+
# encoding = nil,
|
|
554
|
+
# key = "type",
|
|
555
|
+
# language = nil,
|
|
556
|
+
# value = "text/html"
|
|
557
|
+
# >
|
|
558
|
+
# ],
|
|
559
|
+
# uri = "https://demo.io/articles"
|
|
560
|
+
# >
|
|
561
|
+
# ]
|
|
562
|
+
# >
|
|
563
|
+
|
|
564
|
+
list.to_s
|
|
565
|
+
|
|
566
|
+
# "<https://demo.io/articles>; type=text/html"
|
|
567
|
+
----
|
|
568
|
+
|
|
569
|
+
=== Extensions
|
|
570
|
+
|
|
571
|
+
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:
|
|
572
|
+
|
|
573
|
+
[source,ruby]
|
|
574
|
+
----
|
|
575
|
+
list = RFC::Web::Link.new.call(
|
|
576
|
+
{"link" => %(</articles>; hint="A demo."; errata="For demonstration only."; copyright=2025)},
|
|
577
|
+
root_uri: "https://demo.io"
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
# #<data RFC::Web::Link::Models::List:0x00000980
|
|
581
|
+
# links = [
|
|
582
|
+
# #<data RFC::Web::Link::Models::Link:0x000009a0
|
|
583
|
+
# pairs = [
|
|
584
|
+
# #<data RFC::Web::Link::Models::Pair:0x000009c0
|
|
585
|
+
# delimiter = "=",
|
|
586
|
+
# encoding = nil,
|
|
587
|
+
# key = "hint",
|
|
588
|
+
# language = nil,
|
|
589
|
+
# value = "\"A demo.\""
|
|
590
|
+
# >,
|
|
591
|
+
# #<data RFC::Web::Link::Models::Pair:0x00000a00
|
|
592
|
+
# delimiter = "=",
|
|
593
|
+
# encoding = nil,
|
|
594
|
+
# key = "errata",
|
|
595
|
+
# language = nil,
|
|
596
|
+
# value = "\"For demonstration only.\""
|
|
597
|
+
# >,
|
|
598
|
+
# #<data RFC::Web::Link::Models::Pair:0x00000a40
|
|
599
|
+
# delimiter = "=",
|
|
600
|
+
# encoding = nil,
|
|
601
|
+
# key = "copyright",
|
|
602
|
+
# language = nil,
|
|
603
|
+
# value = "2025"
|
|
604
|
+
# >
|
|
605
|
+
# ],
|
|
606
|
+
# uri = "https://demo.io/articles"
|
|
607
|
+
# >
|
|
608
|
+
# ]
|
|
609
|
+
# >
|
|
610
|
+
|
|
611
|
+
list.to_s
|
|
612
|
+
|
|
613
|
+
# "<https://demo.io/articles>; hint=\"A demo.\"; errata=\"For demonstration only.\"; copyright=2025"
|
|
614
|
+
----
|
|
615
|
+
|
|
616
|
+
You can also encode your extensions by using the `+*=+` delimiter. Example:
|
|
617
|
+
|
|
618
|
+
[source,ruby]
|
|
619
|
+
----
|
|
620
|
+
list = RFC::Web::Link.new.call(
|
|
621
|
+
{"link" => "</articles>; demo*=UTF-8'en'd%C3%A9j%C3%A0%20vu"},
|
|
622
|
+
root_uri: "https://demo.io"
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
# #<data RFC::Web::Link::Models::List:0x00000ad0
|
|
626
|
+
# links = [
|
|
627
|
+
# #<data RFC::Web::Link::Models::Link:0x00000af0
|
|
628
|
+
# pairs = [
|
|
629
|
+
# #<data RFC::Web::Link::Models::Pair:0x00000b10
|
|
630
|
+
# delimiter = "*=",
|
|
631
|
+
# encoding = "UTF-8",
|
|
632
|
+
# key = "demo",
|
|
633
|
+
# language = "en",
|
|
634
|
+
# value = "déjà vu"
|
|
635
|
+
# >
|
|
636
|
+
# ],
|
|
637
|
+
# uri = "https://demo.io/articles"
|
|
638
|
+
# >
|
|
639
|
+
# ]
|
|
640
|
+
# >
|
|
641
|
+
|
|
642
|
+
list.to_s
|
|
643
|
+
|
|
644
|
+
# "<https://demo.io/articles>; demo*=UTF-8'en'd%C3%A9j%C3%A0%20vu"
|
|
645
|
+
----
|
|
646
|
+
|
|
647
|
+
=== Models
|
|
648
|
+
|
|
649
|
+
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.
|
|
650
|
+
|
|
651
|
+
==== List
|
|
652
|
+
|
|
653
|
+
This model encapsulates the list of links parsed from an HTTP link header or created by you. A list can be created multiple ways:
|
|
654
|
+
|
|
655
|
+
*All At Once*
|
|
656
|
+
|
|
657
|
+
[source,ruby]
|
|
658
|
+
----
|
|
659
|
+
list = RFC::Web::Link::Models::List[
|
|
660
|
+
links: Set[
|
|
661
|
+
RFC::Web::Link::Models::Link[
|
|
662
|
+
uri: "https://demo.io",
|
|
663
|
+
pairs: Set[
|
|
664
|
+
RFC::Web::Link::Models::Pair[key: :title, value: "Demo"]
|
|
665
|
+
]
|
|
666
|
+
]
|
|
667
|
+
]
|
|
668
|
+
]
|
|
669
|
+
----
|
|
670
|
+
|
|
671
|
+
💡 Ensure you use a `Set` when adding your links and pairs.
|
|
672
|
+
|
|
673
|
+
*Separately*
|
|
674
|
+
|
|
675
|
+
[source,ruby]
|
|
676
|
+
----
|
|
677
|
+
pair = RFC::Web::Link::Models::Pair[key: :title, value: "Demo"]
|
|
678
|
+
list = RFC::Web::Link::Models::Link[uri: "https://demo.io"].add(pair)
|
|
679
|
+
list = RFC::Web::Link::Models::List.new.add(link)
|
|
680
|
+
----
|
|
681
|
+
|
|
682
|
+
Once you have a list, you can send the following messages (including {data_link} messages) along limited enumerable messages:
|
|
683
|
+
|
|
684
|
+
[source,ruby]
|
|
685
|
+
----
|
|
686
|
+
list.add line
|
|
687
|
+
list.all?
|
|
688
|
+
list.any?
|
|
689
|
+
list.clear
|
|
690
|
+
list.each
|
|
691
|
+
list.empty?
|
|
692
|
+
list.find
|
|
693
|
+
list.include? line
|
|
694
|
+
list.map
|
|
695
|
+
list.none?
|
|
696
|
+
list.one?
|
|
697
|
+
list.reject
|
|
698
|
+
list.select
|
|
699
|
+
list.size
|
|
700
|
+
list.to_s
|
|
701
|
+
list.to_str
|
|
702
|
+
----
|
|
703
|
+
|
|
704
|
+
You'll notice you can explicitly and implicitly cast your list to a string. This makes the following quite handy:
|
|
705
|
+
|
|
706
|
+
[source,ruby]
|
|
707
|
+
----
|
|
708
|
+
"link: #{list}"
|
|
709
|
+
# link: <https://demo.io>; title=Demo
|
|
710
|
+
----
|
|
711
|
+
|
|
712
|
+
==== Link
|
|
713
|
+
|
|
714
|
+
This model encapsulates a single HTTP link header. A link can be created multiple ways:
|
|
715
|
+
|
|
716
|
+
*All At Once*
|
|
717
|
+
|
|
718
|
+
[source,ruby]
|
|
719
|
+
----
|
|
720
|
+
list = RFC::Web::Link::Models::Link[
|
|
721
|
+
uri: "https://demo.io",
|
|
722
|
+
pairs: Set[
|
|
723
|
+
RFC::Web::Link::Models::Pair[key: :title, value: "Demo"]
|
|
724
|
+
]
|
|
725
|
+
]
|
|
726
|
+
----
|
|
727
|
+
|
|
728
|
+
💡 Ensure you use a `Set` when adding your pairs.
|
|
729
|
+
|
|
730
|
+
*Separately*
|
|
731
|
+
|
|
732
|
+
[source,ruby]
|
|
733
|
+
----
|
|
734
|
+
pair = RFC::Web::Link::Models::Pair[key: :title, value: "Demo"]
|
|
735
|
+
list = RFC::Web::Link::Models::Link[uri: "https://demo.io"].add(pair)
|
|
736
|
+
----
|
|
737
|
+
|
|
738
|
+
Once you have a link, you can then send the following messages (including {data_link} messages):
|
|
739
|
+
|
|
740
|
+
[source,ruby]
|
|
741
|
+
----
|
|
742
|
+
link.empty?
|
|
743
|
+
link.include? pair
|
|
744
|
+
link.add pair
|
|
745
|
+
link.append :relation, "index"
|
|
746
|
+
link.append :title,
|
|
747
|
+
"déjà vu",
|
|
748
|
+
delimiter: "*=",
|
|
749
|
+
encoding: "UTF-8",
|
|
750
|
+
language: "en"
|
|
751
|
+
link.has? :title
|
|
752
|
+
link.has? "title"
|
|
753
|
+
list.to_s
|
|
754
|
+
list.to_str
|
|
755
|
+
----
|
|
756
|
+
|
|
757
|
+
You'll notice you can explicitly and implicitly cast your link to a string. This makes the following quite handy:
|
|
758
|
+
|
|
759
|
+
[source,ruby]
|
|
760
|
+
----
|
|
761
|
+
"link: #{link}"
|
|
762
|
+
# link: <https://demo.io>; title=Demo"
|
|
763
|
+
----
|
|
764
|
+
|
|
765
|
+
==== Pair
|
|
766
|
+
|
|
767
|
+
This models encapsulates a single HTTP link header pair (i.e. key/value). A pair can be created as follows:
|
|
768
|
+
|
|
769
|
+
*Basic*
|
|
770
|
+
|
|
771
|
+
[source,ruby]
|
|
772
|
+
----
|
|
773
|
+
pair = RFC::Web::Link::Models::Pair[key: :title, value: "Demo"]
|
|
774
|
+
|
|
775
|
+
# #<data RFC::Web::Link::Models::Pair:0x00001e90
|
|
776
|
+
# delimiter = "=",
|
|
777
|
+
# encoding = nil,
|
|
778
|
+
# key = "title",
|
|
779
|
+
# language = nil,
|
|
780
|
+
# value = "Demo"
|
|
781
|
+
# >
|
|
782
|
+
|
|
783
|
+
pair.encoded? # false
|
|
784
|
+
----
|
|
785
|
+
|
|
786
|
+
*Encoded*
|
|
787
|
+
|
|
788
|
+
[source,ruby]
|
|
789
|
+
----
|
|
790
|
+
pair = RFC::Web::Link::Models::Pair[
|
|
791
|
+
key: :title,
|
|
792
|
+
delimiter: "*=",
|
|
793
|
+
value: "Demo",
|
|
794
|
+
encoding: "UTF-8",
|
|
795
|
+
language: "en"
|
|
796
|
+
]
|
|
797
|
+
|
|
798
|
+
# #<data RFC::Web::Link::Models::Pair:0x00001ec0
|
|
799
|
+
# delimiter = "*=",
|
|
800
|
+
# encoding = "UTF-8",
|
|
801
|
+
# key = "title",
|
|
802
|
+
# language = "en",
|
|
803
|
+
# value = "Demo"
|
|
804
|
+
# >
|
|
805
|
+
|
|
806
|
+
pair.encoded? # true
|
|
807
|
+
----
|
|
808
|
+
|
|
809
|
+
The 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`.
|
|
810
|
+
|
|
811
|
+
As with `List` and `Link`, you have full access to all {data_link} messages and can explicitly and implicitly cast to a string. Example:
|
|
812
|
+
|
|
813
|
+
[source,ruby]
|
|
814
|
+
----
|
|
815
|
+
"Pair: #{pair}"
|
|
816
|
+
# Pair: title=Demo
|
|
817
|
+
----
|
|
818
|
+
|
|
819
|
+
=== Error Handling
|
|
820
|
+
|
|
821
|
+
As per {rfc_8288_link}, this implementation is designed to safely ignore malformed web links. To illustrate, we'll start with an initialized parser and root URI:
|
|
822
|
+
|
|
823
|
+
[source,ruby]
|
|
824
|
+
----
|
|
825
|
+
parser = RFC::Web::Link.new
|
|
826
|
+
root_uri = "https://demo.io"
|
|
827
|
+
----
|
|
828
|
+
|
|
829
|
+
The following examples show the result of parsing different malformed URIs:
|
|
830
|
+
|
|
831
|
+
*With Nil Value*
|
|
832
|
+
|
|
833
|
+
[source,ruby]
|
|
834
|
+
----
|
|
835
|
+
parser.call({"link" => nil}, root_uri:)
|
|
836
|
+
|
|
837
|
+
# #<data RFC::Web::Link::Models::List:0x00000880
|
|
838
|
+
# links = []
|
|
839
|
+
# >
|
|
840
|
+
----
|
|
841
|
+
|
|
842
|
+
*With Empty Value*
|
|
843
|
+
|
|
844
|
+
[source,ruby]
|
|
845
|
+
----
|
|
846
|
+
parser.call({"link" => ""}, root_uri:)
|
|
847
|
+
|
|
848
|
+
# #<data RFC::Web::Link::Models::List:0x00000880
|
|
849
|
+
# links = []
|
|
850
|
+
# >
|
|
851
|
+
----
|
|
852
|
+
|
|
853
|
+
*With No Leading Less Than Sign For URI*
|
|
854
|
+
|
|
855
|
+
[source,ruby]
|
|
856
|
+
----
|
|
857
|
+
parser.call({"link" => "/articles; rel=index"}, root_uri:)
|
|
858
|
+
|
|
859
|
+
# #<data RFC::Web::Link::Models::List:0x00000880
|
|
860
|
+
# links = []
|
|
861
|
+
# >
|
|
862
|
+
----
|
|
863
|
+
|
|
864
|
+
*Without Attributes*
|
|
865
|
+
|
|
866
|
+
[source,ruby]
|
|
867
|
+
----
|
|
868
|
+
parser.call({"link" => "</articles>"}, root_uri:)
|
|
869
|
+
|
|
870
|
+
# #<data RFC::Web::Link::Models::List:0x00000880
|
|
871
|
+
# links = [
|
|
872
|
+
# #<data RFC::Web::Link::Models::Link:0x000008b0
|
|
873
|
+
# pairs = [],
|
|
874
|
+
# uri = "https://demo.io/articles"
|
|
875
|
+
# >
|
|
876
|
+
# ]
|
|
877
|
+
# >
|
|
878
|
+
----
|
|
879
|
+
|
|
880
|
+
*With Missing Attribute Value*
|
|
881
|
+
|
|
882
|
+
[source,ruby]
|
|
883
|
+
----
|
|
884
|
+
parser.call({"link" => "</articles>; rel"}, root_uri:)
|
|
885
|
+
|
|
886
|
+
# #<data RFC::Web::Link::Models::List:0x000008a0
|
|
887
|
+
# links = [
|
|
888
|
+
# #<data RFC::Web::Link::Models::Link:0x00000910
|
|
889
|
+
# pairs = [
|
|
890
|
+
# #<data RFC::Web::Link::Models::Pair:0x00000930
|
|
891
|
+
# delimiter = nil,
|
|
892
|
+
# encoding = nil,
|
|
893
|
+
# key = "rel",
|
|
894
|
+
# language = nil,
|
|
895
|
+
# value = nil
|
|
896
|
+
# >
|
|
897
|
+
# ],
|
|
898
|
+
# uri = "https://demo.io/articles"
|
|
899
|
+
# >
|
|
900
|
+
# ]
|
|
901
|
+
# >
|
|
902
|
+
----
|
|
903
|
+
|
|
904
|
+
== Development
|
|
905
|
+
|
|
906
|
+
To contribute, run:
|
|
907
|
+
|
|
908
|
+
[source,bash]
|
|
909
|
+
----
|
|
910
|
+
git clone https://github.com/bkuhlmann/rfc-web-link
|
|
911
|
+
cd rfc-web-link
|
|
912
|
+
bin/setup
|
|
913
|
+
----
|
|
914
|
+
|
|
915
|
+
You can also use the IRB console for direct access to all objects:
|
|
916
|
+
|
|
917
|
+
[source,bash]
|
|
918
|
+
----
|
|
919
|
+
bin/console
|
|
920
|
+
----
|
|
921
|
+
|
|
922
|
+
== Tests
|
|
923
|
+
|
|
924
|
+
To test, run:
|
|
925
|
+
|
|
926
|
+
[source,bash]
|
|
927
|
+
----
|
|
928
|
+
bin/rake
|
|
929
|
+
----
|
|
930
|
+
|
|
931
|
+
== link:https://alchemists.io/policies/license[License]
|
|
932
|
+
|
|
933
|
+
== link:https://alchemists.io/policies/security[Security]
|
|
934
|
+
|
|
935
|
+
== link:https://alchemists.io/policies/code_of_conduct[Code of Conduct]
|
|
936
|
+
|
|
937
|
+
== link:https://alchemists.io/policies/contributions[Contributions]
|
|
938
|
+
|
|
939
|
+
== link:https://alchemists.io/policies/developer_certificate_of_origin[Developer Certificate of Origin]
|
|
940
|
+
|
|
941
|
+
== link:https://alchemists.io/projects/rfc-web-link/versions[Versions]
|
|
942
|
+
|
|
943
|
+
== link:https://alchemists.io/community[Community]
|
|
944
|
+
|
|
945
|
+
== Credits
|
|
946
|
+
|
|
947
|
+
* Built with link:https://alchemists.io/projects/gemsmith[Gemsmith].
|
|
948
|
+
* Engineered by link:https://alchemists.io/team/brooke_kuhlmann[Brooke Kuhlmann].
|