rfc-web-link 0.0.0 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a7682f76ac7525d7b34cd49068dca16fc0f5f000a43172291a23544097b63a09
4
- data.tar.gz: 0a49df14b408ab5f76d61152b381b81f8164849d66b74172d55f5954dc4eacab
3
+ metadata.gz: e5349bcccb250a2fb639bc10efcecb8313d1f27a9bc7f0c16fdde8ee38d59058
4
+ data.tar.gz: 3c47df4d22c978196942d28a03ef7cc3f1bdb28fd0937086661fe82e3e522c34
5
5
  SHA512:
6
- metadata.gz: af490a377b861570077f79e2aca9fc4633e7e50d4956cf2ac89404ef3091882fa8ac736bc4a1b54ff5cb0bd9491da5d92fa46cd117275b49bce4be292004db44
7
- data.tar.gz: c3f1e5d143c1ab1a4b8820321c828fdc54e0c2210f9c08f1733b6a67ed23bb3be7c78c5d915497cfe32ad06734dff808840a3c0b3f7b45ebfc1ebc428b8cc376
6
+ metadata.gz: 248afa0c4f87e8f4b57188dd43441238686e40e6379961e3e83ca3abb01e173d6095fdb838409c80e92ae15cf785b3fd119bc85b50f2ebdd8fb4dd905ea3708e
7
+ data.tar.gz: ec99e8ab113533799c1532ca5a69ae0c1fa44a9363f3f5158aac1e69bb7910c0b3e6ff40e1af0342dc48f777f1f6d96946bd9d80beb2f7a14e82607b56e144bb
checksums.yaml.gz.sig CHANGED
Binary file
data/README.adoc CHANGED
@@ -3,6 +3,7 @@
3
3
  :figure-caption!:
4
4
 
5
5
  :data_link: link:https://alchemists.io/articles/ruby_data[Data]
6
+ :enumerable_link: link:https://docs.ruby-lang.org/en/master/Enumerable.html[Enumerable]
6
7
  :rfc_8288_link: link:https://www.rfc-editor.org/info/rfc8288[RFC 8288]
7
8
 
8
9
  = RFC Web Link
@@ -22,7 +23,7 @@ toc::[]
22
23
 
23
24
  * Implements {rfc_8288_link}.
24
25
  * 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
+ * Provides explicit or implicit casting of whole value objects into a web links.
26
27
 
27
28
  == Requirements
28
29
 
@@ -66,14 +67,14 @@ The fastest way to get started is to create a new instance of the parser that yo
66
67
 
67
68
  [source,ruby]
68
69
  ----
69
- parser = RFC::Web::Link.new
70
+ parser = RFC::Web::Link.new "https://demo.io"
70
71
  ----
71
72
 
72
73
  Then you can use the `parser` instance to parse HTTP headers and build lists you can interact with:
73
74
 
74
75
  [source,ruby]
75
76
  ----
76
- parser.call({"link" => "</articles>; rel=index"}, root_uri: "https://demo.io")
77
+ parser.call({"link" => "</articles>; rel=index"})
77
78
  ----
78
79
 
79
80
  The above will yield the following set of links (in this case, only with a single link):
@@ -97,39 +98,67 @@ The above will yield the following set of links (in this case, only with a singl
97
98
  >
98
99
  ----
99
100
 
100
- As you can see, you get an immutable {data_link} whole value object for which you can directly access all attributes.
101
+ As you can see, you get an immutable {data_link} (whole value) object for which you can directly access all attributes.
101
102
 
102
103
  === Root URI
103
104
 
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
+ 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:
106
+
107
+ *Initialization*
108
+
109
+ The following sets the root URI globally so you only have to define it once:
110
+
111
+ [source,ruby]
112
+ ----
113
+ parser = RFC::Web::Link.new "https://demo.io"
114
+ ----
115
+
116
+ *Call*
117
+
118
+ The following sets the root URI per call.
119
+
120
+ [source,ruby]
121
+ ----
122
+ parser = RFC::Web::Link.new
123
+ parser.call({"link" => "</articles>; rel=index"}, root_uri: "https://demo.io")
124
+ ----
125
+
126
+ *Call (override)*
127
+
128
+ The following sets the root URI globally but also allows you to override the global value with a custom value per call.
129
+
130
+ [source,ruby]
131
+ ----
132
+ parser = RFC::Web::Link.new "https://demo.io"
133
+
134
+ parser.call({"link" => "</articles>; rel=index"}, root_uri: "https://alt.io")
135
+ ----
105
136
 
106
137
  === Delimiters
107
138
 
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:
139
+ Each delimiter provides the following behavior:
140
+
141
+ * *Semicolon* (`;`): Delimits each pair associated with a line.
142
+ * *Comma* (`,`): Delimits each line of a link.
109
143
 
110
- * *Semicolon* (`;`): Used to delimit each pair associated with a line.
111
- * *Comma* (`,`): Used to delimit each line of a link.
144
+ They must be quoted when not used as delimiters.
112
145
 
113
146
  === Attributes
114
147
 
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:
148
+ There are six {rfc_8288_link} registered target attributes:
116
149
 
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
- |===
150
+ * anchor
151
+ * hreflang
152
+ * media
153
+ * rel
154
+ * title
155
+ * type
127
156
 
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.
157
+ Each is described in detail below.
129
158
 
130
- ==== Anchor
159
+ ==== anchor
131
160
 
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:
161
+ 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
162
 
134
163
  * *Fragment*: `#footer`
135
164
  * *Quoted (special characters)*: `"#overview,body,footer"`
@@ -142,10 +171,8 @@ Relative URIs will automatically be expanded into an absolute URIs when parsed.
142
171
 
143
172
  [source,ruby]
144
173
  ----
145
- list = RFC::Web::Link.new.call(
146
- {"link" => "</articles>; anchor=#footer"},
147
- root_uri: "https://demo.io"
148
- )
174
+ parser = RFC::Web::Link.new "https://demo.io"
175
+ list = parser.call({"link" => "</articles>; anchor=#footer"})
149
176
 
150
177
  # #<data RFC::Web::Link::Models::List:0x000016f0
151
178
  # links = [
@@ -173,10 +200,8 @@ list.to_s
173
200
 
174
201
  [source,ruby]
175
202
  ----
176
- list = RFC::Web::Link.new.call(
177
- {"link" => %(</articles>; anchor="#overview,body,footer")},
178
- root_uri: "https://demo.io"
179
- )
203
+ parser = RFC::Web::Link.new "https://demo.io"
204
+ list = parser.call({"link" => %(</articles>; anchor="#overview,body,footer")})
180
205
 
181
206
  # #<data RFC::Web::Link::Models::List:0x00001660
182
207
  # links = [
@@ -204,10 +229,8 @@ list.to_s
204
229
 
205
230
  [source,ruby]
206
231
  ----
207
- list = RFC::Web::Link.new.call(
208
- {"link" => "</articles>; anchor=/policies"},
209
- root_uri: "https://demo.io"
210
- )
232
+ parser = RFC::Web::Link.new "https://demo.io"
233
+ list = parser.call({"link" => "</articles>; anchor=/policies"})
211
234
 
212
235
  # #<data RFC::Web::Link::Models::List:0x000017c0
213
236
  # links = [
@@ -235,10 +258,8 @@ list.to_s
235
258
 
236
259
  [source,ruby]
237
260
  ----
238
- list = RFC::Web::Link.new.call(
239
- {"link" => "</articles>; anchor=https://demo.io/about"},
240
- root_uri: "https://demo.io"
241
- )
261
+ parser = RFC::Web::Link.new "https://demo.io"
262
+ list = parser.call({"link" => "</articles>; anchor=https://demo.io/about"})
242
263
 
243
264
  # #<data RFC::Web::Link::Models::List:0x00001890
244
265
  # links = [
@@ -262,18 +283,16 @@ list.to_s
262
283
  # "<https://demo.io/articles>; anchor=https://demo.io/about"
263
284
  ----
264
285
 
265
- ==== Language
286
+ ==== hreflang
266
287
 
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:
288
+ 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
289
 
269
290
  *Single*
270
291
 
271
292
  [source,ruby]
272
293
  ----
273
- list = RFC::Web::Link.new.call(
274
- {"link" => "</articles>; hreflang=en"},
275
- root_uri: "https://demo.io"
276
- )
294
+ parser = RFC::Web::Link.new "https://demo.io"
295
+ list = parser.call({"link" => "</articles>; hreflang=en"})
277
296
 
278
297
  # #<data RFC::Web::Link::Models::List:0x00000890
279
298
  # links = [
@@ -301,10 +320,8 @@ list.to_s
301
320
 
302
321
  [source,ruby]
303
322
  ----
304
- list = RFC::Web::Link.new.call(
305
- {"link" => "</articles>; hreflang=en; hreflang=de"},
306
- root_uri: "https://demo.io"
307
- )
323
+ parser = RFC::Web::Link.new "https://demo.io"
324
+ list = parser.call({"link" => "</articles>; hreflang=en; hreflang=de"})
308
325
 
309
326
  # #<data RFC::Web::Link::Models::List:0x000016c0
310
327
  # links = [
@@ -335,16 +352,14 @@ list.to_s
335
352
  # "<https://demo.io/articles>; hreflang=en; hreflang=de"
336
353
  ----
337
354
 
338
- ==== Media
355
+ ==== media
339
356
 
340
- Media is optional but, when supplied, is a hint indicating the kind of media associated with the link. Example:
357
+ Optional but, when supplied, is a hint indicating the kind of media associated with the link. Example:
341
358
 
342
359
  [source,ruby]
343
360
  ----
344
- list = RFC::Web::Link.new.call(
345
- {"link" => "</articles>; media=print"},
346
- root_uri: "https://demo.io"
347
- )
361
+ parser = RFC::Web::Link.new "https://demo.io"
362
+ list = parser.call({"link" => "</articles>; media=print"})
348
363
 
349
364
  # #<data RFC::Web::Link::Models::List:0x000017d0
350
365
  # links = [
@@ -368,18 +383,16 @@ list.to_s
368
383
  # "<https://demo.io/articles>; media=print"
369
384
  ----
370
385
 
371
- ==== Relation
386
+ ==== rel
372
387
 
373
- A relation is the only attribute that is required by the specification and should be the first one defined. Example:
388
+ Required and must be first defined. Example:
374
389
 
375
390
  *Single*
376
391
 
377
392
  [source,ruby]
378
393
  ----
379
- list = RFC::Web::Link.new.call(
380
- {"link" => "</articles>; relation=index"},
381
- root_uri: "https://demo.io"
382
- )
394
+ parser = RFC::Web::Link.new "https://demo.io"
395
+ list = parser.call({"link" => "</articles>; rel=index"})
383
396
 
384
397
  # #<data RFC::Web::Link::Models::List:0x00000890
385
398
  # links = [
@@ -388,7 +401,7 @@ list = RFC::Web::Link.new.call(
388
401
  # #<data RFC::Web::Link::Models::Pair:0x000008d0
389
402
  # delimiter = "=",
390
403
  # encoding = nil,
391
- # key = "relation",
404
+ # key = "rel",
392
405
  # language = nil,
393
406
  # value = "index"
394
407
  # >
@@ -409,10 +422,8 @@ Short hand, for multiple relations, is supported as well. To use, each relation
409
422
 
410
423
  [source,ruby]
411
424
  ----
412
- list = RFC::Web::Link.new.call(
413
- {"link" => %(</articles>; rel="one two three")},
414
- root_uri: "https://demo.io"
415
- )
425
+ parser = RFC::Web::Link.new "https://demo.io"
426
+ list = parser.call({"link" => %(</articles>; rel="one two three")})
416
427
 
417
428
  # #<data RFC::Web::Link::Models::List:0x00000960
418
429
  # links = [
@@ -462,9 +473,9 @@ list.to_s
462
473
 
463
474
  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
475
 
465
- ==== Title
476
+ ==== title
466
477
 
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:
478
+ 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
479
 
469
480
  * *Plain* (`=`): `title=Demo` (example)
470
481
  * *Encoded* (`*=`): `+title*=UTF-8'en'd%C3%A9j%C3%A0%20vu+` (example)
@@ -475,10 +486,8 @@ The title _must not_ appear more than once. If multiple occurrences are detected
475
486
 
476
487
  [source,ruby]
477
488
  ----
478
- list = RFC::Web::Link.new.call(
479
- {"link" => "</articles>; title=Demo"},
480
- root_uri: "https://demo.io"
481
- )
489
+ parser = RFC::Web::Link.new "https://demo.io"
490
+ list = parser.call({"link" => "</articles>; title=Demo"})
482
491
 
483
492
  # #<data RFC::Web::Link::Models::List:0x00000890
484
493
  # links = [
@@ -506,10 +515,8 @@ list.to_s
506
515
 
507
516
  [source,ruby]
508
517
  ----
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
- )
518
+ parser = RFC::Web::Link.new "https://demo.io"
519
+ list = parser.call({"link" => "</articles>; title*=UTF-8'en'd%C3%A9j%C3%A0%20vu"})
513
520
 
514
521
  # #<data RFC::Web::Link::Models::List:0x00000890
515
522
  # links = [
@@ -533,16 +540,14 @@ list.to_s
533
540
  # "<https://demo.io/articles>; title*=UTF-8'en'd%C3%A9j%C3%A0%20vu"
534
541
  ----
535
542
 
536
- ==== Type
543
+ ==== type
537
544
 
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:
545
+ 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
546
 
540
547
  [source,ruby]
541
548
  ----
542
- list = RFC::Web::Link.new.call(
543
- {"link" => "</articles>; type=text/html"},
544
- root_uri: "https://demo.io"
545
- )
549
+ parser = RFC::Web::Link.new "https://demo.io"
550
+ list = parser.call({"link" => "</articles>; type=text/html"})
546
551
 
547
552
  # #<data RFC::Web::Link::Models::List:0x00000890
548
553
  # links = [
@@ -572,9 +577,10 @@ Extensions allow you to extend your implementation beyond the registered target
572
577
 
573
578
  [source,ruby]
574
579
  ----
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"
580
+ parser = RFC::Web::Link.new "https://demo.io"
581
+
582
+ list = parser.call(
583
+ {"link" => %(</articles>; hint="A demo."; errata="For demonstration only."; copyright=2025)}
578
584
  )
579
585
 
580
586
  # #<data RFC::Web::Link::Models::List:0x00000980
@@ -617,10 +623,8 @@ You can also encode your extensions by using the `+*=+` delimiter. Example:
617
623
 
618
624
  [source,ruby]
619
625
  ----
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
- )
626
+ parser = RFC::Web::Link.new "https://demo.io"
627
+ list = parser.call({"link" => "</articles>; demo*=UTF-8'en'd%C3%A9j%C3%A0%20vu"})
624
628
 
625
629
  # #<data RFC::Web::Link::Models::List:0x00000ad0
626
630
  # links = [
@@ -650,7 +654,7 @@ You've already seen all of the models (whole value objects) used in the examples
650
654
 
651
655
  ==== List
652
656
 
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:
657
+ This model has both {data_link} and {enumerable_link} 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:
654
658
 
655
659
  *All At Once*
656
660
 
@@ -679,23 +683,13 @@ list = RFC::Web::Link::Models::Link[uri: "https://demo.io"].add(pair)
679
683
  list = RFC::Web::Link::Models::List.new.add(link)
680
684
  ----
681
685
 
682
- Once you have a list, you can send the following messages (including {data_link} messages) along limited enumerable messages:
686
+ Once you have a list, you can send the following messages including {data_link} and {enumerable_link} messages:
683
687
 
684
688
  [source,ruby]
685
689
  ----
686
690
  list.add line
687
- list.all?
688
- list.any?
689
691
  list.clear
690
- list.each
691
692
  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
693
  list.size
700
694
  list.to_s
701
695
  list.to_str
@@ -717,7 +711,7 @@ This model encapsulates a single HTTP link header. A link can be created multipl
717
711
 
718
712
  [source,ruby]
719
713
  ----
720
- list = RFC::Web::Link::Models::Link[
714
+ link = RFC::Web::Link::Models::Link[
721
715
  uri: "https://demo.io",
722
716
  pairs: Set[
723
717
  RFC::Web::Link::Models::Pair[key: :title, value: "Demo"]
@@ -732,7 +726,7 @@ list = RFC::Web::Link::Models::Link[
732
726
  [source,ruby]
733
727
  ----
734
728
  pair = RFC::Web::Link::Models::Pair[key: :title, value: "Demo"]
735
- list = RFC::Web::Link::Models::Link[uri: "https://demo.io"].add(pair)
729
+ link = RFC::Web::Link::Models::Link[uri: "https://demo.io"].add(pair)
736
730
  ----
737
731
 
738
732
  Once you have a link, you can then send the following messages (including {data_link} messages):
@@ -748,8 +742,11 @@ link.append :title,
748
742
  delimiter: "*=",
749
743
  encoding: "UTF-8",
750
744
  language: "en"
751
- link.has? :title
752
- link.has? "title"
745
+ link.find_pair key: "title", value: "Demo"
746
+ link.find_pair key: "title", value: /dem/i
747
+ link.find_pair key: "title", delimiter: /\*=/, value: "Demo"
748
+ link.pair? key: "title"
749
+ link.pair? key: "title", value: "Demo"
753
750
  list.to_s
754
751
  list.to_str
755
752
  ----
@@ -818,12 +815,11 @@ As with `List` and `Link`, you have full access to all {data_link} messages and
818
815
 
819
816
  === Error Handling
820
817
 
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:
818
+ As per {rfc_8288_link}, this implementation is designed to safely ignore malformed web links. To illustrate, we'll start with an initialized parser:
822
819
 
823
820
  [source,ruby]
824
821
  ----
825
- parser = RFC::Web::Link.new
826
- root_uri = "https://demo.io"
822
+ parser = RFC::Web::Link.new "https://demo.io"
827
823
  ----
828
824
 
829
825
  The following examples show the result of parsing different malformed URIs:
@@ -832,7 +828,7 @@ The following examples show the result of parsing different malformed URIs:
832
828
 
833
829
  [source,ruby]
834
830
  ----
835
- parser.call({"link" => nil}, root_uri:)
831
+ parser.call({"link" => nil})
836
832
 
837
833
  # #<data RFC::Web::Link::Models::List:0x00000880
838
834
  # links = []
@@ -843,7 +839,7 @@ parser.call({"link" => nil}, root_uri:)
843
839
 
844
840
  [source,ruby]
845
841
  ----
846
- parser.call({"link" => ""}, root_uri:)
842
+ parser.call({"link" => ""})
847
843
 
848
844
  # #<data RFC::Web::Link::Models::List:0x00000880
849
845
  # links = []
@@ -854,7 +850,7 @@ parser.call({"link" => ""}, root_uri:)
854
850
 
855
851
  [source,ruby]
856
852
  ----
857
- parser.call({"link" => "/articles; rel=index"}, root_uri:)
853
+ parser.call({"link" => "/articles; rel=index"})
858
854
 
859
855
  # #<data RFC::Web::Link::Models::List:0x00000880
860
856
  # links = []
@@ -865,7 +861,7 @@ parser.call({"link" => "/articles; rel=index"}, root_uri:)
865
861
 
866
862
  [source,ruby]
867
863
  ----
868
- parser.call({"link" => "</articles>"}, root_uri:)
864
+ parser.call({"link" => "</articles>"})
869
865
 
870
866
  # #<data RFC::Web::Link::Models::List:0x00000880
871
867
  # links = [
@@ -881,7 +877,7 @@ parser.call({"link" => "</articles>"}, root_uri:)
881
877
 
882
878
  [source,ruby]
883
879
  ----
884
- parser.call({"link" => "</articles>; rel"}, root_uri:)
880
+ parser.call({"link" => "</articles>; rel"})
885
881
 
886
882
  # #<data RFC::Web::Link::Models::List:0x000008a0
887
883
  # links = [
@@ -7,18 +7,18 @@ module RFC
7
7
  module Link
8
8
  # A HTTP header field decoder which adheres to RFC 8187.
9
9
  class Decoder
10
- def initialize delimiter: "'", default_encoding: Encoding::UTF_8, client: URI
10
+ def initialize delimiter: "'", client: URI
11
11
  @delimiter = delimiter
12
- @default_encoding = default_encoding
13
12
  @client = client
13
+ @quote = %(")
14
14
  end
15
15
 
16
16
  def call text
17
17
  value, encoding, language = parse text
18
18
 
19
19
  value = client.decode_uri_component(value)
20
- .force_encoding(encoding || default_encoding)
21
- .encode(default_encoding)
20
+ .force_encoding(encoding || Encoding::UTF_8)
21
+ .encode(Encoding::UTF_8)
22
22
 
23
23
  {value:, encoding:, language:}
24
24
  rescue ArgumentError, NoMethodError
@@ -27,10 +27,10 @@ module RFC
27
27
 
28
28
  private
29
29
 
30
- attr_reader :delimiter, :default_encoding, :client
30
+ attr_reader :delimiter, :client, :quote
31
31
 
32
32
  def parse text
33
- case String(text).split delimiter
33
+ case String(text).delete_prefix(quote).delete_suffix(quote).split delimiter
34
34
  in [value] then [value, nil, nil]
35
35
  in [encoding, language, value] then [value, encoding, language]
36
36
  else [nil, nil, nil]
@@ -3,27 +3,40 @@
3
3
  module RFC
4
4
  module Web
5
5
  module Link
6
- # A HTTP header field encoder which adheres to RFC 8187.
6
+ # A HTTP header value encoder which adheres to RFC 8187.
7
7
  class Encoder
8
- def initialize pattern: /[^0-9a-zA-Z!\#$&+\-.^_`|~]/
9
- @pattern = pattern
8
+ def initialize ignored_characters: /\A[0-9a-zA-Z!\#$%&'*+\-.^_`|~]*\z/,
9
+ ignored_keys: %w[type]
10
+ @ignored_characters = ignored_characters
11
+ @ignored_keys = ignored_keys
10
12
  end
11
13
 
12
- def call(text) = text.match?(pattern) ? convert_characters(text).join : text
14
+ def call value, key: nil
15
+ text = String value
16
+
17
+ if text.match?(ignored_characters) || ignored_keys.include?(key) then text
18
+ elsif text.match? ignored_characters_and_spaces then text.dump
19
+ else convert_characters(text).join
20
+ end
21
+ end
13
22
 
14
23
  private
15
24
 
16
- attr_reader :pattern
25
+ attr_reader :ignored_characters, :ignored_keys
17
26
 
18
27
  def convert_characters text
19
28
  text.each_char.map do |character|
20
- next character unless character.match? pattern
29
+ next character if character.match? ignored_characters
21
30
 
22
31
  character.bytes
23
32
  .map { format "%%%02X", it }
24
33
  .join
25
34
  end
26
35
  end
36
+
37
+ def ignored_characters_and_spaces
38
+ @ignored_characters_and_spaces ||= Regexp.new ignored_characters.source.sub("]", "\\s]")
39
+ end
27
40
  end
28
41
  end
29
42
  end
@@ -26,7 +26,17 @@ module RFC
26
26
  self
27
27
  end
28
28
 
29
- def has?(key) = pairs.any? { it.key == key.to_s }
29
+ def find_pair **attributes
30
+ pairs.find do |pair|
31
+ attributes.all? { |key, value| pair.public_send(key).match? value }
32
+ end
33
+ end
34
+
35
+ def pair? **attributes
36
+ pairs.any? do |pair|
37
+ attributes.all? { |key, value| pair.public_send(key).match? value }
38
+ end
39
+ end
30
40
 
31
41
  def to_s(delimiter: "; ") = "<#{uri}>; #{pairs.join delimiter}"
32
42
 
@@ -9,8 +9,9 @@ module RFC
9
9
  # Models a list of links.
10
10
  List = Data.define :links do
11
11
  extend Forwardable
12
+ include Enumerable
12
13
 
13
- delegate %i[all? any? empty? find include? map none? one? size] => :links
14
+ delegate %i[empty? size] => :links
14
15
 
15
16
  def initialize links: Set.new
16
17
  super
@@ -26,11 +27,11 @@ module RFC
26
27
  self
27
28
  end
28
29
 
29
- def each(&block) = block ? links.each(&block) : self
30
+ def each
31
+ return enum_for :each unless block_given?
30
32
 
31
- def reject(&) = with links: Set[*links.reject(&)]
32
-
33
- def select(&) = with links: Set[*links.select(&)]
33
+ links.each { yield it }
34
+ end
34
35
 
35
36
  def to_s(delimiter: ", ") = links.join delimiter
36
37
 
@@ -12,15 +12,12 @@ module RFC
12
12
 
13
13
  def encoded? = delimiter == "*="
14
14
 
15
- def to_s key_map: {"relation" => "rel", "language" => "hreflang"}.freeze,
16
- encoder: Encoder.new
17
- transformed_key = key_map.fetch(key) { it }
18
-
15
+ def to_s encoder: Encoder.new
19
16
  if encoding
20
- "#{transformed_key}#{delimiter}#{encoding}'#{language}'" \
17
+ "#{key}#{delimiter}#{encoding}'#{language}'" \
21
18
  "#{encoder.call value.encode(encoding)}"
22
19
  else
23
- "#{transformed_key}#{delimiter}#{value}"
20
+ "#{key}#{delimiter}#{encoder.call value, key:}"
24
21
  end
25
22
  end
26
23
 
@@ -6,19 +6,20 @@ module RFC
6
6
  module Parsers
7
7
  # Parses a header link into a list of records.
8
8
  class Header
9
- def initialize pattern: /link/i, list: List.new
9
+ def initialize root_uri = nil, pattern: /link/i, list: List.new
10
+ @root_uri = root_uri
10
11
  @pattern = pattern
11
12
  @list = list
12
13
  end
13
14
 
14
- def call headers, root_uri:
15
+ def call headers, root_uri: nil
15
16
  text = headers.find { |key, value| break value if key.match? pattern }
16
- list.call text, root_uri:
17
+ list.call text, root_uri: root_uri || self.root_uri
17
18
  end
18
19
 
19
20
  private
20
21
 
21
- attr_reader :pattern, :list
22
+ attr_reader :root_uri, :pattern, :list
22
23
  end
23
24
  end
24
25
  end
@@ -6,8 +6,8 @@ module RFC
6
6
  module Parsers
7
7
  # Parses a header line into a link record.
8
8
  class Line
9
- def initialize delimiter: /;\s*?/, pair: Pair.new, model: Models::Link
10
- @delimiter = delimiter
9
+ def initialize patterns: PATTERNS, pair: Pair.new, model: Models::Link
10
+ @patterns = patterns
11
11
  @pair = pair
12
12
  @model = model
13
13
  end
@@ -21,7 +21,9 @@ module RFC
21
21
 
22
22
  private
23
23
 
24
- attr_reader :delimiter, :pair, :model
24
+ attr_reader :patterns, :pair, :model
25
+
26
+ def instance_variables_to_inspect = %i[@pair @model]
25
27
 
26
28
  def process parts,
27
29
  root_uri,
@@ -43,9 +45,17 @@ module RFC
43
45
  end
44
46
 
45
47
  def build_uri value, root_uri:
46
- value.delete_prefix("<")
47
- .delete_suffix(">")
48
- .then { it.start_with?("/") ? "#{root_uri}#{it}" : it }
48
+ value.match(uri_pattern)[:value].then do |uri|
49
+ uri.start_with?("/") ? "#{root_uri}#{uri}" : uri
50
+ end
51
+ end
52
+
53
+ def uri_pattern
54
+ @uri_pattern ||= patterns.fetch :uri
55
+ end
56
+
57
+ def delimiter
58
+ @delimiter ||= patterns.fetch :line_delimiter
49
59
  end
50
60
  end
51
61
  end
@@ -8,37 +8,31 @@ module RFC
8
8
  module Parsers
9
9
  # Parses header links into a list of records.
10
10
  class List
11
- RELATION_PATTERN = /
12
- (?<prefix>rel=") # Prefix.
13
- .+ # One or more characters.
14
- \s+? # One or more spaces, lazy.
15
- .+ # One or more characters.
16
- (?<suffix>") # Suffix.
17
- /x
18
-
19
- def initialize relation_pattern: RELATION_PATTERN, line: Line.new, list: Models::List.new
20
- @scanner = StringScanner.new ""
21
- @relation_pattern = relation_pattern
11
+ def initialize patterns: PATTERNS, line: Line.new, list: Models::List.new
12
+ @patterns = patterns
22
13
  @line = line
23
14
  @list = list
15
+ @scanner = StringScanner.new ""
24
16
  @comma = ","
25
17
  @quote = %(")
26
18
  end
27
19
 
28
20
  def call text, root_uri:
29
- text = String text
21
+ match = String(text).match uri_pattern
30
22
  list.clear
31
23
 
32
- return list unless text.start_with? "<"
24
+ return list unless match
33
25
 
34
- scanner.string = text
26
+ scanner.string = match.string
35
27
 
36
28
  build_list root_uri
37
29
  end
38
30
 
39
31
  private
40
32
 
41
- attr_reader :scanner, :relation_pattern, :line, :list, :comma, :quote
33
+ attr_reader :patterns, :line, :list, :scanner, :comma, :quote
34
+
35
+ def instance_variables_to_inspect = %i[@line @list]
42
36
 
43
37
  def build_list root_uri, buffer: +"", lines: []
44
38
  check scanner.getch, buffer, lines until scanner.eos?
@@ -65,19 +59,23 @@ module RFC
65
59
  buffer << %("#{scanner.pre_match[start..]}")
66
60
  end
67
61
 
68
- # rubocop:todo Metrics/AbcSize
69
62
  def maybe_split_by_relation text, root_uri:
70
63
  match = text.match relation_pattern
71
64
 
72
65
  return list.add line.call(text, root_uri:) unless match
73
66
 
74
- match.to_s
75
- .delete_prefix(match[:prefix])
76
- .delete_suffix(match[:suffix])
77
- .split
78
- .each { list.add line.call(text.sub(relation_pattern, "rel=#{it}"), root_uri:) }
67
+ match[:value].split.each do |value|
68
+ list.add line.call(text.sub(relation_pattern, "rel=#{value}"), root_uri:)
69
+ end
70
+ end
71
+
72
+ def uri_pattern
73
+ @uri_pattern ||= patterns.fetch :uri
74
+ end
75
+
76
+ def relation_pattern
77
+ @relation_pattern ||= patterns.fetch :relation
79
78
  end
80
- # rubocop:enable Metrics/AbcSize
81
79
  end
82
80
  end
83
81
  end
@@ -6,16 +6,14 @@ module RFC
6
6
  module Parsers
7
7
  # Parses a header key/value pair into a record.
8
8
  class Pair
9
- def initialize split_pattern: /(?<target>=)|(?<extended>\*=)/,
10
- decoder: Decoder.new,
11
- model: Models::Pair
12
- @split_pattern = split_pattern
9
+ def initialize patterns: PATTERNS, decoder: Decoder.new, model: Models::Pair
10
+ @patterns = patterns
13
11
  @decoder = decoder
14
12
  @model = model
15
13
  end
16
14
 
17
15
  def call text, root_uri:
18
- key, delimiter, value = text.split split_pattern
16
+ key, delimiter, value = text.split delimiter_pattern
19
17
  key.strip!
20
18
 
21
19
  attributes = decoder.call value
@@ -28,7 +26,13 @@ module RFC
28
26
 
29
27
  private
30
28
 
31
- attr_reader :split_pattern, :decoder, :model
29
+ attr_reader :patterns, :decoder, :model
30
+
31
+ def instance_variables_to_inspect = %i[@decoder @model]
32
+
33
+ def delimiter_pattern
34
+ @delimiter_pattern ||= patterns.fetch :pair_delimiter
35
+ end
32
36
  end
33
37
  end
34
38
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RFC
4
+ module Web
5
+ module Link
6
+ module Parsers
7
+ PATTERNS = {
8
+ uri: /
9
+ < # Prefix.
10
+ (?<value>.+) # Value named group.
11
+ > # Suffix.
12
+ /x,
13
+ relation: /
14
+ rel=" # Prefix.
15
+ (?<value>.+) # Value named group.
16
+ " # Suffix.
17
+ /x,
18
+ line_delimiter: /
19
+ ; # Semicolon.
20
+ \s*? # Optional spaces, lazy.
21
+ /x,
22
+ pair_delimiter: /
23
+ (?<target>=) # Target named group.
24
+ | # Or.
25
+ (?<extended>\*=) # Extended named group.
26
+ /x
27
+ }.freeze
28
+ end
29
+ end
30
+ end
31
+ end
data/lib/rfc/web/link.rb CHANGED
@@ -5,6 +5,7 @@ require "rfc/web/link/encoder"
5
5
  require "rfc/web/link/models/link"
6
6
  require "rfc/web/link/models/list"
7
7
  require "rfc/web/link/models/pair"
8
+ require "rfc/web/link/parsers"
8
9
  require "rfc/web/link/parsers/header"
9
10
  require "rfc/web/link/parsers/line"
10
11
  require "rfc/web/link/parsers/list"
@@ -14,7 +15,7 @@ module RFC
14
15
  module Web
15
16
  # Main namespace.
16
17
  module Link
17
- def self.new(**) = Parsers::Header.new(**)
18
+ def self.new(*, **) = Parsers::Header.new(*, **)
18
19
  end
19
20
  end
20
21
  end
data/rfc-web-link.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "rfc-web-link"
5
- spec.version = "0.0.0"
5
+ spec.version = "0.1.0"
6
6
  spec.authors = ["Brooke Kuhlmann"]
7
7
  spec.email = ["brooke@alchemists.io"]
8
8
  spec.homepage = "https://alchemists.io/projects/rfc-web-link"
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rfc-web-link
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brooke Kuhlmann
@@ -51,6 +51,7 @@ files:
51
51
  - lib/rfc/web/link/models/link.rb
52
52
  - lib/rfc/web/link/models/list.rb
53
53
  - lib/rfc/web/link/models/pair.rb
54
+ - lib/rfc/web/link/parsers.rb
54
55
  - lib/rfc/web/link/parsers/header.rb
55
56
  - lib/rfc/web/link/parsers/line.rb
56
57
  - lib/rfc/web/link/parsers/list.rb
metadata.gz.sig CHANGED
Binary file