hypermedia 1.1.1 → 1.2.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.
- data/lib/api/api.rb +28 -1
- data/lib/api/document.rb +7 -53
- data/lib/api/entity.rb +103 -0
- data/lib/api/form.rb +24 -7
- data/lib/api/html.rb +48 -0
- data/lib/api/link.rb +18 -4
- data/lib/hypermedia.rb +2 -1
- metadata +3 -2
- data/lib/api/model.rb +0 -52
data/lib/api/api.rb
CHANGED
@@ -4,7 +4,9 @@ module HypermediaAPI
|
|
4
4
|
BadURI = Class.new(Exception)
|
5
5
|
MissingForm = Class.new(Exception)
|
6
6
|
MissingLink = Class.new(Exception)
|
7
|
+
UnsupportedHTTPMethod = Class.new(Exception)
|
7
8
|
|
9
|
+
# Returns an open Net::HTTP connection to the given URI.
|
8
10
|
def HypermediaAPI.connection (uri)
|
9
11
|
connection = Net::HTTP.new(uri.host, uri.port)
|
10
12
|
|
@@ -16,13 +18,30 @@ module HypermediaAPI
|
|
16
18
|
connection
|
17
19
|
end
|
18
20
|
|
21
|
+
# Raises an error. DELETE requests are unsupported.
|
22
|
+
def HypermediaAPI.delete (*)
|
23
|
+
raise HypermediaAPI::UnsupportedHTTPMethod, "Only GET and POST requests are supported (attempted DELETE)"
|
24
|
+
end
|
25
|
+
|
26
|
+
# GETs the given uri (with or without form data) and returns a
|
27
|
+
# HypermediaAPI::Document representing the response. Currently the valid
|
28
|
+
# options are 1) an :inputs Hash containing form data key-value pairs and
|
29
|
+
# 2) an :auth Hash containing HTTP basic auth username and password.
|
19
30
|
def HypermediaAPI.get (uri_string, options = {})
|
20
31
|
uri = URI(uri_string)
|
21
32
|
get = Net::HTTP::Get.new(uri.request_uri)
|
22
33
|
|
34
|
+
if options[:inputs]
|
35
|
+
get.set_form_data(options[:inputs])
|
36
|
+
end
|
37
|
+
|
23
38
|
HypermediaAPI.send_request(uri, get, options)
|
24
39
|
end
|
25
40
|
|
41
|
+
# POSTs form data to the given uri and returns a HypermediaAPI::Document
|
42
|
+
# representing the response. Currently the valid options are 1) an :inputs
|
43
|
+
# Hash containing form data key-value pairs and 2) an :auth Hash containing
|
44
|
+
# HTTP basic auth username and password.
|
26
45
|
def HypermediaAPI.post (uri_string, options = {})
|
27
46
|
uri = URI(uri_string)
|
28
47
|
post = Net::HTTP::Post.new(uri.request_uri)
|
@@ -31,8 +50,16 @@ module HypermediaAPI
|
|
31
50
|
HypermediaAPI.send_request(uri, post, options)
|
32
51
|
end
|
33
52
|
|
53
|
+
# Raises an error. PUT requests are unsupported.
|
54
|
+
def HypermediaAPI.put (*)
|
55
|
+
raise HypermediaAPI::UnsupportedHTTPMethod, "Only GET and POST requests are supported (attempted PUT)"
|
56
|
+
end
|
57
|
+
|
58
|
+
# Sends a request to the given URI and returns a HypermediaAPI::Document
|
59
|
+
# representing the response. Currently the only valid option is an :auth Hash
|
60
|
+
# containing HTTP basic auth username and password.
|
34
61
|
def HypermediaAPI.send_request (uri, request, options)
|
35
|
-
if auth = options[:
|
62
|
+
if auth = options[:auth]
|
36
63
|
request.basic_auth(auth[:username], auth[:password])
|
37
64
|
end
|
38
65
|
|
data/lib/api/document.rb
CHANGED
@@ -1,68 +1,22 @@
|
|
1
1
|
# encoding: UTF-8
|
2
2
|
|
3
3
|
class HypermediaAPI::Document
|
4
|
-
|
5
|
-
article_element = @document.at_css("article.#{klass.name.underscore}")
|
6
|
-
return nil unless article_element
|
7
|
-
|
8
|
-
klass.new_from_fields(article_element.element_children.filter('code'))
|
9
|
-
end
|
10
|
-
|
11
|
-
def entities (klass)
|
12
|
-
article_elements = @document.css("article.#{klass.name.underscore}")
|
13
|
-
article_elements.map {|article_element| klass.new_from_fields(article_element.element_children.filter('code')) }
|
14
|
-
end
|
15
|
-
|
16
|
-
def form (css_selector)
|
17
|
-
if form_element = @document.at_css(css_selector)
|
18
|
-
action_uri = form_element['action']
|
19
|
-
http_method = form_element['method'].downcase
|
20
|
-
|
21
|
-
HypermediaAPI::Form.new(action_uri, http_method)
|
22
|
-
else
|
23
|
-
raise HypermediaAPI::MissingForm, "The API does not have a form matching '#{css_selector}'."
|
24
|
-
end
|
25
|
-
end
|
4
|
+
include HypermediaAPI::Html
|
26
5
|
|
6
|
+
# Returns the document's HTTP headers as a Hash.
|
27
7
|
def headers
|
28
8
|
@headers
|
29
9
|
end
|
30
10
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
def initialize (nokogiri_html_document, status, headers, auth)
|
36
|
-
@document = nokogiri_html_document
|
11
|
+
# Sets the HTML, status, headers, and auth info of the new document.
|
12
|
+
def initialize (nokogiri_html, status, headers, http_basic_auth)
|
13
|
+
@nhtml = nokogiri_html
|
37
14
|
@status = status.to_i
|
38
15
|
@headers = headers
|
39
|
-
@
|
40
|
-
end
|
41
|
-
|
42
|
-
def link (css_selector)
|
43
|
-
if a_element = @document.at_css(css_selector)
|
44
|
-
href_uri = a_element['href']
|
45
|
-
HypermediaAPI::Link.new(href_uri)
|
46
|
-
else
|
47
|
-
raise HypermediaAPI::MissingLink, "The API does not have a link matching '#{css_selector}'."
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
def look_up_entity (klass, kv_pair)
|
52
|
-
index_link = self.link("nav#index-links a##{klass.name.underscore.pluralize}-link")
|
53
|
-
index_doc = index_link.click(basic_auth: @auth)
|
54
|
-
|
55
|
-
data_name, value = kv_pair.first
|
56
|
-
article_element = index_doc.instance_variable_get(:@document).at_xpath("//article[@class='#{klass.name.underscore}']/code[@data-name='#{data_name}' and text()='#{value}']/ancestor::article")
|
57
|
-
return nil unless article_element
|
58
|
-
|
59
|
-
bookmark_link_element = article_element.at_css('a[rel="bookmark"]')
|
60
|
-
bookmark_link = HypermediaAPI::Link.new(bookmark_link_element['href'])
|
61
|
-
entity_doc = bookmark_link.click(basic_auth: @auth)
|
62
|
-
|
63
|
-
entity_doc.entity(klass)
|
16
|
+
@http_basic_auth = http_basic_auth
|
64
17
|
end
|
65
18
|
|
19
|
+
# Returns the HTTP status code of the document as an integer.
|
66
20
|
def status
|
67
21
|
@status
|
68
22
|
end
|
data/lib/api/entity.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
class HypermediaAPI::Entity
|
4
|
+
include HypermediaAPI::Html
|
5
|
+
|
6
|
+
# Sets the fields for this entity class.
|
7
|
+
def self.fields (*field_names)
|
8
|
+
return (@field_names || []) if field_names.empty?
|
9
|
+
@field_names = field_names.map(&:to_sym).each {|field_name| attr_reader field_name }
|
10
|
+
end
|
11
|
+
|
12
|
+
# Returns a Hash containing the HTTP Basic Auth :username and :password for
|
13
|
+
# this entity class.
|
14
|
+
def self.auth
|
15
|
+
@auth ||= { username: '', password: '' }
|
16
|
+
end
|
17
|
+
|
18
|
+
# Sets the HTTP Basic Auth :username and :password for this entity class.
|
19
|
+
def self.authorize (username, password)
|
20
|
+
@auth = { username: username, password: password }
|
21
|
+
end
|
22
|
+
|
23
|
+
# Sets the authorization and API root url for subclasses of this entity class.
|
24
|
+
def self.inherited (subclass)
|
25
|
+
subclass.authorize(self.auth[:username], self.auth[:password])
|
26
|
+
subclass.root_url(@root_url)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Creates a new entity of this entity class using the HTML in article_element.
|
30
|
+
def self.new_from_article_element (article_element)
|
31
|
+
entity = self.new
|
32
|
+
entity.instance_variable_set(:"@nhtml", article_element)
|
33
|
+
entity.instance_variable_set(:"@http_basic_auth", self.auth)
|
34
|
+
|
35
|
+
if bookmark_a_element = article_element.element_children.filter('a[rel=bookmark]').first
|
36
|
+
link = HypermediaAPI::Link.new(bookmark_a_element['href'], self.auth)
|
37
|
+
entity.instance_variable_set(:"@bookmark_link", link)
|
38
|
+
end
|
39
|
+
|
40
|
+
values = article_element.element_children.filter('code').map do |field_element|
|
41
|
+
value_str = field_element.content
|
42
|
+
name = field_element['data-name']
|
43
|
+
|
44
|
+
value = case field_element['data-type']
|
45
|
+
when 'date' then value_str.empty? ? nil : Date.parse(value_str)
|
46
|
+
when 'integer' then value_str.empty? ? nil : value_str.to_i
|
47
|
+
when 'float' then value_str.empty? ? nil : value_str.to_f
|
48
|
+
when 'boolean' then value_str.empty? ? nil : value_str != 'false'
|
49
|
+
when 'string' then value_str
|
50
|
+
end
|
51
|
+
|
52
|
+
[name, value]
|
53
|
+
end
|
54
|
+
|
55
|
+
values.each do |name, value|
|
56
|
+
next unless entity.respond_to?(name)
|
57
|
+
entity.instance_variable_set(:"@#{name}", value)
|
58
|
+
end
|
59
|
+
|
60
|
+
entity
|
61
|
+
end
|
62
|
+
|
63
|
+
# Submits a query using the named query form found in the API root document of
|
64
|
+
# this entity class, then returns a Hypermedia::Document representing the
|
65
|
+
# response.
|
66
|
+
def self.query (query_name, form_values)
|
67
|
+
query_form = self.root_doc.form("##{query_name.to_s.dasherize}")
|
68
|
+
query_form.submit(form_values)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns the API root document for this entity class.
|
72
|
+
def self.root_doc
|
73
|
+
@root_doc ||= HypermediaAPI.get(@root_url, auth: self.auth)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Sets the API root url for this entity class.
|
77
|
+
def self.root_url (url)
|
78
|
+
@root_url = url
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns a HypermediaAPI::Link pointing to the bookmark uri of the entity.
|
82
|
+
def bookmark_link
|
83
|
+
@bookmark_link
|
84
|
+
end
|
85
|
+
|
86
|
+
# Reloads the HypermediaAPI::Entity from the uri in the bookmark link.
|
87
|
+
def reload!
|
88
|
+
response = self.bookmark_link.click
|
89
|
+
|
90
|
+
if response.status == 200
|
91
|
+
entity = response.entity(self.class)
|
92
|
+
|
93
|
+
self.instance_variable_set(:"@nhtml", entity.instance_variable_get(:"@nhtml"))
|
94
|
+
self.instance_variable_set(:"@bookmark_link", entity.instance_variable_get(:"@bookmark_link"))
|
95
|
+
|
96
|
+
self.class.fields.each do |field_name|
|
97
|
+
self.instance_variable_set(:"@#{field_name}", entity.send(field_name))
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
self
|
102
|
+
end
|
103
|
+
end
|
data/lib/api/form.rb
CHANGED
@@ -1,16 +1,33 @@
|
|
1
1
|
# encoding: UTF-8
|
2
2
|
|
3
3
|
class HypermediaAPI::Form
|
4
|
-
|
5
|
-
|
6
|
-
@
|
4
|
+
# Returns the action uri of this form.
|
5
|
+
def action
|
6
|
+
@action_uri_str
|
7
7
|
end
|
8
8
|
|
9
|
+
# Returns the HTTP basic auth of this form.
|
10
|
+
def auth
|
11
|
+
@http_basic_auth
|
12
|
+
end
|
13
|
+
|
14
|
+
# Sets the action uri, HTTP method, and HTTP basic auth of this form.
|
15
|
+
def initialize (action_uri_str, http_method, http_basic_auth = nil)
|
16
|
+
@action_uri_str = action_uri_str.to_s
|
17
|
+
@http_method = http_method.to_s.upcase
|
18
|
+
@http_basic_auth = http_basic_auth || { username: '', password: '' }
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns the HTTP method of this form.
|
22
|
+
def method
|
23
|
+
@http_method
|
24
|
+
end
|
25
|
+
|
26
|
+
# Submits this form with the given inputs, then returns a Hypermedia::Document
|
27
|
+
# representing the response.
|
9
28
|
def submit (inputs, options = {})
|
10
|
-
|
11
|
-
when 'post' then HypermediaAPI.post(@action_uri, options.merge(inputs: inputs))
|
12
|
-
end
|
29
|
+
HypermediaAPI.send(self.method.downcase, self.action, { inputs: inputs, auth: self.auth }.merge(options))
|
13
30
|
rescue SocketError
|
14
|
-
raise HypermediaAPI::BadURI, "The client was unable to interact with a resource at #{
|
31
|
+
raise HypermediaAPI::BadURI, "The client was unable to interact with a resource at #{self.action}."
|
15
32
|
end
|
16
33
|
end
|
data/lib/api/html.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module HypermediaAPI::Html
|
4
|
+
# Returns the HTTP basic auth of the HTML.
|
5
|
+
def auth
|
6
|
+
@http_basic_auth
|
7
|
+
end
|
8
|
+
|
9
|
+
# Returns the first HypermediaAPI::Entity of the given class found in the
|
10
|
+
# HTML, or nil if no such entity exists.
|
11
|
+
def entity (entity_class)
|
12
|
+
article_element = @nhtml.at_css("article.#{entity_class.name.underscore}")
|
13
|
+
entity_class.new_from_article_element(article_element)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns an array of every HypermediaAPI::Entity of the given class found in
|
17
|
+
# the HTML.
|
18
|
+
def entities (entity_class)
|
19
|
+
@nhtml.css("article.#{entity_class.name.underscore}").map do |article_element|
|
20
|
+
entity_class.new_from_article_element(article_element)
|
21
|
+
end.compact
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns a HypermediaAPI::Form representing the first form element that
|
25
|
+
# matches the given css_selector. If no such form is found, raises an error.
|
26
|
+
def form (css_selector)
|
27
|
+
if form_element = @nhtml.css(css_selector).filter('form').first
|
28
|
+
HypermediaAPI::Form.new(form_element['action'], form_element['method'], self.auth)
|
29
|
+
else
|
30
|
+
raise HypermediaAPI::MissingForm, "There is no form matching '#{css_selector}' in HTML:\n#{self.html}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns the HTML as a string.
|
35
|
+
def html
|
36
|
+
@nhtml.to_html
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns a HypermediaAPI::Link representing the first anchor element that
|
40
|
+
# matches the given css_selector. If no such anchor is found, raises an error.
|
41
|
+
def link (css_selector)
|
42
|
+
if a_element = @nhtml.css(css_selector).filter('a').first
|
43
|
+
HypermediaAPI::Link.new(a_element['href'], self.auth)
|
44
|
+
else
|
45
|
+
raise HypermediaAPI::MissingLink, "There is no link matching '#{css_selector}' in HTML:\n#{self.html}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/api/link.rb
CHANGED
@@ -1,13 +1,27 @@
|
|
1
1
|
# encoding: UTF-8
|
2
2
|
|
3
3
|
class HypermediaAPI::Link
|
4
|
-
|
5
|
-
|
4
|
+
# Returns the HTTP basic auth of this link.
|
5
|
+
def auth
|
6
|
+
@http_basic_auth
|
6
7
|
end
|
7
8
|
|
9
|
+
# Sends a GET request to the href uri of this link, then returns a
|
10
|
+
# HypermediaAPI::Document representing the response.
|
8
11
|
def click (options = {})
|
9
|
-
document = HypermediaAPI.get(
|
12
|
+
document = HypermediaAPI.get(self.href, { auth: self.auth }.merge(options))
|
10
13
|
rescue SocketError
|
11
|
-
raise HypermediaAPI::BadURI, "The client was unable to interact with a resource at #{
|
14
|
+
raise HypermediaAPI::BadURI, "The client was unable to interact with a resource at #{self.href}."
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns the href uri of this link.
|
18
|
+
def href
|
19
|
+
@href_uri_str
|
20
|
+
end
|
21
|
+
|
22
|
+
# Sets the href uri and HTTP basic auth of this link.
|
23
|
+
def initialize (href_uri_str, http_basic_auth = nil)
|
24
|
+
@href_uri_str = href_uri_str.to_s
|
25
|
+
@http_basic_auth = http_basic_auth || { username: '', password: '' }
|
12
26
|
end
|
13
27
|
end
|
data/lib/hypermedia.rb
CHANGED
@@ -6,7 +6,8 @@ require 'net/http'
|
|
6
6
|
require 'active_support/all'
|
7
7
|
|
8
8
|
require_relative 'api/api.rb'
|
9
|
+
require_relative 'api/html.rb'
|
9
10
|
require_relative 'api/document.rb'
|
11
|
+
require_relative 'api/entity.rb'
|
10
12
|
require_relative 'api/form.rb'
|
11
13
|
require_relative 'api/link.rb'
|
12
|
-
require_relative 'api/model.rb'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hypermedia
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -51,9 +51,10 @@ extra_rdoc_files: []
|
|
51
51
|
files:
|
52
52
|
- ./lib/api/api.rb
|
53
53
|
- ./lib/api/document.rb
|
54
|
+
- ./lib/api/entity.rb
|
54
55
|
- ./lib/api/form.rb
|
56
|
+
- ./lib/api/html.rb
|
55
57
|
- ./lib/api/link.rb
|
56
|
-
- ./lib/api/model.rb
|
57
58
|
- ./lib/hypermedia.rb
|
58
59
|
homepage:
|
59
60
|
licenses: []
|
data/lib/api/model.rb
DELETED
@@ -1,52 +0,0 @@
|
|
1
|
-
# encoding: UTF-8
|
2
|
-
|
3
|
-
class HypermediaAPI::Model
|
4
|
-
def self.auth
|
5
|
-
@auth ||= { username: '', password: '' }
|
6
|
-
end
|
7
|
-
|
8
|
-
def self.authorize (username, password)
|
9
|
-
@auth = { username: username, password: password }
|
10
|
-
end
|
11
|
-
|
12
|
-
def self.inherited (subclass)
|
13
|
-
subclass.authorize(self.auth[:username], self.auth[:password])
|
14
|
-
subclass.root_url(@root_url)
|
15
|
-
end
|
16
|
-
|
17
|
-
def self.new_from_fields (field_elements)
|
18
|
-
instance = self.new
|
19
|
-
|
20
|
-
field_elements.each do |field_element|
|
21
|
-
value_str = field_element.content
|
22
|
-
name = field_element['data-name']
|
23
|
-
|
24
|
-
value = case field_element['data-type']
|
25
|
-
when 'date' then value_str.empty? ? nil : Date.parse(value_str)
|
26
|
-
when 'integer' then value_str.empty? ? nil : value_str.to_i
|
27
|
-
when 'float' then value_str.empty? ? nil : value_str.to_f
|
28
|
-
when 'boolean' then value_str.empty? ? nil : value_str != 'false'
|
29
|
-
when 'string' then value_str
|
30
|
-
end
|
31
|
-
|
32
|
-
if instance.respond_to?(name)
|
33
|
-
instance.send(:"#{name}=", value)
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
instance
|
38
|
-
end
|
39
|
-
|
40
|
-
def self.root_url (url)
|
41
|
-
@root_url = url
|
42
|
-
end
|
43
|
-
|
44
|
-
def self.root_doc
|
45
|
-
@root_doc ||= HypermediaAPI.get(@root_url, basic_auth: self.auth)
|
46
|
-
end
|
47
|
-
|
48
|
-
def self.query (query_name, form_values)
|
49
|
-
query_form = self.root_doc.form("##{query_name.to_s.dasherize}")
|
50
|
-
query_form.submit(form_values, basic_auth: self.auth)
|
51
|
-
end
|
52
|
-
end
|