link-header-parser 6.1.1 → 7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bec652a3b5e7759eef7d39f146793f1cd4c294f7feb9224743d3de0096306e99
4
- data.tar.gz: 0d277b9ea131896cdb57d28246202b06071e2ba48c64bc7b4d6080d9e6d4240d
3
+ metadata.gz: 3d7f18899021fd6fb79e5915e8d1945fc67a1de6093f8b8043e05b3de01cbffc
4
+ data.tar.gz: 30652a856807b2ab19dc1a2b0d7a419e7ff399f182a31eff3734d403eba1b8bb
5
5
  SHA512:
6
- metadata.gz: 003a7d910fae4dfcb416a262afbfcdd846406cf0bfedbe7425b8d302a9922243e57892dbb03dd8b6d7efef3d3bfd43f957759bce8a6912830ffd1773093f811b
7
- data.tar.gz: 7f74b994d6806e58d3393824fb68d2fda896d75b1de01941269d3110fada1eb36631b6e030f20bf17573a35358f43446630ed003e9d023ded98378df2788b984
6
+ metadata.gz: a509d74c25c362a61eba888acf9ea4b5e733e66db19003e2c82ac9b9d72de70d491993ed9b1c5db4265905332fffbf2cdc4cb1f331bb8f7f19a5914d4b8cfbf5
7
+ data.tar.gz: 73a5ebd6172a225bf6ffff23250c241dcfe78855cb2aa97b65811deece22ae3a10b292fc444f973ec141396edc33736a2210183c2a3b85bac2f1b8160a0e5e21
data/README.md CHANGED
@@ -6,6 +6,12 @@
6
6
  [![Downloads](https://img.shields.io/gem/dt/link-header-parser.svg?logo=rubygems&style=for-the-badge)](https://rubygems.org/gems/link-header-parser)
7
7
  [![Source](https://img.shields.io/badge/get_it_on_codeberg-2185d0?labelColor=555&logo=codeberg&logoColor=fff&style=for-the-badge)](https://codeberg.org/jgarber/link-header-parser-ruby)
8
8
 
9
+ ## Key Features
10
+
11
+ - Implements [RFC-8288](https://tools.ietf.org/html/rfc8288) HTTP Link Header parsing algorithm (see [Appendix B](https://tools.ietf.org/html/rfc8288#appendix-B)).
12
+ - Supports [IRI](https://en.wikipedia.org/wiki/Internationalized_Resource_Identifier)s by using the [Addressable](https://rubygems.org/gems/addressable) gem.
13
+ - Supports Ruby 2.6 and newer.
14
+
9
15
  ## Getting Started
10
16
 
11
17
  Before installing and using link-header-parser-ruby, you'll want to have [Ruby](https://www.ruby-lang.org) 2.6 (or newer) installed. Using a Ruby version managment tool like [rbenv](https://github.com/rbenv/rbenv), [chruby](https://github.com/postmodern/chruby), or [rvm](https://github.com/rvm/rvm) is recommended.
@@ -24,114 +30,7 @@ gem "link-header-parser"
24
30
 
25
31
  ## Usage
26
32
 
27
- With link-header-parser-ruby added to your project's `Gemfile` and installed, you may parse a URL's HTTP Link headers by doing:
28
-
29
- ```ruby
30
- require "net/http"
31
- require "link-header-parser"
32
-
33
- url = "https://sixtwothree.org"
34
- link_headers = Net::HTTP.get_response(URI.parse(url)).get_fields("Link")
35
-
36
- collection = LinkHeaderParser.parse(link_headers, base: url)
37
- ```
38
-
39
- The `parse` method accepts two arguments:
40
-
41
- 1. an `Array` of strings representing HTTP Link headers (e.g. `['</>; rel="home"', '</chapters/1>; anchor="#copyright"; rel="license"']`)
42
- 1. a `String` (or any `String`-like object) representing the absolute URL of the resource providing the HTTP Link headers
43
-
44
- In the example above, `collection` is an instance of `LinkHeadersCollection` which includes Ruby's [`Enumerable`](https://ruby-doc.org/core/Enumerable.html) mixin. This mixin allows for use of common methods like `each`, `first`/`last`, and `map`.
45
-
46
- For example, you could retrieve an array of `target_uri`s:
47
-
48
- ```ruby
49
- puts collection.map(&:target_uri)
50
- #=> ["https://assets.sixtwothree.org/", "https://fonts.googleapis.com/", "https://fonts.gstatic.com/", "https://sixtwothree.org/webmentions"]
51
- ```
52
-
53
- ### Working with a `LinkHeadersCollection`
54
-
55
- In addition to the included `Enumerable` methods, the following methods may be used to interact with a `LinkHeadersCollection`:
56
-
57
- #### The `relation_types` Method
58
-
59
- ```ruby
60
- puts collection.relation_types
61
- #=> ["preconnect", "webmention"]
62
- ```
63
-
64
- #### The `group_by_relation_type` Method
65
-
66
- Using the `collection` from above, the `group_by_relation_type` method returns a `Hash`:
67
-
68
- ```ruby
69
- {
70
- preconnect: [
71
- #<LinkHeaderParser::LinkHeader target_uri: "https://assets.sixtwothree.org/", relation_types: ["preconnect"]>,
72
- #<LinkHeaderParser::LinkHeader target_uri: "https://fonts.googleapis.com/", relation_types: ["preconnect"]>,
73
- #<LinkHeaderParser::LinkHeader target_uri: "https://fonts.gstatic.com/", relation_types: ["preconnect"]>
74
- ],
75
- webmention: [
76
- #<LinkHeaderParser::LinkHeader target_uri: "https://sixtwothree.org/webmentions", relation_types: ["webmention"]>
77
- ]
78
- }
79
- ```
80
-
81
- ### Working with a `LinkHeader`
82
-
83
- You may interact with one or more `LinkHeader`s in a `LinkHeadersCollection` using the methods outlined below. The naming conventions for these methods draws heavily on the terminology established in [RFC-5988](https://tools.ietf.org/html/rfc5988) and [RFC-8288](https://tools.ietf.org/html/rfc8288).
84
-
85
- #### Link Target ([§ 3.1](https://tools.ietf.org/html/rfc8288#section-3.1))
86
-
87
- ```ruby
88
- link_header = LinkHeaderParser.parse(%(</index.html>; rel="home"), base: "https://example.com/").first
89
-
90
- link_header.target_string
91
- #=> "/index.html"
92
-
93
- link_header.target_uri
94
- #=> "https://example.com/index.html"
95
- ```
96
-
97
- The `target_string` method returns a string of the value between the opening and closing angle brackets at the beginning of the Link header. The `target_uri` method returns a string representing the resolved URL.
98
-
99
- #### Link Context ([§ 3.2](https://tools.ietf.org/html/rfc8288#section-3.2))
100
-
101
- ```ruby
102
- link_header = LinkHeaderParser.parse(%(</chapters/1>; anchor="#copyright"; rel="license"), base: "https://example.com/").first
103
-
104
- link_header.context_string
105
- #=> "#copyright"
106
-
107
- link_header.context_uri
108
- #=> "https://example.com/chapters/1#copyright"
109
- ```
110
-
111
- The `anchor` parameter's value may be a fragment identifier (e.g. `#foo`), a relative URL (e.g. `/foo`), or an absolute URL (e.g. `https://context.example.com`). The `context_string` method returns the `anchor` parameter's value (when present) and defaults to the `target_string` value. The `context_uri` method returns a string representing the resolved URL.
112
-
113
- #### Relation Type ([§ 3.3](https://tools.ietf.org/html/rfc8288#section-3.3))
114
-
115
- ```ruby
116
- link_header = LinkHeaderParser.parse(%(</chapters/1>; rel="prev start"), base: "https://example.com/").first
117
-
118
- link_header.relations_string
119
- #=> "prev start"
120
-
121
- link_header.relation_types
122
- #=> ["prev", "start"]
123
- ```
124
-
125
- #### Link Parameters ([Appendix B.3](https://tools.ietf.org/html/rfc8288#appendix-B.3))
126
-
127
- ```ruby
128
- link_header = LinkHeaderParser.parse(%(</posts.rss>; rel="alternate"; hreflang="en-US"; title="sixtwothree.org: Posts"; type="application/rss+xml"), base: "https://sixtwothree.org").first
129
-
130
- link_header.link_parameters
131
- #=> [#<LinkHeaderParser::LinkHeaderParameter name: "rel", value: "alternate">, #<LinkHeaderParser::LinkHeaderParameter name: "hreflang", value: "en-US">, #<LinkHeaderParser::LinkHeaderParameter name: "title", value: "sixtwothree.org: Posts">, #<LinkHeaderParser::LinkHeaderParameter name: "type", value: "application/rss+xml">]
132
- ```
133
-
134
- Note that the `Array` returned by the `link_parameters` method may include multiple `LinkHeaderParameter`s with the same name depending on the provided Link header. Certain methods on `LinkHeader` will return values from the first occurrence of a parameter name (e.g. `link_header.relations_string`) in accordance with [RFC-8288](https://tools.ietf.org/html/rfc8288).
33
+ See [USAGE.md](USAGE.md) for documentation of link-header-parser-ruby's features.
135
34
 
136
35
  ## Acknowledgments
137
36
 
data/USAGE.md ADDED
@@ -0,0 +1,143 @@
1
+ # Using link-header-parser-ruby
2
+
3
+ Before using link-header-parser-ruby, please read the [Getting Started](README.md#getting-started) and [Installation](README.md#installation) sections of the project's [README.md](README.md).
4
+
5
+ ## Parsing HTTP Link headers
6
+
7
+ With link-header-parser-ruby added to your project's `Gemfile` and installed, you may parse a HTTP Link headers from an HTTP response by doing something like:
8
+
9
+ ```ruby
10
+ require "net/http"
11
+ require "link-header-parser"
12
+
13
+ url = "https://sixtwothree.org"
14
+ link_headers = Net::HTTP.get_response(URI.parse(url)).get_fields("Link")
15
+
16
+ collection = LinkHeaderParser.parse(link_headers, base_uri: url)
17
+ ```
18
+
19
+ The `LinkHeaderParser.parse` method accepts two arguments:
20
+
21
+ 1. an `Array` of strings representing HTTP Link headers (e.g. `['</>; rel="home"', '</chapters/1>; anchor="#copyright"; rel="license"']`), and
22
+ 2. a `String` (or any object that responds to `#to_s`) representing the absolute URL of the resource providing the HTTP Link headers.
23
+
24
+ ## Working with a `LinkHeaderSet`
25
+
26
+ In the example above, `collection` is a `LinkHeaderSet`, an [`Array`](https://docs.ruby-lang.org/en/master/Array.html)-like object whose elements are one or more `LinkHeader` objects. The `LinkHeaderSet` is enhanced with a few custom methods noted below.
27
+
28
+ Building on the example `collection` from above:
29
+
30
+ ```ruby
31
+ collection.relation_types
32
+ #=> ["preconnect", "webmention"]
33
+
34
+ collection.group_by_relation_type
35
+ #=>
36
+ {
37
+ "preconnect" => [
38
+ #<LinkHeaderParser::LinkHeader @base_uri=#<Addressable::URI URI:https://sixtwothree.org> @target_string="https://assets.sixtwothree.org/">,
39
+ #<LinkHeaderParser::LinkHeader @base_uri=#<Addressable::URI URI:https://sixtwothree.org> @target_string="https://fonts.googleapis.com/">,
40
+ #<LinkHeaderParser::LinkHeader @base_uri=#<Addressable::URI URI:https://sixtwothree.org> @target_string="https://fonts.gstatic.com/">
41
+ ],
42
+ "webmention" => [
43
+ #<LinkHeaderParser::LinkHeader @base_uri=#<Addressable::URI URI:https://sixtwothree.org> @target_string="https://sixtwothree.org/webmentions">
44
+ ]
45
+ }
46
+ ```
47
+
48
+ ## Working with a `LinkHeader`
49
+
50
+ You may also directly parse a `LinkHeader` using `LinkHeaderParser::LinkHeader.parse` as shown in the examples below. The naming conventions for `LinkHeader`'s instance methods draws heavily on the terminology established in [RFC-5988](https://tools.ietf.org/html/rfc5988) and [RFC-8288](https://tools.ietf.org/html/rfc8288).
51
+
52
+ ### Link Target ([§ 3.1](https://tools.ietf.org/html/rfc8288#section-3.1))
53
+
54
+ ```ruby
55
+ link_header = LinkHeaderParser::LinkHeader.parse(%(</index.html>; rel="home"), base_uri: "https://website.example")
56
+
57
+ link_header.target_string
58
+ #=> "/index.html"
59
+
60
+ link_header.target_uri
61
+ #=> "https://website.example/index.html"
62
+ ```
63
+
64
+ The `target_string` method returns a string of the value between the opening and closing angle brackets at the beginning of the Link header. The `target_uri` method returns a string representing the resolved URL.
65
+
66
+ ### Link Context ([§ 3.2](https://tools.ietf.org/html/rfc8288#section-3.2))
67
+
68
+ ```ruby
69
+ link_header = LinkHeaderParser::LinkHeader.parse(%(</chapters/1>; anchor="#copyright"; rel="license"), base_uri: "https://website.example")
70
+
71
+ link_header.context_string
72
+ #=> "#copyright"
73
+
74
+ link_header.context_uri
75
+ #=> "https://website.example/chapters/1#copyright"
76
+ ```
77
+
78
+ The `anchor` parameter's value may be a fragment identifier (e.g. `#copyright`), a relative URL (e.g. `/copyright`), or an absolute URL (e.g. `https://website.example/copyright`). The `context_string` method returns the `anchor` parameter's value (when present) and defaults to the `target_string` value. The `context_uri` method returns a string representing the resolved URL.
79
+
80
+ ### Relation Type ([§ 3.3](https://tools.ietf.org/html/rfc8288#section-3.3))
81
+
82
+ ```ruby
83
+ link_header = LinkHeaderParser::LinkHeader.parse(%(</chapters/1>; rel="prev start"), base_uri: "https://website.example")
84
+
85
+ link_header.relations_string
86
+ #=> "prev start"
87
+
88
+ link_header.relation_types
89
+ #=> ["prev", "start"]
90
+ ```
91
+
92
+ ### Target Attributes ([Appendix B.3](https://tools.ietf.org/html/rfc8288#appendix-B.2))
93
+
94
+ ```ruby
95
+ link_header = LinkHeaderParser::LinkHeader.parse(%(</posts.rss>; rel="alternate"; hreflang="en-US"; title="Posts"; type="application/rss+xml"), base_uri: "https://website.example")
96
+
97
+ link_header.target_attributes
98
+ #=>
99
+ [
100
+ #<LinkHeaderParser::TargetAttribute @name="rel" @value="alternate">,
101
+ #<LinkHeaderParser::TargetAttribute @name="hreflang" @value="en-US">,
102
+ #<LinkHeaderParser::TargetAttribute @name="title" @value="Posts">,
103
+ #<LinkHeaderParser::TargetAttribute @name="type" @value="application/rss+xml">
104
+ ]
105
+ ```
106
+
107
+ > [!NOTE]
108
+ > The `target_attributes` method returns a `TargetAttributeSet`, an `Array`-like object. Its elements, instances of `TargetAttribute`, may include multiple `TargetAttribute`s with the same `name` depending on the provided HTTP Link header. Some methods on `LinkHeader` return values from _only the first occurrence_ of a target attribute's name (e.g. `link_header.relations_string`) in accordance with [RFC-8288](https://tools.ietf.org/html/rfc8288).
109
+
110
+ ## Working with a `TargetAttributeSet`
111
+
112
+ Beyond standard `Array` methods, the `TargetAttributeSet` adds a `find_by_name` method that may be used to locate the first occurrence of a particular target attribute. Building on the example above:
113
+
114
+ ```ruby
115
+ link_header.target_attributes.find_by_name("type")
116
+ #=> #<LinkHeaderParser::TargetAttribute @name="type" @value="application/rss+xml">
117
+ ```
118
+
119
+ ## Creating HTTP Link headers
120
+
121
+ From v7.0.0, link-header-parser-ruby may be used to created HTTP Link headers. It's maybe not the most efficient way to do this, but it's possible:
122
+
123
+ ```ruby
124
+ # Create a new `LinkHeader` for the `/feed.rss` target string.
125
+ link_header = LinkHeaderParser::LinkHeader.new("/feed.rss", base_uri: "https://website.example")
126
+
127
+ # Append a target attribute using `Array#<<`.
128
+ link_header.target_attributes << "rel=alternate"
129
+
130
+ # Append multiple target attributes using `Array#append`.
131
+ link_header.target_attributes.append(%(hreflang="en-US"), LinkHeaderParser::TargetAttribute.new(name: "type", value: "application/rss+xml"))
132
+
133
+ # Coerce the `LinkHeader` and its target attributes to a string.
134
+ link_header.to_s
135
+ #=> "</feed.rss>; rel=\"alternate\"; hreflang=\"en-US\"; type=\"application/rss+xml\""
136
+ ```
137
+
138
+ > [!TIP]
139
+ > When adding elements to either a `LinkHeaderSet` or `TargetAttributeSet`, you may pass a string or an instance of the related object (`LinkHeader` and `TargetAttribute` respectively). As necessary, link-header-parser will automatically create a new instance of the related object using the provided input before adding the new element.
140
+
141
+ ## Exception Handling
142
+
143
+ link-header-parser-ruby may raise an `Addressable::URI::InvalidURIError` exception if the value passed to the `base_uri` option is an invalid URI (e.g. `https:`).
@@ -1,28 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
4
- require "uri"
3
+ require "delegate"
4
+
5
+ require "addressable/uri"
5
6
 
6
7
  require_relative "link_header_parser/link_header"
7
- require_relative "link_header_parser/link_header_parameter"
8
- require_relative "link_header_parser/link_headers_collection"
8
+ require_relative "link_header_parser/link_header_set"
9
+ require_relative "link_header_parser/target_attribute"
10
+ require_relative "link_header_parser/target_attribute_set"
9
11
 
10
12
  module LinkHeaderParser
11
13
  # Parse an array of HTTP Link headers.
12
14
  #
13
- # Convenience method for {LinkHeaderParser::LinkHeadersCollection#initialize}.
15
+ # Convenience method for {LinkHeaderSet#initialize}.
14
16
  #
15
17
  # @example
16
18
  # require "net/http"
17
19
  #
18
- # url = "https://sixtwothree.org"
20
+ # url = "https://website.example"
19
21
  # link_headers = Net::HTTP.get_response(URI.parse(url)).get_fields("Link")
20
22
  #
21
- # LinkHeaderParser.parse(link_headers, base: url)
23
+ # LinkHeaderParser.parse(link_headers, base_uri: url)
24
+ #
25
+ # @param (see LinkHeaderSet#initialize)
22
26
  #
23
- # @param (see LinkHeaderParser::LinkHeadersCollection#initialize)
24
- # @return (see LinkHeaderParser::LinkHeadersCollection#initialize)
25
- def self.parse(*headers, base:)
26
- LinkHeadersCollection.new(*headers, base: base)
27
+ # @return [LinkHeaderSet]
28
+ def self.parse(*field_values, base_uri:)
29
+ LinkHeaderSet.new(*field_values, base_uri: base_uri)
27
30
  end
28
31
  end
@@ -8,21 +8,58 @@ module LinkHeaderParser
8
8
  PARAMETERS_REGEXP_PATTERN = /(?<!;)\s*[^;]+/.freeze
9
9
  private_constant :PARAMETERS_REGEXP_PATTERN
10
10
 
11
- # The +String+ value used to create this {LinkHeader}.
11
+ # Parse a +field_value+ string into a {LinkHeader}.
12
12
  #
13
- # @return [String]
14
- attr_reader :field_value
13
+ # @example
14
+ # url = "https://website.example"
15
+ # link_header = %(</webmentions>; rel="webmention")
16
+ #
17
+ # LinkHeaderParser::LinkHeader.parse(link_header, base_uri: url)
18
+ #
19
+ # @param field_value [String, #to_s]
20
+ # @param base_uri (see #initialize)
21
+ #
22
+ # @return [self]
23
+ def self.parse(field_value, base_uri:)
24
+ return field_value if field_value.is_a?(self)
25
+
26
+ match_data = field_value.to_s.match(FIELD_VALUE_REGEXP_PATTERN)
27
+
28
+ new(match_data[:target_string], *match_data[:parameters].scan(PARAMETERS_REGEXP_PATTERN), base_uri: base_uri)
29
+ end
30
+
31
+ # @return [Addressable::URI]
32
+ attr_reader :base_uri
33
+
34
+ # The parsed parameters for this Link header extracted from +field_value+.
35
+ #
36
+ # @see https://tools.ietf.org/html/rfc8288#appendix-B.2
37
+ # IETF RFC 8288 Web Linking Appendix B.2.2.4 Parsing a Link Field Value
38
+ #
39
+ # @return [TargetAttributeSet]
40
+ attr_reader :target_attributes
15
41
 
16
- # Create a new parsed Link header.
42
+ # The target URL for this Link header extracted from +field_value+.
17
43
  #
18
44
  # @see https://tools.ietf.org/html/rfc8288#appendix-B.2
19
- # IETF RFC 8288 Web Linking Appendix B.2 Parsing a Link Field Value
45
+ # IETF RFC 8288 Web Linking Appendix B.2.2.4 Parsing a Link Field Value
46
+ #
47
+ # @return [String]
48
+ attr_reader :target_string
49
+
50
+ # Create a new {LinkHeader}.
20
51
  #
21
- # @param field_value [String, #to_str]
22
- # @param base [String, #to_str]
23
- def initialize(field_value, base:)
24
- @field_value = field_value.to_str
25
- @base = base.to_str
52
+ # @see LinkParameter.parse
53
+ #
54
+ # @param target_string [String, #to_s]
55
+ # @param link_parameters [Array<String, #to_s>]
56
+ # @param base_uri [String, #to_s]
57
+ #
58
+ # @raise [Addressable::URI::InvalidURIError]
59
+ def initialize(target_string, *link_parameters, base_uri:)
60
+ @target_string = target_string.to_s.strip
61
+ @target_attributes = TargetAttributeSet.new(*link_parameters)
62
+ @base_uri = Addressable::URI.parse(base_uri)
26
63
  end
27
64
 
28
65
  # The context URL for this Link header extracted from +field_value+ (or
@@ -33,7 +70,7 @@ module LinkHeaderParser
33
70
  #
34
71
  # @return [String]
35
72
  def context_string
36
- @context_string ||= grouped_link_parameters[:anchor]&.first || target_string
73
+ target_attributes.find_by_name("anchor")&.value || target_string
37
74
  end
38
75
 
39
76
  # The resolved context URL for this Link header.
@@ -43,38 +80,16 @@ module LinkHeaderParser
43
80
  #
44
81
  # @return [String]
45
82
  def context_uri
46
- @context_uri ||= URI.join(target_uri, context_string).normalize.to_s
83
+ Addressable::URI.join(target_uri, context_string).to_s
47
84
  end
48
85
 
49
86
  # @return [String]
50
87
  def inspect
51
- "#<#{self.class.name}:#{format("%#0x", object_id)} " \
52
- "target_uri: #{target_uri.inspect}, " \
53
- "relation_types: #{relation_types.inspect}>"
54
- end
55
-
56
- # The parsed parameters for this Link header extracted from +field_value+.
57
- #
58
- # @see https://tools.ietf.org/html/rfc8288#appendix-B.3
59
- # IETF RFC 8288 Web Linking Appendix B.3 Parsing Parameters
60
- #
61
- # @return [Array<LinkHeaderParser::LinkHeaderParameter>]
62
- def link_parameters
63
- @link_parameters ||= field_value_match_data[:parameters]
64
- .scan(PARAMETERS_REGEXP_PATTERN)
65
- .map { |parameter| LinkHeaderParameter.new(parameter.strip) }
66
- end
67
-
68
- # The +relations_string+ value returned as an +Array+.
69
- #
70
- # @see LinkHeader#relations_string
71
- #
72
- # @see https://tools.ietf.org/html/rfc8288#appendix-B.2
73
- # IETF RFC 8288 Web Linking Appendix B.2.2.10 and Appendix B.2.2.17.1 Parsing a Link Field Value
74
- #
75
- # @return [Array<String>]
76
- def relation_types
77
- @relation_types ||= relations_string.split.map(&:downcase)
88
+ format "#<%<class>s:%<id>#0x @base_uri=%<base_uri>s @target_string=%<target_string>s>",
89
+ class: self.class,
90
+ id: object_id << 1,
91
+ base_uri: base_uri.inspect,
92
+ target_string: target_string.inspect
78
93
  end
79
94
 
80
95
  # The relation types for this Link header extracted from +field_value+.
@@ -84,17 +99,19 @@ module LinkHeaderParser
84
99
  #
85
100
  # @return [String]
86
101
  def relations_string
87
- @relations_string ||= grouped_link_parameters[:rel]&.first.to_s
102
+ target_attributes.find_by_name("rel")&.value.to_s
88
103
  end
89
104
 
90
- # The target URL for this Link header extracted from +field_value+
105
+ # The +relations_string+ value returned as an array.
106
+ #
107
+ # @see #relations_string
91
108
  #
92
109
  # @see https://tools.ietf.org/html/rfc8288#appendix-B.2
93
- # IETF RFC 8288 Web Linking Appendix B.2.2.4 Parsing a Link Field Value
110
+ # IETF RFC 8288 Web Linking Appendix B.2.2.10 and Appendix B.2.2.17.1 Parsing a Link Field Value
94
111
  #
95
- # @return [String]
96
- def target_string
97
- @target_string ||= field_value_match_data[:target_string]
112
+ # @return [Array<String>]
113
+ def relation_types
114
+ relations_string.split.map(&:downcase)
98
115
  end
99
116
 
100
117
  # The resolved target URL for this Link header.
@@ -104,40 +121,14 @@ module LinkHeaderParser
104
121
  #
105
122
  # @return [String]
106
123
  def target_uri
107
- @target_uri ||= URI.join(base, target_string).normalize.to_s
108
- end
109
-
110
- # Return a +Hash+ representation of this {LinkHeader}.
111
- #
112
- # @return [Hash{Symbol => String, Array, Hash{Symbol => Array}}]
113
- def to_hash
114
- {
115
- target_string: target_string,
116
- target_uri: target_uri,
117
- context_string: context_string,
118
- context_uri: context_uri,
119
- relations_string: relations_string,
120
- relation_types: relation_types,
121
- link_parameters: grouped_link_parameters,
122
- }
124
+ @target_uri ||= base_uri.join(target_string).to_s
123
125
  end
124
126
 
125
- alias to_h to_hash
126
-
127
- private
128
-
129
- attr_reader :base
130
-
131
- def field_value_match_data
132
- @field_value_match_data ||= field_value.match(FIELD_VALUE_REGEXP_PATTERN)
127
+ # @return [String]
128
+ def to_str
129
+ "<#{target_string}>; #{target_attributes}"
133
130
  end
134
131
 
135
- def grouped_link_parameters
136
- @grouped_link_parameters ||= link_parameters
137
- .map(&:to_a)
138
- .group_by(&:shift)
139
- .transform_keys(&:to_sym)
140
- .transform_values(&:flatten)
141
- end
132
+ alias to_s to_str
142
133
  end
143
134
  end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkHeaderParser
4
+ class LinkHeaderSet < SimpleDelegator
5
+ # Create a new {LinkHeaderSet} from an array of HTTP Link header
6
+ # +field_values+.
7
+ #
8
+ # @example
9
+ # require "net/http"
10
+ #
11
+ # url = "https://website.example"
12
+ # link_headers = Net::HTTP.get_response(URI.parse(url)).get_fields("Link")
13
+ #
14
+ # LinkHeaderParser::LinkHeaderSet.new(link_headers, base_uri: url)
15
+ #
16
+ # @param field_values [Array<String>]
17
+ # @param base_uri (see LinkHeader.parse)
18
+ def initialize(*field_values, base_uri:)
19
+ @base_uri = base_uri
20
+
21
+ super(parsed_link_headers_from(field_values))
22
+ end
23
+
24
+ # Append an HTTP Link header to this {LinkHeaderSet}.
25
+ #
26
+ # @param field_value [String]
27
+ #
28
+ # @return [self]
29
+ def <<(field_value)
30
+ super(*parsed_link_headers_from(field_value))
31
+ end
32
+
33
+ # Append one or more HTTP Link headers to this {LinkHeaderSet}.
34
+ #
35
+ # @param field_values [Array<String>]
36
+ #
37
+ # @return [self]
38
+ def append(*field_values)
39
+ super(*parsed_link_headers_from(field_values))
40
+ end
41
+
42
+ # Obtain a hash of this {LinkHeaderSet}'s {LinkHeader}s grouped by their
43
+ # relation types.
44
+ #
45
+ # @return [Hash{String => LinkHeaderSet<LinkHeader>}]
46
+ def group_by_relation_type
47
+ obj = relation_types.to_h { |relation_type| [relation_type, LinkHeaderSet.new(base_uri: base_uri)] }
48
+
49
+ each_with_object(obj) do |link_header, hash|
50
+ link_header.relation_types.each { |relation_type| hash[relation_type] << link_header }
51
+ end
52
+ end
53
+
54
+ # @return [String]
55
+ def inspect
56
+ format "#<%<class>s:%<id>#0x @base_uri=%<base_uri>s>",
57
+ class: self.class,
58
+ id: object_id << 1,
59
+ base_uri: base_uri.inspect
60
+ end
61
+
62
+ # Retrieve a unique sorted array of this collection's {LinkHeader}
63
+ # relation types.
64
+ #
65
+ # @return [Array<String>]
66
+ def relation_types
67
+ Set.new(flat_map(&:relation_types)).to_a.sort
68
+ end
69
+
70
+ # @return [String]
71
+ def to_str
72
+ join(", ")
73
+ end
74
+
75
+ alias to_s to_str
76
+
77
+ # Prepend one or more HTTP Link headers to this {LinkHeaderSet}.
78
+ #
79
+ # @param field_values [Array<String>]
80
+ #
81
+ # @return [self]
82
+ def unshift(*field_values)
83
+ super(*parsed_link_headers_from(field_values))
84
+ end
85
+
86
+ private
87
+
88
+ attr_reader :base_uri
89
+
90
+ # @return [self]
91
+ def parsed_link_headers_from(*field_values)
92
+ field_values.flatten!
93
+ field_values.map { |field_value| LinkHeader.parse(field_value, base_uri: base_uri) }
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkHeaderParser
4
+ class TargetAttribute
5
+ # @see https://tools.ietf.org/html/rfc8288#appendix-B.3
6
+ # IETF RFC 8288 Web Linking Appendix B.3 Parsing Parameters
7
+ # @see https://tools.ietf.org/html/rfc8288#appendix-B.4
8
+ # IETF RFC 8288 Web Linking Appendix B.4 Parsing a Quoted String
9
+ PARAMETER_REGEXP_PATTERN = /^(?<name>.+?)(?:=\s*"?(?<value>.*?)"?)?$/.freeze
10
+ private_constant :PARAMETER_REGEXP_PATTERN
11
+
12
+ # Parse a +link_parameter+ string into a {TargetAttribute}.
13
+ #
14
+ # @example
15
+ # LinkHeaderParser::TargetAttribute.parse(%(rel="webmention"))
16
+ #
17
+ # @param link_parameter [String, #to_s]
18
+ #
19
+ # @return [self]
20
+ def self.parse(link_parameter)
21
+ return link_parameter if link_parameter.is_a?(self)
22
+
23
+ new(**link_parameter.to_s.match(PARAMETER_REGEXP_PATTERN).named_captures.transform_keys(&:to_sym))
24
+ end
25
+
26
+ # @return [String]
27
+ attr_reader :name
28
+
29
+ # @return [String]
30
+ attr_reader :value
31
+
32
+ # Create a new {TargetAttribute}.
33
+ #
34
+ # @see https://tools.ietf.org/html/rfc8288#appendix-B.2
35
+ # IETF RFC 8288 Web Linking Appendix B.2.2.4 Parsing a Link Field Value
36
+ #
37
+ # @param name [String, #to_s]
38
+ # @param value [String, #to_s]
39
+ def initialize(name:, value:)
40
+ @name = name.to_s.strip.downcase
41
+ @value = value.to_s.strip
42
+ end
43
+
44
+ # @return [String]
45
+ def inspect
46
+ format "#<%<class>s:%<id>#0x @name=%<name>s @value=%<value>s>",
47
+ class: self.class,
48
+ id: object_id << 1,
49
+ name: name.inspect,
50
+ value: value.inspect
51
+ end
52
+
53
+ # @return [String]
54
+ def to_str
55
+ %(#{name}="#{value}")
56
+ end
57
+
58
+ alias to_s to_str
59
+ end
60
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkHeaderParser
4
+ class TargetAttributeSet < SimpleDelegator
5
+ # Create a new {TargetAttributeSet} from an array of HTTP Link header
6
+ # +link_parameters+.
7
+ #
8
+ # @param link_parameters [Array<String>]
9
+ def initialize(*link_parameters)
10
+ super(parsed_target_attributes_from(link_parameters))
11
+ end
12
+
13
+ # Append an HTTP Link header parameter to this {TargetAttributeSet}.
14
+ #
15
+ # @param parameter_value [String]
16
+ #
17
+ # @return [self]
18
+ def <<(parameter_value)
19
+ super(*parsed_target_attributes_from(parameter_value))
20
+ end
21
+
22
+ # Append one or more HTTP Link header parameters to this
23
+ # {TargetAttributeSet}.
24
+ #
25
+ # @param link_parameters [Array<String>]
26
+ #
27
+ # @return [self]
28
+ def append(*link_parameters)
29
+ super(*parsed_target_attributes_from(link_parameters))
30
+ end
31
+
32
+ # Find and retrieve a {TargetAttribute} from this {TargetAttributeSet} by
33
+ # its +#name+.
34
+ #
35
+ # @return [TargetAttribute, nil]
36
+ def find_by_name(name)
37
+ find { |target_attribute| target_attribute.name == name }
38
+ end
39
+
40
+ # @return [String]
41
+ def inspect
42
+ format "#<%<class>s:%<id>#0x>",
43
+ class: self.class,
44
+ id: object_id << 1
45
+ end
46
+
47
+ # @return [String]
48
+ def to_str
49
+ join("; ")
50
+ end
51
+
52
+ alias to_s to_str
53
+
54
+ # Prepend one or more HTTP Link header parameter to this
55
+ # {TargetAttributeSet}.
56
+ #
57
+ # @param link_parameters [Array<String>]
58
+ #
59
+ # @return [self]
60
+ def unshift(*link_parameters)
61
+ super(*parsed_target_attributes_from(link_parameters))
62
+ end
63
+
64
+ private
65
+
66
+ # @return [self]
67
+ def parsed_target_attributes_from(*link_parameters)
68
+ link_parameters.flatten!
69
+ link_parameters.map { |parameter_value| TargetAttribute.parse(parameter_value) }
70
+ end
71
+ end
72
+ end
@@ -4,7 +4,7 @@ Gem::Specification.new do |spec|
4
4
  spec.required_ruby_version = ">= 2.6"
5
5
 
6
6
  spec.name = "link-header-parser"
7
- spec.version = "6.1.1"
7
+ spec.version = "7.0.0"
8
8
  spec.authors = ["Jason Garber"]
9
9
  spec.email = ["jason@sixtwothree.org"]
10
10
 
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
14
14
  spec.license = "MIT"
15
15
 
16
16
  spec.files = Dir["lib/**/*"].reject { |f| File.directory?(f) }
17
- spec.files += ["LICENSE", "CHANGELOG.md", "README.md"]
17
+ spec.files += ["LICENSE", "CHANGELOG.md", "README.md", "USAGE.md"]
18
18
  spec.files += ["link-header-parser.gemspec"]
19
19
 
20
20
  spec.require_paths = ["lib"]
@@ -27,4 +27,6 @@ Gem::Specification.new do |spec|
27
27
  "rubygems_mfa_required" => "true",
28
28
  "source_code_uri" => "#{spec.homepage}/src/tag/v#{spec.version}",
29
29
  }
30
+
31
+ spec.add_dependency "addressable", "~> 2.8"
30
32
  end
metadata CHANGED
@@ -1,14 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: link-header-parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.1.1
4
+ version: 7.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason Garber
8
8
  bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: addressable
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.8'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.8'
12
26
  description: Parse HTTP Link headers.
13
27
  email:
14
28
  - jason@sixtwothree.org
@@ -19,21 +33,23 @@ files:
19
33
  - CHANGELOG.md
20
34
  - LICENSE
21
35
  - README.md
36
+ - USAGE.md
22
37
  - lib/link-header-parser.rb
23
38
  - lib/link_header_parser/link_header.rb
24
- - lib/link_header_parser/link_header_parameter.rb
25
- - lib/link_header_parser/link_headers_collection.rb
39
+ - lib/link_header_parser/link_header_set.rb
40
+ - lib/link_header_parser/target_attribute.rb
41
+ - lib/link_header_parser/target_attribute_set.rb
26
42
  - link-header-parser.gemspec
27
43
  homepage: https://codeberg.org/jgarber/link-header-parser-ruby
28
44
  licenses:
29
45
  - MIT
30
46
  metadata:
31
47
  bug_tracker_uri: https://codeberg.org/jgarber/link-header-parser-ruby/issues
32
- changelog_uri: https://codeberg.org/jgarber/link-header-parser-ruby/releases/tag/v6.1.1
33
- documentation_uri: https://rubydoc.info/gems/link-header-parser/6.1.1
48
+ changelog_uri: https://codeberg.org/jgarber/link-header-parser-ruby/releases/tag/v7.0.0
49
+ documentation_uri: https://rubydoc.info/gems/link-header-parser/7.0.0
34
50
  homepage_uri: https://codeberg.org/jgarber/link-header-parser-ruby
35
51
  rubygems_mfa_required: 'true'
36
- source_code_uri: https://codeberg.org/jgarber/link-header-parser-ruby/src/tag/v6.1.1
52
+ source_code_uri: https://codeberg.org/jgarber/link-header-parser-ruby/src/tag/v7.0.0
37
53
  rdoc_options: []
38
54
  require_paths:
39
55
  - lib
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LinkHeaderParser
4
- class LinkHeaderParameter
5
- PARAMETER_REGEXP_PATTERN = /^(?<name>.+?)(?:="?(?<value>.*?)"?)?$/.freeze
6
- private_constant :PARAMETER_REGEXP_PATTERN
7
-
8
- # The +String+ value used to create this {LinkHeaderParameter}.
9
- #
10
- # @return [String]
11
- attr_reader :parameter
12
-
13
- # Create a new parsed Link header parameter.
14
- #
15
- # @param parameter [String, #to_str]
16
- def initialize(parameter)
17
- @parameter = parameter.to_str
18
- end
19
-
20
- # @return [String]
21
- def inspect
22
- "#<#{self.class.name}:#{format("%#0x", object_id)} " \
23
- "name: #{name.inspect}, " \
24
- "value: #{value.inspect}>"
25
- end
26
-
27
- # @see https://tools.ietf.org/html/rfc8288#appendix-B.3
28
- # IETF RFC 8288 Web Linking Appendix B.3.2.9 Parsing Parameters
29
- #
30
- # @return [String]
31
- def name
32
- @name ||= parameter_match_data[:name].downcase
33
- end
34
-
35
- # @see https://tools.ietf.org/html/rfc8288#appendix-B.3
36
- # IETF RFC 8288 Web Linking Appendix B.3.2.8 Parsing Parameters
37
- #
38
- # @return [String]
39
- def value
40
- @value ||= parameter_match_data[:value].to_s
41
- end
42
-
43
- # Return an +Array+ representation of this {LinkHeaderParameter}.
44
- #
45
- # @return [Array<String>]
46
- def to_ary
47
- [name, value]
48
- end
49
-
50
- alias to_a to_ary
51
-
52
- private
53
-
54
- def parameter_match_data
55
- @parameter_match_data ||= parameter.match(PARAMETER_REGEXP_PATTERN)
56
- end
57
- end
58
- end
@@ -1,86 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LinkHeaderParser
4
- class LinkHeadersCollection
5
- extend Forwardable
6
-
7
- include Enumerable
8
-
9
- def_delegators :members, :[], :<<, :each, :last, :length, :push
10
-
11
- # The +Array+ of HTTP Link headers used to create this
12
- # {LinkHeadersCollection}.
13
- #
14
- # @return [Array<String>]
15
- attr_reader :headers
16
-
17
- # Parse an array of HTTP Link headers.
18
- #
19
- # @param headers [Array<String, #to_str>]
20
- # @param base [String, #to_str]
21
- def initialize(*headers, base:)
22
- headers = headers.to_ary
23
-
24
- headers.flatten!
25
-
26
- @headers = headers.map!(&:to_str)
27
- @base = base.to_str
28
-
29
- push(*distinct_link_headers)
30
- end
31
-
32
- # Retrieve a +Hash+ of this collection's {LinkHeader}s grouped by their
33
- # relation type(s).
34
- #
35
- # @return [Hash{Symbol => Array<LinkHeaderParser::LinkHeader>}]
36
- def group_by_relation_type
37
- relation_types.to_h do |relation_type|
38
- [relation_type.to_sym, select_by_relation_type(relation_type)]
39
- end
40
- end
41
-
42
- # @return [String]
43
- def inspect
44
- "#<#{self.class.name}:#{format("%#0x", object_id)} " \
45
- "headers: #{headers.inspect}, " \
46
- "relation_types: #{relation_types.inspect}>"
47
- end
48
-
49
- # Retrieve a unique sorted +Array+ of this collection's {LinkHeader}
50
- # relation types.
51
- #
52
- # @return [Array<String>]
53
- def relation_types
54
- @relation_types ||= Set.new(flat_map(&:relation_types)).to_a.sort
55
- end
56
-
57
- # Return an +Array+ representation of this {LinkHeadersCollection}.
58
- #
59
- # @see LinkHeader#to_hash
60
- #
61
- # @return [Array<Hash>}>]
62
- def to_ary
63
- map(&:to_hash)
64
- end
65
-
66
- alias to_a to_ary
67
-
68
- private
69
-
70
- attr_reader :base
71
-
72
- def distinct_link_headers
73
- headers
74
- .flat_map { |header| header.split(/,(?=[\s|<])/) }
75
- .map { |header| LinkHeader.new(header.strip, base: base) }
76
- end
77
-
78
- def members
79
- @members ||= []
80
- end
81
-
82
- def select_by_relation_type(relation_type)
83
- select { |member| member.relation_types.include?(relation_type) }
84
- end
85
- end
86
- end