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