hypermedia 1.1.1 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|