All Files
(100.0%
covered at
119.35
hits/line)
11 files in total.
238 relevant lines.
238 lines covered and
0 lines missed
-
1
require_relative './nitlink/parser'
-
-
1
module Nitlink
-
end
-
1
module Nitlink
-
1
class MalformedLinkHeaderError < StandardError; end
-
1
class EncodedParamSyntaxError < StandardError; end
-
1
class UnsupportedCharsetError < StandardError; end
-
1
class UnknownResponseTypeError < StandardError; end
-
end
-
1
module Nitlink
-
1
class HashWithIndifferentAccess < ::Hash
-
1
def initialize(hash = {})
-
75
super()
-
75
hash.each do |key, value|
-
198
self[convert_key(key)] = value
-
end
-
end
-
-
1
def [](key)
-
247
super(convert_key(key))
-
end
-
-
1
def []=(key, value)
-
212
super(convert_key(key), value)
-
end
-
-
1
def delete(key)
-
1
super(convert_key(key))
-
end
-
-
1
def fetch(key, *args)
-
10
super(convert_key(key), *args)
-
end
-
-
1
def key?(key)
-
9
super(convert_key(key))
-
end
-
-
1
def values_at(*indices)
-
3
indices.map { |key| self[convert_key(key)] }
-
end
-
-
1
def merge(other)
-
3
dup.merge!(other)
-
end
-
-
1
def merge!(other)
-
3
other.each do |key, value|
-
8
self[convert_key(key)] = value
-
end
-
3
self
-
end
-
-
1
def reverse_merge(other)
-
2
self.class.new(other).merge(self)
-
end
-
-
1
def reverse_merge!(other_hash)
-
1
replace(reverse_merge(other_hash))
-
end
-
-
1
def replace(other_hash)
-
1
super(other_hash)
-
end
-
-
# Convert to a Hash with String keys.
-
1
def to_hash
-
1
Hash.new(default).merge!(self)
-
end
-
-
1
protected
-
-
1
def convert_key(key)
-
687
key.is_a?(Symbol) ? key.to_s : key
-
end
-
end
-
end
-
1
module Nitlink
-
1
Link = Struct.new(:target, :relation_type, :context, :target_attributes) do
-
end
-
end
-
1
require_relative './hash_with_indifferent_access'
-
-
1
module Nitlink
-
1
class LinkCollection < Array
-
1
def by_rel(relation_type)
-
45
raise ArgumentError.new('relation_type cannot be blank') if (!relation_type || relation_type.empty?)
-
92
find { |link| link.relation_type == relation_type.downcase.to_s }
-
end
-
-
1
def to_h(options = { with_indifferent_access: true })
-
5
options = Nitlink::HashWithIndifferentAccess.new(options)
-
5
indifferent = options.key?(:with_indifferent_access) ? options[:with_indifferent_access] : true
-
-
5
hash = indifferent ? Nitlink::HashWithIndifferentAccess.new : {}
-
9
each { |link| hash[link.relation_type.to_s] ||= link }
-
5
hash
-
end
-
-
1
alias_method :to_hash, :to_h
-
end
-
end
-
1
require 'cgi'
-
-
1
module Nitlink
-
1
class ParamDecoder
-
1
def decode(param_value)
-
9
charset, language, value_chars = param_value.split("'")
-
-
9
raise syntax_error(param_value) unless charset && language && value_chars
-
8
raise wrong_charset(charset) unless charset.downcase == 'utf-8'
-
-
6
CGI.unescape(value_chars)
-
end
-
-
1
private
-
-
1
def syntax_error(val)
-
1
EncodedParamSyntaxError.new(%Q{Syntax error decoding encoded parameter value "#{ val }", must be in the form: charset "'" [ language ] "'" value-chars})
-
end
-
-
1
def wrong_charset(charset)
-
2
UnsupportedCharsetError.new("Invalid charset #{charset}, encoded parameter values must use the UTF-8 character encoding")
-
end
-
end
-
end
-
1
module Nitlink
-
1
class ParamExtractor
-
1
QUOTED_VALUE = /\A"(.*)"\Z/m
-
1
QUOTED_PAIR = /\\./m
-
-
1
LEADING_OWS = /\A[\x09\x20]+/
-
1
TRAILING_OWS = /[\x09\x20]+\Z/
-
-
1
def extract(rest)
-
77
@rest = rest
-
77
parameter_strings = Splitter.new(rest).split_on_unquoted(';')
-
77
raw_params = parameter_strings.map do |parameter_str|
-
172
strip_ows(parameter_str).split('=', 2)
-
end
-
-
77
return format(raw_params)
-
end
-
-
1
private
-
-
1
def format(raw_params)
-
raw_params.map do |raw_param_name, raw_param_value|
-
172
next if !raw_param_name
-
108
param_name = rstrip_ows(raw_param_name.downcase)
-
-
108
if raw_param_value
-
104
param_value = lstrip_ows(raw_param_value)
-
104
param_value = format_quoted_value(param_value) if quoted?(param_value)
-
else
-
4
param_value = nil
-
end
-
-
108
[param_name, param_value]
-
77
end.compact
-
end
-
-
1
def format_quoted_value(quoted_value)
-
51
without_quotes = quoted_value.strip[QUOTED_VALUE, 1]
-
55
without_quotes.gsub(QUOTED_PAIR) { |match| match.chars.to_a.last }
-
end
-
-
1
def quoted?(param_value)
-
104
param_value =~ QUOTED_VALUE
-
end
-
-
1
def lstrip_ows(str)
-
276
str.gsub(LEADING_OWS, '')
-
end
-
-
1
def rstrip_ows(str)
-
280
str.gsub(TRAILING_OWS, '')
-
end
-
-
1
def strip_ows(str)
-
172
rstrip_ows(lstrip_ows str)
-
end
-
end
-
end
-
1
require_relative './exceptions'
-
1
require_relative './splitter'
-
1
require_relative './link_collection'
-
1
require_relative './link'
-
1
require_relative './response_normalizer'
-
1
require_relative './param_extractor'
-
1
require_relative './param_decoder'
-
-
1
module Nitlink
-
1
class Parser
-
1
SINGLE_LINK = /\A\s*<([^>]*)>(.*)/
-
1
RWS = /[\x09\x20]+/
-
-
1
attr_reader :options
-
-
1
def parse(response, http_method = 'GET')
-
60
@http_method = http_method
-
60
@request_uri, @status, link_header, @content_location_header = ResponseNormalizer.new.metadata(response)
-
-
60
links = LinkCollection.new
-
60
return links unless link_header
-
-
59
unfolded_header = link_header.gsub(/\r?\n[\x20\x09]+/, '')
-
59
link_strings = Splitter.new(unfolded_header).split_on_unquoted(',')
-
-
59
parse_links(link_strings, links)
-
end
-
-
1
private
-
-
1
def parse_links(link_strings, link_collection)
-
59
link_strings.each do |link_string|
-
64
well_formed, target_string, rest = link_string.match(SINGLE_LINK).to_a
-
64
raise malformed(link_string) unless well_formed
-
-
62
link_parameters = ParamExtractor.new.extract(rest)
-
129
create_links(target_string, link_parameters).each { |link| link_collection.push(link) }
-
end
-
-
56
link_collection
-
end
-
-
1
def create_links(target_string, link_parameters)
-
62
target, relation_types, context, target_attributes = link_attributes(target_string, link_parameters)
-
61
relation_types.map do |relation_type|
-
67
Link.new(target, relation_type, context, target_attributes)
-
end
-
end
-
-
1
def link_attributes(target_string, link_parameters)
-
62
target = @request_uri.merge(target_string)
-
-
61
relations_string = first_match(link_parameters, 'rel') || ''
-
61
relation_types = relations_string.split(RWS)
-
-
61
context_string = first_match(link_parameters, 'anchor') || identity
-
61
context = (context_string && @request_uri.scheme) ? @request_uri.merge(context_string) : nil
-
-
61
target_attributes = extract_target_attributes(link_parameters)
-
61
[target, relation_types, context, target_attributes]
-
end
-
-
1
def first_match(link_parameters, param_name)
-
284
(link_parameters.find { |name, _value| name == param_name } || []).last
-
end
-
-
1
def extract_target_attributes(link_parameters)
-
61
target_attributes = []
-
61
link_parameters.each do |param_name, param_value|
-
88
next if %(rel anchor).include?(param_name)
-
17
next if %(media title title* type).include?(param_name) && first_match(target_attributes, param_name)
-
-
16
begin
-
16
param_value = decode(param_value) if param_name.end_with?('*')
-
rescue EncodedParamSyntaxError, UnsupportedCharsetError
-
1
next
-
end
-
-
15
target_attributes.push [param_name, param_value]
-
end
-
-
61
Hash[target_attributes]
-
end
-
-
1
def malformed(link_string)
-
2
MalformedLinkHeaderError.new("Malformed link header (#{ link_string })")
-
end
-
-
1
def decode(param_value)
-
4
ParamDecoder.new.decode(param_value)
-
end
-
-
1
def identity
-
54
if %w(GET HEAD).include?(@http_method.upcase) && [200, 203, 204, 206, 304].include?(@status)
-
51
@request_uri
-
else
-
3
@content_location_header
-
end
-
end
-
end
-
end
-
1
require_relative '../nitlink'
-
-
1
module Nitlink
-
1
module ResponseDecorator
-
1
def links
-
10
Nitlink::Parser.new.parse(self)
-
end
-
end
-
end
-
-
1
third_party_clients = ['Curl::Easy', 'Excon::Response', 'Faraday::Response', 'HTTP::Message', 'HTTP::Response', 'HTTParty::Response', 'Net::HTTPResponse', 'Patron::Response', 'RestClient::Response', 'Typhoeus::Response', 'Unirest::HttpResponse']
-
-
1
decoratable_responses = third_party_clients.select do |klass|
-
11
module_name, class_name = klass.split('::', 2)
-
11
Object.const_defined?(module_name) && Object.const_get(module_name).const_defined?(class_name)
-
end
-
-
1
decoratable_responses.each do |klass|
-
10
module_name, class_name = klass.split('::', 2)
-
20
Object.const_get(module_name).const_get(class_name).class_eval { include Nitlink::ResponseDecorator }
-
end
-
1
require 'uri'
-
1
require_relative './hash_with_indifferent_access'
-
-
1
module Nitlink
-
1
class ResponseNormalizer
-
1
def metadata(response)
-
86
response_class = response.class.name
-
-
86
uri, status, (link, content_location) = case response_class
-
when 'Curl::Easy'
-
2
[response.url.chomp('?'), response.response_code, grab_headers(headers_from_string response.header_str)]
-
when 'Excon::Response'
-
2
scheme = response.port == 443 ? 'https' : 'http'
-
# We have to reconstruct to URL annoyingly
-
2
uri = URI::HTTP.new(scheme, nil, response.host, nil, nil, response.path, nil, nil, nil)
-
-
2
[uri, response.status, grab_headers(response.headers)]
-
when 'Faraday::Response'
-
2
response = response.to_hash
-
2
[response[:url], response[:status], grab_headers(response[:response_headers])]
-
when 'HTTP::Message'
-
2
[response.header.request_uri, response.status, grab_headers(Hash[response.header.all])]
-
when 'HTTP::Response'
-
2
[response.uri, response.status, grab_headers(response.headers.to_h)]
-
when 'HTTParty::Response'
-
2
[response.request.uri, response.code, grab_headers(response.response.to_hash)]
-
when 'Patron::Response'
-
2
[response.url, response.status, grab_headers(response.headers)]
-
when 'RestClient::Response'
-
2
[response.request.url, response.code, grab_headers(response.net_http_res.to_hash)]
-
when 'Tempfile', 'StringIO'
-
# ↑ returned by OpenURI
-
1
[response.base_uri, response.status[0], grab_headers(response.meta)]
-
when 'Typhoeus::Response'
-
2
[response.request.base_url, response.code, grab_headers(response.headers)]
-
when 'Unirest::HttpResponse'
-
4
warn "Unirest support is deprecated and will be removed in Nitlink 2.0"
-
4
return metadata(response.raw_body)
-
when 'Hash'
-
56
response = Nitlink::HashWithIndifferentAccess.new(response)
-
56
response[:headers] = headers_from_string(response[:headers]) if String === response[:headers]
-
-
56
[response[:request_uri], response[:status], grab_headers(response[:headers])]
-
else
-
7
if defined?(Net::HTTPResponse) && Net::HTTPResponse === response
-
6
[response.uri, response.code, grab_headers(response.to_hash)]
-
else
-
1
raise unknown_type(response)
-
end
-
end
-
-
81
[URI.parse(uri.to_s), (status ? Integer(status) : status), link, content_location]
-
end
-
-
1
private
-
-
1
def headers_from_string(header_str)
-
3
headers = header_str.split("\n").map do |header|
-
30
header.strip.split(/\s*:\s*/, 2)
-
end
-
-
3
Hash[headers.reject(&:empty?)]
-
end
-
-
1
def grab_headers(headers)
-
398
normalized_headers = Hash[headers.map { |key, value| [key.to_s.downcase, Array(value).join(',')] }]
-
81
[normalized_headers['link'], normalized_headers['content-location']]
-
end
-
-
1
def unknown_type(response)
-
1
UnknownResponseTypeError.new("Unknown response type #{response.class.name}")
-
end
-
end
-
end
-
1
require 'strscan'
-
-
1
module Nitlink
-
1
class Splitter
-
1
def initialize(string)
-
148
@string = string
-
148
@scanner = StringScanner.new(string)
-
end
-
-
1
def split_on_unquoted(seperator)
-
# 0 = start of string
-
147
split_positions, ignored_split_positions = [0], []
-
147
in_quote = false
-
-
147
until @scanner.eos?
-
3870
char = @scanner.getch
-
3870
@scanner.getch if in_quote && char == "\\"
-
-
3870
in_quote = !in_quote if char == '"'
-
3870
@scanner.skip_until(/>/) and next if char == '<' && in_url?
-
-
3795
if char == seperator
-
121
ignored_split_positions = []
-
121
(in_quote ? ignored_split_positions : split_positions) << @scanner.pos
-
end
-
end
-
147
split_positions << @string.length # end of string
-
147
split_positions += ignored_split_positions if in_quote # dangling quote
-
-
147
split_positions.sort.each_cons(2).inject([]) do |split_parts, (start_pos, end_pos)|
-
256
split_parts << @string[start_pos...end_pos].chomp(seperator)
-
end
-
end
-
-
1
def in_url?
-
80
preceeding = @scanner.string[0...(@scanner.pos - 1)].strip
-
80
preceeding.end_with?(',') || preceeding.empty?
-
end
-
end
-
end