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.
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].