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 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