From f127a1f30f3ead1cebe2579685d12baecb6175ac Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 17 Feb 2016 23:16:08 +0100 Subject: [PATCH] Implement RFC 7033, bump version to 1.0.0, add some documentation --- Gemfile.lock | 2 +- README.md | 11 ++- goldfinger.gemspec | 2 +- lib/goldfinger.rb | 7 ++ lib/goldfinger/client.rb | 2 +- lib/goldfinger/link.rb | 53 +++++++++++ lib/goldfinger/result.rb | 77 +++++++++++++-- .../quitter.no_.well-known_webfinger.json | 78 +++++++++++++++- .../quitter.no_.well-known_webfinger.xml | 36 ++++--- spec/goldfinger/result_spec.rb | 93 +++++++++++++------ 10 files changed, 304 insertions(+), 57 deletions(-) create mode 100644 lib/goldfinger/link.rb diff --git a/Gemfile.lock b/Gemfile.lock index 51b4950..426a862 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - goldfinger (0.1.0) + goldfinger (0.1.1) addressable (~> 2.4) http (~> 1.0) nokogiri (~> 1.6) diff --git a/README.md b/README.md index 5de3134..e2d644f 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,16 @@ A Webfinger client for Ruby. Supports `application/xrd+xml` and `application/jrd ## Usage data = Goldfinger.finger('acct:gargron@quitter.no') - data.link('http://schemas.google.com/g/2010#updates-from')[:href] + + data.link('http://schemas.google.com/g/2010#updates-from').href # => "https://quitter.no/api/statuses/user_timeline/7477.atom" + data.aliases + # => ["https://quitter.no/user/7477", "https://quitter.no/gargron"] + + data.subject + # => "acct:gargron@quitter.no" + ## RFC support -The gem only parses link data. It does not currently parse aliases, properties, or more complex structures. +The official Webfinger RFC is [7033](https://tools.ietf.org/html/rfc7033). diff --git a/goldfinger.gemspec b/goldfinger.gemspec index 73031e5..b9fe797 100644 --- a/goldfinger.gemspec +++ b/goldfinger.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'goldfinger' - s.version = '0.1.1' + s.version = '1.0.0' s.platform = Gem::Platform::RUBY s.required_ruby_version = '>= 2.0.0' s.date = '2016-02-17' diff --git a/lib/goldfinger.rb b/lib/goldfinger.rb index 0b7a77c..a811942 100644 --- a/lib/goldfinger.rb +++ b/lib/goldfinger.rb @@ -1,4 +1,5 @@ require 'goldfinger/request' +require 'goldfinger/link' require 'goldfinger/result' require 'goldfinger/utils' require 'goldfinger/client' @@ -13,6 +14,12 @@ module Goldfinger class SSLError < Error end + # Returns result for the Webfinger query + # + # @raise [Goldfinger::NotFoundError] Error raised when the Webfinger resource could not be retrieved + # @raise [Goldfinger::SSLError] Error raised when there was a SSL error when fetching the resource + # @param uri [String] A full resource identifier in the format acct:user@example.com + # @return [Goldfinger::Result] def self.finger(uri) Goldfinger::Client.new(uri).finger end diff --git a/lib/goldfinger/client.rb b/lib/goldfinger/client.rb index 15eb521..b434f63 100644 --- a/lib/goldfinger/client.rb +++ b/lib/goldfinger/client.rb @@ -39,7 +39,7 @@ module Goldfinger def url_from_template(template) xml = Nokogiri::XML(template) - links = xml.xpath('//xmlns:Link[@rel="lrdd"]', xmlns: 'http://docs.oasis-open.org/ns/xri/xrd-1.0') + links = xml.xpath('//xmlns:Link[@rel="lrdd"]') raise Goldfinger::NotFoundError if links.empty? diff --git a/lib/goldfinger/link.rb b/lib/goldfinger/link.rb new file mode 100644 index 0000000..1b4c34c --- /dev/null +++ b/lib/goldfinger/link.rb @@ -0,0 +1,53 @@ +module Goldfinger + # @!attribute [r] href + # @return [String] The href the link points to + # @!attribute [r] type + # @return [String] The mime type of the link + # @!attribute [r] rel + # @return [String] The relation descriptor of the link + class Link + attr_reader :href, :type, :rel + + def initialize(a) + @href = a[:href] + @type = a[:type] + @rel = a[:rel] + @titles = a[:titles] + @properties = a[:properties] + end + + # The "titles" object comprises zero or more name/value pairs whose + # names are a language tag or the string "und". The string is + # human-readable and describes the link relation. + # @see #title + # @return [Array] Array form of the hash + def titles + @titles.to_a + end + + # The "properties" object within the link relation object comprises + # zero or more name/value pairs whose names are URIs (referred to as + # "property identifiers") and whose values are strings or nil. + # Properties are used to convey additional information about the link + # relation. + # @see #property + # @return [Array] Array form of the hash + def properties + @properties.to_a + end + + # Returns a title for a language + # @param lang [String] + # @return [String] + def title(lang) + @titles[lang] + end + + # Returns a property for a key + # @param key [String] + # @return [String] + def property(key) + @properties[key] + end + end +end diff --git a/lib/goldfinger/result.rb b/lib/goldfinger/result.rb index 1c03811..1a98afc 100644 --- a/lib/goldfinger/result.rb +++ b/lib/goldfinger/result.rb @@ -1,17 +1,57 @@ module Goldfinger class Result def initialize(headers, body) - @mime_type = headers.get(HTTP::Headers::CONTENT_TYPE).first - @body = body - @links = {} + @mime_type = headers.get(HTTP::Headers::CONTENT_TYPE).first + @body = body + @subject = nil + @aliases = [] + @links = {} + @properties = {} parse end + # The value of the "subject" member is a URI that identifies the entity + # that the JRD describes. + # @return [String] + def subject + @subject + end + + # The "aliases" array is an array of zero or more URI strings that + # identify the same entity as the "subject" URI. + # @return [Array] + def aliases + @aliases + end + + # The "properties" object comprises zero or more name/value pairs whose + # names are URIs (referred to as "property identifiers") and whose + # values are strings or nil. + # @see #property + # @return [Array] Array form of the hash + def properties + @properties.to_a + end + + # Returns a property for a key + # @param key [String] + # @return [String] + def property(key) + @properties[key] + end + + # The "links" array has any number of member objects, each of which + # represents a link. + # @see #link + # @return [Array] Array form of the hash def links @links.to_a end + # Returns a key for a relation + # @param key [String] + # @return [Goldfinger::Link] def link(rel) @links[rel] end @@ -29,13 +69,38 @@ module Goldfinger def parse_json json = JSON.parse(@body) - json['links'].each { |link| @links[link['rel']] = Hash[link.keys.map { |key| [key.to_sym, link[key]] }] } + + @subject = json['subject'] + @aliases = json['aliases'] || [] + @properties = json['properties'] || {} + + json['links'].each do |link| + tmp = Hash[link.keys.map { |key| [key.to_sym, link[key]] }] + @links[link['rel']] = Goldfinger::Link.new(tmp) + end end def parse_xml xml = Nokogiri::XML(@body) - links = xml.xpath('//xmlns:Link', xmlns: 'http://docs.oasis-open.org/ns/xri/xrd-1.0') - links.each { |link| @links[link.attribute('rel').value] = Hash[link.attributes.keys.map { |key| [key.to_sym, link.attribute(key).value] }] } + + @subject = xml.at_xpath('//xmlns:Subject').content + @aliases = xml.xpath('//xmlns:Alias').map { |a| a.content } + + properties = xml.xpath('/xmlns:XRD/xmlns:Property') + properties.each { |prop| @properties[prop.attribute('type').value] = prop.attribute('nil') ? nil : prop.content } + + xml.xpath('//xmlns:Link').each do |link| + rel = link.attribute('rel').value + tmp = Hash[link.attributes.keys.map { |key| [key.to_sym, link.attribute(key).value] }] + + tmp[:titles] = {} + tmp[:properties] = {} + + link.xpath('.//xmlns:Title').each { |title| tmp[:titles][title.attribute('lang').value] = title.content } + link.xpath('.//xmlns:Property').each { |prop| tmp[:properties][prop.attribute('type').value] = prop.attribute('nil') ? nil : prop.content } + + @links[rel] = Goldfinger::Link.new(tmp) + end end end end diff --git a/spec/fixtures/quitter.no_.well-known_webfinger.json b/spec/fixtures/quitter.no_.well-known_webfinger.json index c2aa46c..ce934d9 100644 --- a/spec/fixtures/quitter.no_.well-known_webfinger.json +++ b/spec/fixtures/quitter.no_.well-known_webfinger.json @@ -1 +1,77 @@ -{"subject":"acct:gargron@quitter.no","aliases":["https:\/\/quitter.no\/user\/7477","https:\/\/quitter.no\/gargron"],"links":[{"rel":"http:\/\/webfinger.net\/rel\/profile-page","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/gmpg.org\/xfn\/11","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"describedby","type":"application\/rdf+xml","href":"https:\/\/quitter.no\/gargron\/foaf"},{"rel":"http:\/\/apinamespace.org\/atom","type":"application\/atomsvc+xml","href":"https:\/\/quitter.no\/api\/statusnet\/app\/service\/gargron.xml"},{"rel":"http:\/\/apinamespace.org\/twitter","href":"https:\/\/quitter.no\/api\/"},{"rel":"http:\/\/specs.openid.net\/auth\/2.0\/provider","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/schemas.google.com\/g\/2010#updates-from","type":"application\/atom+xml","href":"https:\/\/quitter.no\/api\/statuses\/user_timeline\/7477.atom"},{"rel":"magic-public-key","href":"data:application\/magic-public-key,RSA.1ZBkHTavLvxH3FzlKv4O6WtlILKRFfNami3_Rcu8EuogtXSYiS-bB6hElZfUCSHbC4uLemOA34PEhz__CDMozax1iI_t8dzjDnh1x0iFSup7pSfW9iXk_WU3Dm74yWWW2jildY41vWgrEstuQ1dJ8vVFfSJ9T_tO4c-T9y8vDI8=.AQAB"},{"rel":"salmon","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-replies","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-mention","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/ostatus.org\/schema\/1.0\/subscribe","template":"https:\/\/quitter.no\/main\/ostatussub?profile={uri}"}]} +{ + "subject": "acct:gargron@quitter.no", + "aliases": [ + "https://quitter.no/user/7477", + "https://quitter.no/gargron" + ], + "properties": { + "http://webfinger.example/ns/name": "Bob Smith" + }, + "links": [ + { + "rel": "http://webfinger.net/rel/profile-page", + "type": "text/html", + "href": "https://quitter.no/gargron" + }, + { + "rel": "http://gmpg.org/xfn/11", + "type": "text/html", + "href": "https://quitter.no/gargron" + }, + { + "rel": "describedby", + "type": "application/rdf+xml", + "href": "https://quitter.no/gargron/foaf" + }, + { + "rel": "http://apinamespace.org/atom", + "type": "application/atomsvc+xml", + "href": "https://quitter.no/api/statusnet/app/service/gargron.xml" + }, + { + "rel": "http://apinamespace.org/twitter", + "href": "https://quitter.no/api/" + }, + { + "rel": "http://specs.openid.net/auth/2.0/provider", + "href": "https://quitter.no/gargron" + }, + { + "rel": "http://schemas.google.com/g/2010#updates-from", + "type": "application/atom+xml", + "href": "https://quitter.no/api/statuses/user_timeline/7477.atom" + }, + { + "rel": "magic-public-key", + "href": "data:application/magic-public-key,RSA.1ZBkHTavLvxH3FzlKv4O6WtlILKRFfNami3_Rcu8EuogtXSYiS-bB6hElZfUCSHbC4uLemOA34PEhz__CDMozax1iI_t8dzjDnh1x0iFSup7pSfW9iXk_WU3Dm74yWWW2jildY41vWgrEstuQ1dJ8vVFfSJ9T_tO4c-T9y8vDI8=.AQAB" + }, + { + "rel": "salmon", + "href": "https://quitter.no/main/salmon/user/7477" + }, + { + "rel": "http://salmon-protocol.org/ns/salmon-replies", + "href": "https://quitter.no/main/salmon/user/7477" + }, + { + "rel": "http://salmon-protocol.org/ns/salmon-mention", + "href": "https://quitter.no/main/salmon/user/7477" + }, + { + "rel": "http://ostatus.org/schema/1.0/subscribe", + "template": "https://quitter.no/main/ostatussub?profile={uri}" + }, + { + "rel": "http://spec.example.net/photo/1.0", + "type": "image/jpeg", + "href": "http://photos.example.com/gpburdell.jpg", + "titles": { + "en": "User Photo", + "de": "Benutzerfoto" + }, + "properties": { + "http://spec.example.net/created/1.0": "1970-01-01" + } + } + ] +} diff --git a/spec/fixtures/quitter.no_.well-known_webfinger.xml b/spec/fixtures/quitter.no_.well-known_webfinger.xml index 1e6c043..9d7681f 100644 --- a/spec/fixtures/quitter.no_.well-known_webfinger.xml +++ b/spec/fixtures/quitter.no_.well-known_webfinger.xml @@ -1,18 +1,24 @@ - acct:gargron@quitter.no - https://quitter.no/user/7477 - https://quitter.no/gargron - - - - - - - - - - - - + acct:gargron@quitter.no + https://quitter.no/user/7477 + https://quitter.no/gargron + Bob Smith + + + + + + + + + + + + + + User Photo + Benutzerfoto + 1970-01-01 + diff --git a/spec/goldfinger/result_spec.rb b/spec/goldfinger/result_spec.rb index 6636b7a..c7cf34c 100644 --- a/spec/goldfinger/result_spec.rb +++ b/spec/goldfinger/result_spec.rb @@ -1,41 +1,74 @@ describe Goldfinger::Result do - context 'application/xrd+xml' do + shared_examples 'a working finger result' do + subject { Goldfinger::Result.new(headers, body) } + + describe '#links' do + it 'returns a non-empty array' do + expect(subject.links).to be_instance_of Array + expect(subject.links).to_not be_empty + end + end + + describe '#link' do + it 'returns a value for a given rel' do + expect(subject.link('http://webfinger.net/rel/profile-page').href).to eql 'https://quitter.no/gargron' + end + + it 'returns nil if no such link exists' do + expect(subject.link('zzzz')).to be_nil + end + + it 'returns titles map' do + expect(subject.link('http://spec.example.net/photo/1.0').title('en')).to eql 'User Photo' + end + + it 'returns a properties map' do + expect(subject.link('http://spec.example.net/photo/1.0').property('http://spec.example.net/created/1.0')).to eql '1970-01-01' + end + end + + describe '#subject' do + it 'returns the subject' do + expect(subject.subject).to eql 'acct:gargron@quitter.no' + end + end + + describe '#aliases' do + it 'returns a non-empty array' do + expect(subject.aliases).to be_instance_of Array + expect(subject.aliases).to_not be_empty + end + end + + describe '#properties' do + it 'returns an array' do + expect(subject.properties).to be_instance_of Array + expect(subject.properties).to_not be_empty + end + end + + describe '#property' do + it 'returns the value for a key' do + expect(subject.property('http://webfinger.example/ns/name')).to eql 'Bob Smith' + end + + it 'returns nil if no such property exists' do + expect(subject.property('zzzz')).to be_nil + end + end + end + + context 'when the input mime type is application/xrd+xml' do let(:headers) { h = HTTP::Headers.new; h.set(HTTP::Headers::CONTENT_TYPE, 'application/xrd+xml'); h } let(:body) { File.read(fixture_path('quitter.no_.well-known_webfinger.xml')) } - subject { Goldfinger::Result.new(headers, body) } - - describe '#links' do - it 'returns a non-empty array' do - expect(subject.links).to be_instance_of Array - expect(subject.links).to_not be_empty - end - end - - describe '#link' do - it 'returns a value for a given rel' do - expect(subject.link('http://webfinger.net/rel/profile-page')[:href]).to eql 'https://quitter.no/gargron' - end - end + it_behaves_like 'a working finger result' end - context 'application/jrd+json' do + context 'when the input mime type is application/jrd+json' do let(:headers) { h = HTTP::Headers.new; h.set(HTTP::Headers::CONTENT_TYPE, 'application/jrd+json'); h } let(:body) { File.read(fixture_path('quitter.no_.well-known_webfinger.json')) } - subject { Goldfinger::Result.new(headers, body) } - - describe '#links' do - it 'returns a non-empty array' do - expect(subject.links).to be_instance_of Array - expect(subject.links).to_not be_empty - end - end - - describe '#link' do - it 'returns a value for a given rel' do - expect(subject.link('http://webfinger.net/rel/profile-page')[:href]).to eql 'https://quitter.no/gargron' - end - end + it_behaves_like 'a working finger result' end end