hyperclient 0.0.8 → 0.1.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/Gemfile +2 -1
- data/Readme.md +15 -21
- data/examples/cyberscore.rb +28 -16
- data/examples/hal_shop.rb +15 -22
- data/hyperclient.gemspec +2 -0
- data/lib/hyperclient.rb +2 -84
- data/lib/hyperclient/attributes.rb +20 -0
- data/lib/hyperclient/collection.rb +53 -0
- data/lib/hyperclient/entry_point.rb +49 -0
- data/lib/hyperclient/http.rb +38 -37
- data/lib/hyperclient/link.rb +93 -0
- data/lib/hyperclient/link_collection.rb +24 -0
- data/lib/hyperclient/resource.rb +26 -65
- data/lib/hyperclient/resource_collection.rb +33 -0
- data/lib/hyperclient/version.rb +1 -1
- data/test/fixtures/element.json +4 -15
- data/test/hyperclient/attributes_test.rb +26 -0
- data/test/hyperclient/collection_test.rb +39 -0
- data/test/hyperclient/entry_point_test.rb +51 -0
- data/test/hyperclient/http_test.rb +37 -23
- data/test/hyperclient/link_collection_test.rb +29 -0
- data/test/hyperclient/link_test.rb +93 -0
- data/test/hyperclient/resource_collection_test.rb +34 -0
- data/test/hyperclient/resource_test.rb +36 -42
- data/test/test_helper.rb +10 -1
- metadata +55 -16
- data/lib/hyperclient/discoverer.rb +0 -84
- data/lib/hyperclient/representation.rb +0 -43
- data/lib/hyperclient/resource_factory.rb +0 -53
- data/test/hyperclient/discoverer_test.rb +0 -101
- data/test/hyperclient/representation_test.rb +0 -52
- data/test/hyperclient/resource_factory_test.rb +0 -32
- data/test/hyperclient_test.rb +0 -68
data/Gemfile
CHANGED
data/Readme.md
CHANGED
@@ -14,13 +14,12 @@ Hyperclient is a Ruby Hypermedia API client written in Ruby.
|
|
14
14
|
Example API client:
|
15
15
|
|
16
16
|
````ruby
|
17
|
-
|
18
|
-
|
17
|
+
options = {}
|
18
|
+
options[:auth] = {type: :digest, user:, 'user', password: 'password'}
|
19
|
+
options[:headers] = {'accept-encoding' => 'deflate, gzip'}
|
20
|
+
options[:debug] = true
|
19
21
|
|
20
|
-
|
21
|
-
auth :digest, 'user', 'password'
|
22
|
-
http_options headers: {'accept-encoding' => 'deflate, gzip'}, debug: true
|
23
|
-
end
|
22
|
+
api = Hyperclient::EntryPoint.new('http://myapp.com/api', options)
|
24
23
|
````
|
25
24
|
|
26
25
|
[More examples][examples]
|
@@ -38,16 +37,15 @@ Hyperclient will try to fetch and discover the resources from your API.
|
|
38
37
|
Accessing the links for a given resource is quite straightforward:
|
39
38
|
|
40
39
|
````ruby
|
41
|
-
api = MyAPIClient.new
|
42
40
|
api.links.posts_categories
|
43
|
-
# => #<Resource
|
41
|
+
# => #<Resource ...>
|
44
42
|
````
|
45
43
|
|
46
44
|
You can also iterate between all the links:
|
47
45
|
|
48
46
|
````ruby
|
49
|
-
api.links.each do |link|
|
50
|
-
puts
|
47
|
+
api.links.each do |name, link|
|
48
|
+
puts name, link.url
|
51
49
|
end
|
52
50
|
````
|
53
51
|
|
@@ -56,7 +54,6 @@ Actually, you can call any [Enumerable][enumerable] method :D
|
|
56
54
|
If a Resource doesn't have friendly name you can always access it as a Hash:
|
57
55
|
|
58
56
|
````ruby
|
59
|
-
api = MyAPIClient.new
|
60
57
|
api.links['http://myapi.org/rels/post_categories']
|
61
58
|
````
|
62
59
|
|
@@ -65,24 +62,21 @@ api.links['http://myapi.org/rels/post_categories']
|
|
65
62
|
Accessing embedded resources is similar to accessing links:
|
66
63
|
|
67
64
|
````ruby
|
68
|
-
api
|
69
|
-
api.resources.posts
|
70
|
-
# => #<Resource @name="posts" ...>
|
65
|
+
api.embedded.posts
|
71
66
|
````
|
72
67
|
|
73
68
|
And you can also iterate between them:
|
74
69
|
|
75
70
|
````ruby
|
76
|
-
api.
|
77
|
-
puts
|
71
|
+
api.embedded.each do |name, resource|
|
72
|
+
puts name, resource.attributes
|
78
73
|
end
|
79
74
|
````
|
80
75
|
|
81
76
|
You can even chain different calls (this also applies for links):
|
82
77
|
|
83
78
|
````ruby
|
84
|
-
api.
|
85
|
-
# => #<Resource @name="author" ...>
|
79
|
+
api.embedded.posts.first.links.author
|
86
80
|
````
|
87
81
|
|
88
82
|
### Attributes
|
@@ -91,7 +85,7 @@ Not only you might have links and embedded resources in a Resource, but also
|
|
91
85
|
its attributes:
|
92
86
|
|
93
87
|
````ruby
|
94
|
-
api.
|
88
|
+
api.embedded.posts.first.attributes
|
95
89
|
# => {title: 'Linting the hell out of your Ruby classes with Pelusa',
|
96
90
|
teaser: 'Gain new insights about your code thanks to static analysis',
|
97
91
|
body: '...' }
|
@@ -106,14 +100,14 @@ Hyperclient uses [HTTParty][httparty] under the hood to perform HTTP calls. You
|
|
106
100
|
call any valid HTTP method on any Resource:
|
107
101
|
|
108
102
|
````ruby
|
109
|
-
post = api.
|
103
|
+
post = api.embedded.posts.first
|
110
104
|
post.get
|
111
105
|
post.head
|
112
106
|
post.put({title: 'New title'})
|
113
107
|
post.delete
|
114
108
|
post.options
|
115
109
|
|
116
|
-
posts = api.
|
110
|
+
posts = api.links.posts
|
117
111
|
posts.post({title: "I'm a blogger!", body: 'Wohoo!!'})
|
118
112
|
````
|
119
113
|
|
data/examples/cyberscore.rb
CHANGED
@@ -3,42 +3,54 @@ require 'hyperclient'
|
|
3
3
|
class Cyberscore
|
4
4
|
include Hyperclient
|
5
5
|
|
6
|
-
entry_point 'http://cs-api.heroku.com/api/'
|
7
|
-
|
8
6
|
def news
|
9
|
-
|
7
|
+
client.links.submissions.embedded.news
|
10
8
|
end
|
11
9
|
|
12
10
|
def submissions
|
13
|
-
|
11
|
+
client.links..submissions.embedded.submissions
|
14
12
|
end
|
15
13
|
|
16
14
|
def games
|
17
|
-
|
15
|
+
client.links.games.embedded.games
|
18
16
|
end
|
19
17
|
|
20
18
|
def add_game(name)
|
21
|
-
|
19
|
+
client.links.submissions.post({name: name})
|
22
20
|
end
|
23
21
|
|
24
22
|
def motd
|
25
|
-
attributes
|
23
|
+
client.attributes.motd
|
24
|
+
end
|
25
|
+
|
26
|
+
def method_missing(method, *args, &block)
|
27
|
+
if client.respond_to?(method)
|
28
|
+
client.send(method, *args, &block)
|
29
|
+
else
|
30
|
+
super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def client
|
36
|
+
@client ||= Hyperclient::EntryPoint.new 'http://cs-api.heroku.com/api',
|
37
|
+
{debug: false, headers: {'content-type' => 'application/json'}}
|
26
38
|
end
|
27
39
|
end
|
28
40
|
|
29
|
-
def
|
30
|
-
|
31
|
-
if
|
32
|
-
|
41
|
+
def print_links(links)
|
42
|
+
links.each do |name, link|
|
43
|
+
if link.is_a?(Array)
|
44
|
+
print_links(link)
|
33
45
|
else
|
34
|
-
puts %{Found "#{
|
46
|
+
puts %{Found "#{name}" at "#{link.url}" }
|
35
47
|
end
|
36
48
|
end
|
37
49
|
end
|
38
50
|
|
39
51
|
def print_games(games)
|
40
52
|
games.each do |game|
|
41
|
-
puts %{Found "#{game.attributes['name']}"
|
53
|
+
puts %{Found "#{game.attributes['name']}" }
|
42
54
|
end
|
43
55
|
end
|
44
56
|
|
@@ -49,15 +61,15 @@ puts "Let's inspect the API:"
|
|
49
61
|
puts "\n"
|
50
62
|
|
51
63
|
puts 'Links from the entry point:'
|
52
|
-
|
64
|
+
print_links(api.links)
|
53
65
|
puts "\n"
|
54
66
|
|
55
67
|
puts 'How is the server feeling today?'
|
56
68
|
puts api.motd
|
57
69
|
puts "\n"
|
58
70
|
|
59
|
-
puts "Let's read the
|
60
|
-
|
71
|
+
puts "Let's read the news:"
|
72
|
+
print_links(api.links.news.links)
|
61
73
|
puts "\n"
|
62
74
|
|
63
75
|
puts "I like games!"
|
data/examples/hal_shop.rb
CHANGED
@@ -1,18 +1,12 @@
|
|
1
1
|
require 'hyperclient'
|
2
|
-
|
3
|
-
class HalShop
|
4
|
-
include Hyperclient
|
5
|
-
|
6
|
-
entry_point 'http://hal-shop.heroku.com'
|
7
|
-
http_options debug: true
|
8
|
-
end
|
2
|
+
require 'pp'
|
9
3
|
|
10
4
|
def print_resources(resources)
|
11
|
-
resources.each do |resource|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
puts %{Found
|
5
|
+
resources.each do |name, resource|
|
6
|
+
begin
|
7
|
+
puts %{Found #{name} at #{resource.url}}
|
8
|
+
rescue
|
9
|
+
puts %{Found #{name}}
|
16
10
|
end
|
17
11
|
end
|
18
12
|
end
|
@@ -24,7 +18,7 @@ def print_attributes(attributes)
|
|
24
18
|
end
|
25
19
|
end
|
26
20
|
|
27
|
-
api =
|
21
|
+
api = Hyperclient::EntryPoint.new 'http://hal-shop.heroku.com'
|
28
22
|
|
29
23
|
puts "Let's inspect the API:"
|
30
24
|
puts "\n"
|
@@ -33,25 +27,24 @@ puts 'Links from the entry point:'
|
|
33
27
|
|
34
28
|
print_resources(api.links)
|
35
29
|
|
36
|
-
puts
|
37
|
-
puts 'Resources at the entry point:'
|
38
|
-
print_resources(api.embedded)
|
39
|
-
|
40
30
|
puts
|
41
31
|
puts "Let's see what stats we have:"
|
42
32
|
print_attributes(api.embedded.stats.attributes)
|
43
33
|
|
44
|
-
products = api.links["http://hal-shop.heroku.com/rels/products"].
|
34
|
+
products = api.links["http://hal-shop.heroku.com/rels/products"].resource
|
45
35
|
|
46
36
|
puts
|
47
37
|
puts "And what's the inventory of products?"
|
48
38
|
puts products.attributes['inventory_size']
|
49
39
|
|
50
|
-
puts
|
51
|
-
puts 'What resources does products have?'
|
52
|
-
print_resources(products.embedded.products)
|
53
|
-
|
54
40
|
puts
|
41
|
+
puts 'What embedded resources does products have?'
|
42
|
+
products.embedded.products.each do |product|
|
43
|
+
puts 'Product:'
|
44
|
+
print_attributes(product.attributes)
|
45
|
+
puts
|
46
|
+
end
|
47
|
+
|
55
48
|
puts 'And links?'
|
56
49
|
print_resources(products.links)
|
57
50
|
|
data/hyperclient.gemspec
CHANGED
@@ -15,8 +15,10 @@ Gem::Specification.new do |gem|
|
|
15
15
|
gem.version = Hyperclient::VERSION
|
16
16
|
|
17
17
|
gem.add_dependency 'httparty'
|
18
|
+
gem.add_dependency 'uri_template'
|
18
19
|
|
19
20
|
gem.add_development_dependency 'minitest'
|
20
21
|
gem.add_development_dependency 'turn'
|
21
22
|
gem.add_development_dependency 'webmock'
|
23
|
+
gem.add_development_dependency 'mocha'
|
22
24
|
end
|
data/lib/hyperclient.rb
CHANGED
@@ -1,89 +1,7 @@
|
|
1
|
-
# Public:
|
2
|
-
# API client.
|
3
|
-
#
|
4
|
-
# Examples
|
5
|
-
#
|
6
|
-
# class MyAPI
|
7
|
-
# extend Hyperclient
|
8
|
-
#
|
9
|
-
# entry_point 'http://api.myapp.com'
|
10
|
-
# end
|
1
|
+
# Public: Hyperclient namespace.
|
11
2
|
#
|
12
3
|
module Hyperclient
|
13
|
-
# Internal: Extend the parent class with our class methods.
|
14
|
-
def self.included(base)
|
15
|
-
base.send :extend, ClassMethods
|
16
|
-
end
|
17
|
-
|
18
|
-
# Public: Initializes the API with the entry point.
|
19
|
-
def entry
|
20
|
-
@entry ||= Resource.new('', resource_options)
|
21
|
-
end
|
22
|
-
|
23
|
-
# Internal: Delegate the method to the API if it exists.
|
24
|
-
#
|
25
|
-
# This way we can call our API client with the resources name instead of
|
26
|
-
# having to add the methods to it.
|
27
|
-
def method_missing(method, *args, &block)
|
28
|
-
if entry.respond_to?(method)
|
29
|
-
entry.send(method, *args, &block)
|
30
|
-
else
|
31
|
-
super
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
module ClassMethods
|
36
|
-
# Public: Set the entry point of your API.
|
37
|
-
#
|
38
|
-
# url - A block to pass the API url.
|
39
|
-
#
|
40
|
-
# Returns nothing.
|
41
|
-
def entry_point(&url)
|
42
|
-
Resource.entry_point = url.call
|
43
|
-
end
|
44
|
-
|
45
|
-
# Public: Sets the authentication options for your API client.
|
46
|
-
#
|
47
|
-
# options - A block used to pass authentication options. Needed data is:
|
48
|
-
# type - A String or Symbol with the authentication method. Can be
|
49
|
-
# either :basic or :digest.
|
50
|
-
# user - A String with the user.
|
51
|
-
# password - A String with the password.
|
52
|
-
#
|
53
|
-
# Example:
|
54
|
-
# auth{ {type: :digest, user: 'user', password: 'secret'} }
|
55
|
-
#
|
56
|
-
# Returns nothing.
|
57
|
-
def auth(&options)
|
58
|
-
options = options.call
|
59
|
-
http_options({auth: {type: options[:type], credentials: [options[:user], options[:password]]}})
|
60
|
-
end
|
61
|
-
|
62
|
-
# Public: Sets the HTTP options that will be used to initialize
|
63
|
-
# Hyperclient::HTTP.
|
64
|
-
#
|
65
|
-
# options - A Hash with options to pass to HTTP.
|
66
|
-
#
|
67
|
-
# Example:
|
68
|
-
#
|
69
|
-
# http_options headers: {'accept-encoding' => 'deflate, gzip'}
|
70
|
-
#
|
71
|
-
# Returns a Hash.
|
72
|
-
def http_options(options = {})
|
73
|
-
@@http_options ||= {}
|
74
|
-
@@http_options.merge!(options)
|
75
|
-
|
76
|
-
{http: @@http_options}
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
private
|
81
|
-
# Internal: Returns a Hash with the options to initialize the entry point
|
82
|
-
# Resource.
|
83
|
-
def resource_options
|
84
|
-
{name: 'Entry point'}.merge(self.class.http_options)
|
85
|
-
end
|
86
4
|
end
|
87
5
|
|
88
|
-
require 'hyperclient/
|
6
|
+
require 'hyperclient/entry_point'
|
89
7
|
require "hyperclient/version"
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'hyperclient/collection'
|
2
|
+
|
3
|
+
module Hyperclient
|
4
|
+
# Public: A wrapper class to easily acces the attributes in a Resource.
|
5
|
+
#
|
6
|
+
# Examples
|
7
|
+
#
|
8
|
+
# resource.attributes['title']
|
9
|
+
# resource.attributes.title
|
10
|
+
#
|
11
|
+
class Attributes < Collection
|
12
|
+
# Public: Initializes the Attributes of a Resource.
|
13
|
+
#
|
14
|
+
# representation - The hash with the HAL representation of the Resource.
|
15
|
+
#
|
16
|
+
def initialize(representation)
|
17
|
+
@collection = representation.delete_if {|key, value| key =~ /^_/}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Hyperclient
|
2
|
+
# Public: A helper class to wrapp a collection of elements and provide
|
3
|
+
# Hash-like access or via a method call.
|
4
|
+
#
|
5
|
+
# Examples
|
6
|
+
#
|
7
|
+
# collection['value']
|
8
|
+
# collection.value
|
9
|
+
#
|
10
|
+
class Collection
|
11
|
+
include Enumerable
|
12
|
+
|
13
|
+
# Public: Initializes the Collection.
|
14
|
+
#
|
15
|
+
# collection - The Hash to be wrapped.
|
16
|
+
def initialize(collection)
|
17
|
+
@collection = collection
|
18
|
+
end
|
19
|
+
|
20
|
+
# Public: Each implementation to allow the class to use the Enumerable
|
21
|
+
# benefits.
|
22
|
+
#
|
23
|
+
# Returns an Enumerator.
|
24
|
+
def each(&block)
|
25
|
+
@collection.each(&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Public: Provides Hash-like access to the collection.
|
29
|
+
#
|
30
|
+
# name - A String or Symbol of the value to get from the collection.
|
31
|
+
#
|
32
|
+
# Returns an Object.
|
33
|
+
def [](name)
|
34
|
+
@collection[name.to_s]
|
35
|
+
end
|
36
|
+
|
37
|
+
# Public: Provides method access to the collection values.
|
38
|
+
#
|
39
|
+
# It allows accessing a value as `collection.name` instead of
|
40
|
+
# `collection['name']`
|
41
|
+
#
|
42
|
+
# Returns an Object.
|
43
|
+
def method_missing(method_name, *args, &block)
|
44
|
+
@collection.fetch(method_name.to_s) { super }
|
45
|
+
end
|
46
|
+
|
47
|
+
# Internal: Accessory method to allow the collection respond to the
|
48
|
+
# methods that will hit method_missing.
|
49
|
+
def respond_to_missing?(method_name, include_private = false)
|
50
|
+
@collection.include?(method_name.to_s)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'hyperclient/link'
|
2
|
+
|
3
|
+
module Hyperclient
|
4
|
+
# Public: The EntryPoint is the main public API for Hyperclient. It is used to
|
5
|
+
# initialize an API client and setup the configuration.
|
6
|
+
#
|
7
|
+
# Examples
|
8
|
+
#
|
9
|
+
# options = {}
|
10
|
+
# options[:headers] = {'accept-encoding' => 'deflate, gzip'}
|
11
|
+
# options[:auth] = {type: 'digest', user: 'foo', password: 'secret'}
|
12
|
+
# options[:debug] = true
|
13
|
+
#
|
14
|
+
# client = Hyperclient::EntryPoint.new('http://my.api.org', options)
|
15
|
+
#
|
16
|
+
class EntryPoint
|
17
|
+
|
18
|
+
# Public: Returns the Hash with the configuration.
|
19
|
+
attr_accessor :config
|
20
|
+
|
21
|
+
# Public: Initializes an EntryPoint.
|
22
|
+
#
|
23
|
+
# url - A String with the entry point of your API.
|
24
|
+
# config - The Hash options used to setup the HTTP client (default: {})
|
25
|
+
# See HTTP for more documentation.
|
26
|
+
def initialize(url, config = {})
|
27
|
+
@config = config.update(base_uri: url)
|
28
|
+
@entry = Link.new({'href' => url}, self).resource
|
29
|
+
end
|
30
|
+
|
31
|
+
# Internal: Delegate the method to the entry point Resource if it exists.
|
32
|
+
#
|
33
|
+
# This way we can call our API client with the resources name instead of
|
34
|
+
# having to add the methods to it.
|
35
|
+
def method_missing(method, *args, &block)
|
36
|
+
if @entry.respond_to?(method)
|
37
|
+
@entry.send(method, *args, &block)
|
38
|
+
else
|
39
|
+
super
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Internal: Accessory method to allow the entry point respond to the
|
44
|
+
# methods that will hit method_missing.
|
45
|
+
def respond_to_missing?(method, include_private = false)
|
46
|
+
@entry.respond_to?(method.to_s)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|