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 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[:basic_auth]
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
- def entity (klass)
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
- def html
32
- @document.to_html
33
- end
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
- @auth = auth
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
- def initialize (action_uri, http_method)
5
- @action_uri = action_uri
6
- @http_method = http_method.downcase
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
- case @http_method
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 #{@action_uri}."
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
- def initialize (href_uri)
5
- @href_uri = href_uri
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(@href_uri, options)
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 #{@href_uri}."
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.1.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