nitlink 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|