nitlink 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/nitlink.rb +4 -0
- data/lib/nitlink/exceptions.rb +6 -0
- data/lib/nitlink/link.rb +4 -0
- data/lib/nitlink/link_collection.rb +16 -0
- data/lib/nitlink/param_decoder.rb +24 -0
- data/lib/nitlink/param_extractor.rb +58 -0
- data/lib/nitlink/parser.rb +101 -0
- data/lib/nitlink/response.rb +14 -0
- data/lib/nitlink/response_normalizer.rb +75 -0
- data/lib/nitlink/splitter.rb +40 -0
- data/lib/nitlink/version.rb +3 -0
- metadata +142 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 76d7bf9f8436e8b45762a03ae9a73b613db238f7
|
4
|
+
data.tar.gz: 4af8d8f6a3c09991d4254355be30c8e9148a9551
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 29b158182dad5e2199db2b2620affc893ec8af246df861ba91cf41e7119ca1515cf17d6830995fa7f6ffc478ba08faab7b067de829a03a79ec5d2ced9d80d409
|
7
|
+
data.tar.gz: e789dc5dafd4415e9b335be5e33b84503564b0a8db932ea203222374094e7f12ac9a840f4fc166c2e52b75e63555094e0d49f1f42a4ab261f012293df8bc3ec6
|
data/lib/nitlink.rb
ADDED
data/lib/nitlink/link.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'hashwithindifferentaccess'
|
2
|
+
|
3
|
+
module Nitlink
|
4
|
+
class LinkCollection < Array
|
5
|
+
def by_rel(relation_type)
|
6
|
+
raise ArgumentError.new('relation_type cannot be blank') if (!relation_type || relation_type.empty?)
|
7
|
+
find { |link| link.relation_type == relation_type.downcase.to_s }
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_h
|
11
|
+
hash = HashWithIndifferentAccess.new
|
12
|
+
each { |link| hash[link.relation_type] ||= link }
|
13
|
+
hash
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module Nitlink
|
4
|
+
class ParamDecoder
|
5
|
+
def decode(param_value)
|
6
|
+
charset, language, value_chars = param_value.split("'")
|
7
|
+
|
8
|
+
raise syntax_error(param_value) unless charset && language && value_chars
|
9
|
+
raise wrong_charset(charset) unless charset.downcase == 'utf-8'
|
10
|
+
|
11
|
+
CGI.unescape(value_chars)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def syntax_error(val)
|
17
|
+
EncodedParamSyntaxError.new(%Q{Syntax error decoding encoded parameter value "#{ val }", must be in the form: charset "'" [ language ] "'" value-chars})
|
18
|
+
end
|
19
|
+
|
20
|
+
def wrong_charset(charset)
|
21
|
+
UnsupportedCharsetError.new("Invalid charset #{charset}, encoded parameter values must use the UTF-8 character encoding")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Nitlink
|
2
|
+
class ParamExtractor
|
3
|
+
QUOTED_VALUE = /\A"(.*)"\Z/m
|
4
|
+
QUOTED_PAIR = /\\./m
|
5
|
+
|
6
|
+
LEADING_OWS = /\A[\x09\x20]+/
|
7
|
+
TRAILING_OWS = /[\x09\x20]+\Z/
|
8
|
+
|
9
|
+
def extract(rest)
|
10
|
+
@rest = rest
|
11
|
+
parameter_strings = Splitter.new(rest).split_on_unquoted(';')
|
12
|
+
raw_params = parameter_strings.map do |parameter_str|
|
13
|
+
strip_ows(parameter_str).split('=', 2)
|
14
|
+
end
|
15
|
+
|
16
|
+
return format(raw_params)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def format(raw_params)
|
22
|
+
raw_params.map do |raw_param_name, raw_param_value|
|
23
|
+
next if !raw_param_name
|
24
|
+
param_name = rstrip_ows(raw_param_name.downcase)
|
25
|
+
|
26
|
+
if raw_param_value
|
27
|
+
param_value = lstrip_ows(raw_param_value)
|
28
|
+
param_value = format_quoted_value(param_value) if quoted?(param_value)
|
29
|
+
else
|
30
|
+
param_value = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
[param_name, param_value]
|
34
|
+
end.compact
|
35
|
+
end
|
36
|
+
|
37
|
+
def format_quoted_value(quoted_value)
|
38
|
+
without_quotes = quoted_value.strip[QUOTED_VALUE, 1]
|
39
|
+
without_quotes.gsub(QUOTED_PAIR) { |match| match.chars.to_a.last }
|
40
|
+
end
|
41
|
+
|
42
|
+
def quoted?(param_value)
|
43
|
+
param_value =~ QUOTED_VALUE
|
44
|
+
end
|
45
|
+
|
46
|
+
def lstrip_ows(str)
|
47
|
+
str.gsub(LEADING_OWS, '')
|
48
|
+
end
|
49
|
+
|
50
|
+
def rstrip_ows(str)
|
51
|
+
str.gsub(TRAILING_OWS, '')
|
52
|
+
end
|
53
|
+
|
54
|
+
def strip_ows(str)
|
55
|
+
rstrip_ows(lstrip_ows str)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require_relative './exceptions'
|
2
|
+
require_relative './splitter'
|
3
|
+
require_relative './link_collection'
|
4
|
+
require_relative './link'
|
5
|
+
require_relative './response_normalizer'
|
6
|
+
require_relative './param_extractor'
|
7
|
+
require_relative './param_decoder'
|
8
|
+
|
9
|
+
module Nitlink
|
10
|
+
class Parser
|
11
|
+
SINGLE_LINK = /\A\s*<([^>]*)>(.*)/
|
12
|
+
RWS = /[\x09\x20]+/
|
13
|
+
|
14
|
+
attr_reader :options
|
15
|
+
|
16
|
+
def parse(response, http_method = 'GET')
|
17
|
+
@http_method = http_method
|
18
|
+
@request_uri, @status, link_header, @content_location_header = ResponseNormalizer.new.metadata(response)
|
19
|
+
|
20
|
+
links = LinkCollection.new
|
21
|
+
return links unless link_header
|
22
|
+
|
23
|
+
unfolded_header = link_header.gsub(/\r?\n[\x20\x09]+/, '')
|
24
|
+
link_strings = Splitter.new(unfolded_header).split_on_unquoted(',')
|
25
|
+
|
26
|
+
parse_links(link_strings, links)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def parse_links(link_strings, link_collection)
|
32
|
+
link_strings.each do |link_string|
|
33
|
+
well_formed, target_string, rest = link_string.match(SINGLE_LINK).to_a
|
34
|
+
raise malformed(link_string) unless well_formed
|
35
|
+
|
36
|
+
link_parameters = ParamExtractor.new.extract(rest)
|
37
|
+
create_links(target_string, link_parameters).each { |link| link_collection.push(link) }
|
38
|
+
end
|
39
|
+
|
40
|
+
link_collection
|
41
|
+
end
|
42
|
+
|
43
|
+
def create_links(target_string, link_parameters)
|
44
|
+
target, relation_types, context, target_attributes = link_attributes(target_string, link_parameters)
|
45
|
+
relation_types.map do |relation_type|
|
46
|
+
Link.new(target, relation_type, context, target_attributes)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def link_attributes(target_string, link_parameters)
|
51
|
+
target = @request_uri.merge(target_string)
|
52
|
+
|
53
|
+
relations_string = first_match(link_parameters, 'rel') || ''
|
54
|
+
relation_types = relations_string.split(RWS)
|
55
|
+
|
56
|
+
context_string = first_match(link_parameters, 'anchor') || identity
|
57
|
+
context = (context_string && @request_uri.scheme) ? @request_uri.merge(context_string) : nil
|
58
|
+
|
59
|
+
target_attributes = extract_target_attributes(link_parameters)
|
60
|
+
[target, relation_types, context, target_attributes]
|
61
|
+
end
|
62
|
+
|
63
|
+
def first_match(link_parameters, param_name)
|
64
|
+
(link_parameters.find { |name, value| name == param_name } || []).last
|
65
|
+
end
|
66
|
+
|
67
|
+
def extract_target_attributes(link_parameters)
|
68
|
+
target_attributes = []
|
69
|
+
link_parameters.each do |param_name, param_value|
|
70
|
+
next if %(rel anchor).include?(param_name)
|
71
|
+
next if %(media title title* type).include?(param_name) && first_match(target_attributes, param_name)
|
72
|
+
|
73
|
+
begin
|
74
|
+
param_value = decode(param_value) if param_name.end_with?('*')
|
75
|
+
rescue EncodedParamSyntaxError, UnsupportedCharsetError
|
76
|
+
next
|
77
|
+
end
|
78
|
+
|
79
|
+
target_attributes.push [param_name, param_value]
|
80
|
+
end
|
81
|
+
|
82
|
+
Hash[target_attributes]
|
83
|
+
end
|
84
|
+
|
85
|
+
def malformed(link_string)
|
86
|
+
MalformedLinkHeaderError.new("Malformed link header (#{ link_string })")
|
87
|
+
end
|
88
|
+
|
89
|
+
def decode(param_value)
|
90
|
+
ParamDecoder.new.decode(param_value)
|
91
|
+
end
|
92
|
+
|
93
|
+
def identity
|
94
|
+
if %w(GET HEAD).include?(@http_method.upcase) && [200, 203, 204, 206, 304].include?(@status)
|
95
|
+
@request_uri
|
96
|
+
else
|
97
|
+
@content_location_header
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative '../nitlink'
|
2
|
+
|
3
|
+
module Nitlink
|
4
|
+
module ResponseDecorator
|
5
|
+
def links
|
6
|
+
Nitlink::Parser.new.parse(self)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
decoratable_responses = ['Curl::Easy', 'Excon::Response', 'Faraday::Response', 'HTTP::Message', 'HTTP::Response', 'HTTParty::Response', 'Net::HTTPResponse', 'Patron::Response', 'RestClient::Response', 'Typhoeus::Response', 'Unirest::HttpResponse']
|
12
|
+
decoratable_responses.select { |klass| Object.const_defined?(klass) }.each do |klass|
|
13
|
+
Object.const_get(klass).class_eval { include Nitlink::ResponseDecorator }
|
14
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'hashwithindifferentaccess'
|
3
|
+
|
4
|
+
module Nitlink
|
5
|
+
class ResponseNormalizer
|
6
|
+
def metadata(response)
|
7
|
+
response_class = response.class.name
|
8
|
+
|
9
|
+
uri, status, (link, content_location) = case response_class
|
10
|
+
when 'Curl::Easy'
|
11
|
+
[response.url, response.response_code, grab_headers(headers_from_string response.header_str)]
|
12
|
+
when 'Excon::Response'
|
13
|
+
scheme = response.port == 443 ? 'https' : 'http'
|
14
|
+
# We have to reconstruct to URL annoyingly
|
15
|
+
uri = URI::HTTP.new(scheme, nil, response.host, nil, nil, response.path, nil, nil, nil)
|
16
|
+
|
17
|
+
[uri, response.status, grab_headers(response.headers)]
|
18
|
+
when 'Faraday::Response'
|
19
|
+
response = response.to_hash
|
20
|
+
[response[:url], response[:status], grab_headers(response[:response_headers])]
|
21
|
+
when 'HTTP::Message'
|
22
|
+
[response.header.request_uri, response.status, grab_headers(Hash[response.header.all])]
|
23
|
+
when 'HTTP::Response'
|
24
|
+
[response.uri, response.status, grab_headers(response.headers.to_h)]
|
25
|
+
when 'HTTParty::Response'
|
26
|
+
[response.request.uri, response.code, grab_headers(response.response.to_hash)]
|
27
|
+
when 'Patron::Response'
|
28
|
+
[response.url, response.status, grab_headers(response.headers)]
|
29
|
+
when 'RestClient::Response'
|
30
|
+
[response.request.url, response.code, grab_headers(response.net_http_res.to_hash)]
|
31
|
+
when 'Tempfile', 'StringIO'
|
32
|
+
# ↑ returned by OpenURI
|
33
|
+
[response.base_uri, response.status[0], grab_headers(response.meta)]
|
34
|
+
when 'Typhoeus::Response'
|
35
|
+
[response.request.base_url, response.code, grab_headers(response.headers)]
|
36
|
+
# :nocov:
|
37
|
+
when 'Unirest::HttpResponse'
|
38
|
+
return metadata(response.raw_body)
|
39
|
+
# :nocov:
|
40
|
+
when 'Hash'
|
41
|
+
response = HashWithIndifferentAccess.new(response)
|
42
|
+
response[:headers] = headers_from_string(response[:headers]) if String === response[:headers]
|
43
|
+
|
44
|
+
[response[:request_uri], response[:status], grab_headers(response[:headers])]
|
45
|
+
else
|
46
|
+
if defined?(Net::HTTPResponse) && Net::HTTPResponse === response
|
47
|
+
[response.uri, response.code, grab_headers(response.to_hash)]
|
48
|
+
else
|
49
|
+
raise unknown_type(response)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
[URI.parse(uri.to_s), (status ? Integer(status) : status), link, content_location]
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def headers_from_string(header_str)
|
59
|
+
headers = header_str.split("\n").map do |header|
|
60
|
+
header.strip.split(/\s*:\s*/, 2)
|
61
|
+
end
|
62
|
+
|
63
|
+
Hash[headers.reject(&:empty?)]
|
64
|
+
end
|
65
|
+
|
66
|
+
def grab_headers(headers)
|
67
|
+
normalized_headers = Hash[headers.map { |key, value| [key.to_s.downcase, Array(value).join(',')] }]
|
68
|
+
[normalized_headers['link'], normalized_headers['content-location']]
|
69
|
+
end
|
70
|
+
|
71
|
+
def unknown_type(response)
|
72
|
+
UnknownResponseTypeError.new("Unknown response type #{response.class.name}")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
module Nitlink
|
4
|
+
class Splitter
|
5
|
+
def initialize(string)
|
6
|
+
@string = string
|
7
|
+
@scanner = StringScanner.new(string)
|
8
|
+
end
|
9
|
+
|
10
|
+
def split_on_unquoted(seperator)
|
11
|
+
# 0 = start of string
|
12
|
+
split_positions, ignored_split_positions = [0], []
|
13
|
+
in_quote = false
|
14
|
+
|
15
|
+
until @scanner.eos?
|
16
|
+
char = @scanner.getch
|
17
|
+
@scanner.getch if in_quote && char == "\\"
|
18
|
+
|
19
|
+
in_quote = !in_quote if char == '"'
|
20
|
+
@scanner.skip_until(/>/) and next if char == '<' && in_url?
|
21
|
+
|
22
|
+
if char == seperator
|
23
|
+
ignored_split_positions = []
|
24
|
+
(in_quote ? ignored_split_positions : split_positions) << @scanner.pos
|
25
|
+
end
|
26
|
+
end
|
27
|
+
split_positions << @string.length # end of string
|
28
|
+
split_positions += ignored_split_positions if in_quote # dangling quote
|
29
|
+
|
30
|
+
split_positions.sort.each_cons(2).inject([]) do |split_parts, (start_pos, end_pos)|
|
31
|
+
split_parts << @string[start_pos...end_pos].chomp(seperator)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def in_url?
|
36
|
+
preceeding = @scanner.string[0...(@scanner.pos - 1)].strip
|
37
|
+
preceeding.end_with?(',') || preceeding.empty?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
metadata
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nitlink
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alex Peattie
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-11-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: hashwithindifferentaccess
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.1.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.1.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.13'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.13'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 3.5.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 3.5.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: simplecov
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.12.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.12.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: simplecov-shield
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.1.0
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.1.0
|
97
|
+
description: |-
|
98
|
+
Nitlink is a nice, nitpicky gem for parsing Link headers, which aims to stick
|
99
|
+
as closely as possible to RFC 5988. Has support for UTF-8 encoded parameters, URI resolution, boolean parameters,
|
100
|
+
weird edge cases and more.
|
101
|
+
email:
|
102
|
+
- me@alexpeattie.com
|
103
|
+
executables: []
|
104
|
+
extensions: []
|
105
|
+
extra_rdoc_files: []
|
106
|
+
files:
|
107
|
+
- lib/nitlink.rb
|
108
|
+
- lib/nitlink/exceptions.rb
|
109
|
+
- lib/nitlink/link.rb
|
110
|
+
- lib/nitlink/link_collection.rb
|
111
|
+
- lib/nitlink/param_decoder.rb
|
112
|
+
- lib/nitlink/param_extractor.rb
|
113
|
+
- lib/nitlink/parser.rb
|
114
|
+
- lib/nitlink/response.rb
|
115
|
+
- lib/nitlink/response_normalizer.rb
|
116
|
+
- lib/nitlink/splitter.rb
|
117
|
+
- lib/nitlink/version.rb
|
118
|
+
homepage: https://github.com/alexpeattie/nitlink
|
119
|
+
licenses:
|
120
|
+
- MIT
|
121
|
+
metadata: {}
|
122
|
+
post_install_message:
|
123
|
+
rdoc_options: []
|
124
|
+
require_paths:
|
125
|
+
- lib
|
126
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: 1.9.3
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
requirements: []
|
137
|
+
rubyforge_project:
|
138
|
+
rubygems_version: 2.5.1
|
139
|
+
signing_key:
|
140
|
+
specification_version: 4
|
141
|
+
summary: Nitlink is a nitpicky gem for parsing Link headers (per RFC 5988)
|
142
|
+
test_files: []
|