discodactyl 0.3.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.
@@ -0,0 +1,102 @@
1
+ require 'open-uri'
2
+ require 'nokogiri'
3
+ require 'discodactyl/link_header'
4
+
5
+ module Discodactyl # :nodoc:
6
+ class ResourceDiscovery
7
+ class << self
8
+
9
+ # perform LRDD on the URI, returning all linked URIs which match
10
+ # the provided rel. Any URITemplates will be expanded with the
11
+ # params if they are provided
12
+ #--
13
+ # TODO: xri support: no host
14
+ # TODO: check if XRD/Property[@type=http://lrdd.net/priority/resource] to indicate resource-priority
15
+ # TODO: handle 3** status redirects
16
+ # TODO: maintain a security bit
17
+ # TODO: URIs for all discovery modes should be appended, not just returned
18
+ # TODO: rewrite this so it's just get links, which yields objects providing at least the xrd link interface (rel, href, type)
19
+ #++
20
+ def get_uris_by_rel(uri, rel, params = {})
21
+ begin
22
+ uri = URI.parse(uri.to_s) unless uri.respond_to?('open')
23
+ resource = uri.open
24
+ rescue OpenURI::HTTPError => e
25
+ status = e.io.status[0] # => 3xx, 4xx, or 5xx
26
+
27
+ # code = Net::HTTPResponse::CODE_TO_OBJ[status]
28
+ # if code == "303"
29
+ # 303 HTTPSeeOther
30
+ # if res.key? 'Link'
31
+ # links = res['Link']
32
+ # end
33
+ # elsif code == "401" # HTTPUnauthorized
34
+ # authenticate
35
+ # return get_uris_by_rel(uri)
36
+ # elsif code == "301" || status == "302"
37
+ # 300 HTTPMultipleChoice
38
+ # 301 HTTPMovedPermanently
39
+ # 302 HTTPFound
40
+ # 304 HTTPNotModified
41
+ # 305 HTTPUseProxy
42
+ # 307 HTTPTemporaryRedirect
43
+ # resource = URI.parse(resource['location']).open
44
+ # else
45
+ rescue NoMethodError
46
+ end
47
+ if resource
48
+ # check for link headers first
49
+ if resource.meta.key? 'Link'
50
+ uris = get_uris_by_rel_from_link_header(resource, rel)
51
+ end
52
+
53
+ unless uris
54
+ # then check for links in the document
55
+ if content_sniff(resource) == 'text/html'
56
+ uris = get_uris_by_rel_from_html(resource, rel)
57
+ # Atom
58
+ elsif content_sniff(resource) == 'application/atom+xml'
59
+ uris = get_uris_by_rel_from_atom(resource, rel)
60
+ end
61
+ end
62
+ end
63
+ unless uris
64
+ host_meta = Discodactyl::HostMeta.from_uri uri
65
+ uris = host_meta.uris_by_rel(rel, params)
66
+ end
67
+ uris
68
+ end
69
+
70
+ def lrdd_discovery(uri)
71
+ get_uris_by_rel(uri, 'describedby')
72
+ end
73
+
74
+ def content_sniff(resource)
75
+ if resource.content_type == 'text/html'
76
+ type = 'text/html'
77
+ elsif resource.content_type == 'application/atom+xml'
78
+ type = 'application/atom+xml'
79
+ end
80
+ type
81
+ end
82
+
83
+ # take an HTTP response with a content-type of HTML,
84
+ # find all links by rel, and return each href
85
+ def get_uris_by_rel_from_html(resource, rel)
86
+ doc = Nokogiri::HTML(resource)
87
+ links = doc.xpath("//*[contains(@rel, \"#{rel}\")]")
88
+ uris = links.map {|link| link['href'] }
89
+ end
90
+
91
+ # take an HTTP response, find a link header
92
+ # with rel, and return its href
93
+ def get_uris_by_rel_from_link_header(response, rel)
94
+ links = [response.meta['Link']].flatten.collect {|link|
95
+ LinkHeader.parse(link);
96
+ }
97
+ link = links.find {|l| l[:rel].include? rel }
98
+ xrd = link[:href]
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,32 @@
1
+ module Discodactyl # :nodoc:
2
+ # Basic URI templates as used in XRD. Not to be confused with
3
+ # http://tools.ietf.org/html/draft-gregorio-uritemplate
4
+ class URITemplate
5
+ attr_accessor :pattern
6
+ def initialize(pattern)
7
+ @pattern = pattern
8
+ end
9
+ def to_uri(params)
10
+ require 'cgi'
11
+ uri = @pattern
12
+ while /\{%([^}]*)\}/ =~ uri
13
+ uri.gsub!($~[0], params[$~[1]])
14
+ end
15
+ while /\{([^}]*)\}/ =~ uri
16
+ uri.gsub!($~[0], CGI::escape(params[$~[1]].to_s))
17
+ end
18
+ uri
19
+ end
20
+
21
+ def ==(other)
22
+ return false unless other.class == self.class
23
+ return false unless other.instance_variables == self.instance_variables
24
+ self.instance_variables.each do |var|
25
+ self_var = self.instance_variable_get(var)
26
+ other_var = other.instance_variable_get(var)
27
+ return false unless self_var.eql?(other_var)
28
+ end
29
+ true
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,2 @@
1
+ require 'discodactyl/xrd/document'
2
+ require 'discodactyl/xrd/link'
@@ -0,0 +1,75 @@
1
+ require 'nokogiri'
2
+ require 'discodactyl/xrd/link'
3
+
4
+ module Discodactyl # :nodoc:
5
+ module XRD # :nodoc:
6
+ XMLNS = {'xrd' => 'http://docs.oasis-open.org/ns/xri/xrd-1.0'}
7
+ class Document
8
+ class << self
9
+ def parse(string)
10
+ raw = Nokogiri::XML(string)
11
+ doc = self.new
12
+
13
+ doc.raw = raw
14
+
15
+ doc
16
+ end
17
+ end
18
+
19
+ attr_accessor :raw
20
+
21
+ def escapeXPath(str)
22
+ inner = str.split('\'').join('\',"\'",\'')
23
+ outer = 'concat(\'\',\'%s\')' % inner
24
+ end
25
+
26
+ def linkelems_by_rel(rel)
27
+ path = '/xrd:XRD/xrd:Link[@rel=%s]'% escapeXPath(rel)
28
+ @raw.xpath path, XMLNS
29
+ end
30
+
31
+ def links_by_rel(rel)
32
+ linkelems_by_rel(rel).map {|e| Link.parse(e) }
33
+ end
34
+
35
+ def uris_by_rel(rel, params = {})
36
+ links_by_rel(rel).map {|l| l.to_uri(params) }
37
+ end
38
+
39
+ # take an XML fragment for a link and append it to the document
40
+ def append(link)
41
+ initial_ids = ids
42
+ raw.root.add_child(link)
43
+ elem = Link.parse(raw.root.last_element_child)
44
+ elem.id = generate_tag_uri if elem.id.nil? || initial_ids.include?(elem.id)
45
+ elem
46
+ end
47
+
48
+ def links
49
+ raw.xpath('/xrd:XRD/xrd:Link', XMLNS).collect {|elem|
50
+ Link.parse(elem)
51
+ }
52
+ end
53
+
54
+ def find_link_by_id(link_id)
55
+ links.find {|link| link.id == link_id}
56
+ end
57
+
58
+ def ids
59
+ links.map(&:id).reject(&:nil?)
60
+ end
61
+
62
+ def to_s
63
+ raw.to_s
64
+ end
65
+
66
+ def generate_tag_uri
67
+ scheme = 'tag'
68
+ authority = 'dactylo.us'
69
+ date = Date.today.to_s
70
+ specific = "/xrd/link/#{rand(2**10)}"
71
+ "#{scheme}:#{authority},#{date}:#{specific}"
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,47 @@
1
+ require 'nokogiri'
2
+ require 'active_support/core_ext/object/misc'
3
+ require 'discodactyl/uri_template'
4
+
5
+ module Discodactyl # :nodoc:
6
+ module XRD # :nodoc:
7
+ class Link
8
+ class << self
9
+ def parse(element)
10
+ returning(link = self.new) do
11
+ link.rel = element['rel']
12
+ link.type = element['type']
13
+ link.href = element['href']
14
+ link.template = URITemplate.new(element['template']) unless link.href
15
+ link.raw = element
16
+ end
17
+ end
18
+ end
19
+
20
+ attr_accessor :href, :template, :rel, :type, :raw
21
+
22
+ def to_uri(params = {})
23
+ @href || @template.to_uri(params)
24
+ end
25
+
26
+ def id
27
+ begin
28
+ @raw.attribute_with_ns('id', 'http://www.w3.org/XML/1998/namespace').value
29
+ rescue
30
+ end
31
+ end
32
+
33
+ def id=(value)
34
+ @raw['xml:id'] = value
35
+ end
36
+
37
+ def to_s
38
+ @raw.to_s
39
+ end
40
+
41
+ def ==(other)
42
+ (other.respond_to?('raw') && (@raw == other.raw)) ||
43
+ super
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env ruby -w
2
+ libdir = File.expand_path('../../lib', __FILE__)
3
+ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
4
+
5
+ require "test/unit"
6
+ require "discodactyl/acct_uri"
7
+
8
+ class TestURIAcct < Test::Unit::TestCase
9
+ def test_parse_without_scheme
10
+ a = URI::ACCT.parse('user@host.example')
11
+ assert_equal('user', a.local_part)
12
+ assert_equal('host.example', a.host)
13
+ end
14
+
15
+ def test_parse_with_scheme
16
+ a = URI::ACCT.parse('acct:user@host.example')
17
+ assert_equal('user', a.local_part)
18
+ assert_equal('host.example', a.host)
19
+ end
20
+
21
+ def test_parse_uri_by_scheme
22
+ a = URI.parse('acct:user@host.example')
23
+ assert_equal('user', a.local_part)
24
+ assert_equal('host.example', a.host)
25
+ end
26
+
27
+ def test_build_from_hash
28
+ a = URI::ACCT.build(:local_part => 'user',
29
+ :host => 'host.example')
30
+ assert_equal('user', a.local_part)
31
+ assert_equal('host.example', a.host)
32
+ end
33
+
34
+ def test_build_from_opaque
35
+ a = URI::ACCT.build(:opaque => 'user@host.example')
36
+ assert_equal('user', a.local_part)
37
+ assert_equal('host.example', a.host)
38
+ end
39
+
40
+ def test_build_from_array
41
+ a = URI::ACCT.build(['user','host.example'])
42
+ assert_equal('user', a.local_part)
43
+ assert_equal('host.example', a.host)
44
+ end
45
+
46
+
47
+ def test_to_s
48
+ a = URI::ACCT.build(:local_part => 'user',
49
+ :host => 'host.example')
50
+ assert_equal('acct:user@host.example', a.to_s)
51
+ end
52
+ def test_id
53
+ a = URI::ACCT.build(:local_part => 'user',
54
+ :host => 'host.example')
55
+ assert_equal('user@host.example', a.id)
56
+ end
57
+ end
@@ -0,0 +1,17 @@
1
+ require "test/unit"
2
+ module Test::Unit::Assertions
3
+ def assert_length(expected, enum, message = nil)
4
+ message = build_message message, '<?> is not length <?>', enum, expected
5
+ assert_equal expected, enum.length, message
6
+ end
7
+
8
+ def assert_include?(atom, enum, message = nil)
9
+ message = build_message message, '<?> does not include <?>.', enum, atom
10
+ assert enum.include?(atom), message
11
+ end
12
+ end
13
+
14
+ class Test::Unit::TestCase
15
+ require 'rr'
16
+ include RR::Adapters::TestUnit
17
+ end
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby -w
2
+ libdir = File.expand_path('../../lib', __FILE__)
3
+ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
4
+
5
+ require "test/unit"
6
+ require "discodactyl/host_meta"
7
+ require "discodactyl/acct_uri"
8
+
9
+ class TestHostMeta < Test::Unit::TestCase
10
+ def test_get_uri_from_host
11
+ uri = 'host.example'
12
+ expected = URI.parse 'http://host.example/.well-known/host-meta'
13
+ assert_equal expected, Discodactyl::HostMeta.get_uri_from_uri(uri)
14
+ end
15
+
16
+ def test_get_uri_from_http
17
+ uri = URI.parse 'http://host.example/some/path'
18
+ expected = URI.parse 'http://host.example/.well-known/host-meta'
19
+ assert_equal expected, Discodactyl::HostMeta.get_uri_from_uri(uri)
20
+ end
21
+
22
+ def test_get_uri_from_acct
23
+ uri = URI.parse 'acct:user@host.example'
24
+ expected = URI.parse 'http://host.example/.well-known/host-meta'
25
+ assert_equal expected, Discodactyl::HostMeta.get_uri_from_uri(uri)
26
+ end
27
+
28
+ # def test_raise_meaningful_exception
29
+ # stub(io).status { [ '404', 'Not Found'] }
30
+ # stub(uri).open { raise OpenURI::HTTPError.new('foo', io)}
31
+ # stub(URI).parse('http://example.com/.well-known/host-meta') { uri }
32
+ #
33
+ # assert_raise Discodactyl::HostMetaHTTPError, "404 Not Found" do
34
+ # Discodactyl::HostMeta.from_uri(uri)
35
+ # end
36
+ # end
37
+ end
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby -w
2
+ libdir = File.expand_path('../../lib', __FILE__)
3
+ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
4
+
5
+ require "test/unit"
6
+ require "discodactyl/link_header"
7
+
8
+ class TestLinkHeader < Test::Unit::TestCase
9
+ def test_parse_rel_and_title
10
+ link = '<http://example.com/TheBook/chapter2>; rel="previous"; title="previous chapter"'
11
+ expected = {:href => 'http://example.com/TheBook/chapter2', :rel => ['previous'], :title => 'previous chapter'}
12
+ assert_equal expected, Discodactyl::LinkHeader.parse(link)
13
+ end
14
+
15
+ def test_parse_seperated_rels
16
+ link = '<http://example.org/>; rel=index; rel="start http://example.net/relation/other"'
17
+ expected = {:href => 'http://example.org/', :rel => ['index', 'start', 'http://example.net/relation/other']}
18
+ assert_equal expected, Discodactyl::LinkHeader.parse(link)
19
+ end
20
+
21
+ def test_parse_simple_link
22
+ link = '</bar>; rel="http://example.com/profile1/foo"'
23
+ expected = {:href => '/bar', :rel => ['http://example.com/profile1/foo']}
24
+ assert_equal expected, Discodactyl::LinkHeader.parse(link)
25
+ end
26
+
27
+ def test_parse_xrd_link
28
+ link = '<http://josephholsten.com/descriptor.xrd>; rel="describedby"; type="application/xrd+xml"'
29
+ expected = {:href => 'http://josephholsten.com/descriptor.xrd', :rel => ['describedby'], :type => 'application/xrd+xml' }
30
+ assert_equal expected, Discodactyl::LinkHeader.parse(link)
31
+ end
32
+ end
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby -w
2
+ libdir = File.expand_path('../../lib', __FILE__)
3
+ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
4
+ testdir = File.expand_path('../../test', __FILE__)
5
+ $LOAD_PATH.unshift(testdir) unless $LOAD_PATH.include?(testdir)
6
+
7
+ require 'test_helper'
8
+ require "test/unit"
9
+ require "discodactyl/resource_discovery"
10
+
11
+ class TestResourceDiscovery < Test::Unit::TestCase
12
+ def test_get_uris_by_rel_from_html
13
+ raw = '<html><head><title></title><link rel="describedby" href="http://host.example/description.xrd"></head><body></body></html>'
14
+
15
+ uris = Discodactyl::ResourceDiscovery.get_uris_by_rel_from_html(raw, 'describedby')
16
+
17
+ assert_include? 'http://host.example/description.xrd', uris
18
+ end
19
+
20
+ def test_get_uris_by_rel_from_html_with_multiple_rels
21
+ raw = '<html><head><title></title><link rel="also describedby" href="http://host.example/description.xrd"></head><body></body></html>'
22
+
23
+ uris = Discodactyl::ResourceDiscovery.get_uris_by_rel_from_html(raw, 'describedby')
24
+
25
+ assert_include? 'http://host.example/description.xrd', uris
26
+ end
27
+
28
+ def test_get_uris_by_rel_from_html_via_disco
29
+ require 'ostruct'
30
+ raw = '<html><head><title></title><link rel="describedby" href="http://host.example/description.xrd"></head><body></body></html>'
31
+ response = FakeResp.new(raw)
32
+ response.meta = {}
33
+ response.content_type = 'text/html'
34
+ uri = OpenStruct.new(:open => response)
35
+
36
+ uris = Discodactyl::ResourceDiscovery.get_uris_by_rel(uri, 'describedby')
37
+
38
+ assert_include? 'http://host.example/description.xrd', uris
39
+ end
40
+
41
+ def test_get_uris_by_rel_from_link_header
42
+ require 'ostruct'
43
+ header = '<http://host.example/description.xrd>; rel="describedby"'
44
+ response = OpenStruct.new(:meta => {'Link' => header})
45
+
46
+ uris = Discodactyl::ResourceDiscovery.get_uris_by_rel_from_link_header(response, 'describedby')
47
+
48
+ assert_equal 'http://host.example/description.xrd', uris
49
+ end
50
+
51
+ def test_get_uris_by_rel_from_header
52
+ require 'ostruct'
53
+ header = '<http://host.example/description.xrd>; rel="describedby"'
54
+ response = OpenStruct.new(:meta => {'Link' => header})
55
+ uri = OpenStruct.new(:open => response)
56
+
57
+ uris = Discodactyl::ResourceDiscovery.get_uris_by_rel(uri, 'describedby')
58
+
59
+ assert_equal 'http://host.example/description.xrd', uris
60
+ end
61
+ end
62
+
63
+ class FakeResp < String
64
+ attr_accessor :meta, :content_type
65
+ end