hyperclient 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rvmrc +1 -1
- data/.travis.yml +2 -0
- data/.yardopts +8 -0
- data/Gemfile +9 -1
- data/Guardfile +6 -0
- data/LICENSE +1 -1
- data/MIT-LICENSE +20 -0
- data/Rakefile +27 -1
- data/Readme.md +145 -0
- data/examples/hal_shop.rb +59 -0
- data/hyperclient.gemspec +11 -6
- data/lib/hyperclient.rb +72 -3
- data/lib/hyperclient/discoverer.rb +63 -0
- data/lib/hyperclient/http.rb +90 -0
- data/lib/hyperclient/resource.rb +82 -0
- data/lib/hyperclient/response.rb +42 -0
- data/lib/hyperclient/version.rb +1 -1
- data/test/fixtures/collection.json +34 -0
- data/test/fixtures/element.json +65 -0
- data/test/fixtures/root.json +6 -0
- data/test/hyperclient/discoverer_test.rb +76 -0
- data/test/hyperclient/http_test.rb +96 -0
- data/test/hyperclient/resource_test.rb +80 -0
- data/test/hyperclient/response_test.rb +50 -0
- data/test/hyperclient_test.rb +63 -0
- data/test/test_helper.rb +7 -0
- metadata +110 -12
- data/README.md +0 -71
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
# Public: A parser for HTTParty that understand the mime application/hal+json.
|
5
|
+
class JSONHalParser < HTTParty::Parser
|
6
|
+
SupportedFormats.merge!({'application/hal+json' => :json})
|
7
|
+
end
|
8
|
+
|
9
|
+
module Hyperclient
|
10
|
+
# Internal: This class wrapps HTTParty and performs the HTTP requests for a
|
11
|
+
# resource.
|
12
|
+
class HTTP
|
13
|
+
extend Forwardable
|
14
|
+
include HTTParty
|
15
|
+
|
16
|
+
parser JSONHalParser
|
17
|
+
|
18
|
+
# Private: Delegate the url to the resource.
|
19
|
+
def_delegators :@resource, :url
|
20
|
+
|
21
|
+
# Public: Initializes a HTTP agent.
|
22
|
+
#
|
23
|
+
# resource - A Resource instance. A Resource is given instead of the url
|
24
|
+
# since the resource url could change during its live.
|
25
|
+
def initialize(resource, options = {})
|
26
|
+
@resource = resource
|
27
|
+
authenticate(options[:auth]) if options && options.include?(:auth)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Public: Sends a GET request the the resource url.
|
31
|
+
#
|
32
|
+
# Returns: The response parsed response.
|
33
|
+
def get
|
34
|
+
self.class.get(url).parsed_response
|
35
|
+
end
|
36
|
+
|
37
|
+
# Public: Sends a POST request the the resource url.
|
38
|
+
#
|
39
|
+
# params - A Hash to send as POST params
|
40
|
+
#
|
41
|
+
# Returns: A HTTParty::Response
|
42
|
+
def post(params)
|
43
|
+
self.class.post(url, body: params)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Public: Sends a PUT request the the resource url.
|
47
|
+
#
|
48
|
+
# params - A Hash to send as PUT params
|
49
|
+
#
|
50
|
+
# Returns: A HTTParty::Response
|
51
|
+
def put(params)
|
52
|
+
self.class.put(url, body: params)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Public: Sends an OPTIONS request the the resource url.
|
56
|
+
#
|
57
|
+
# Returns: A HTTParty::Response
|
58
|
+
def options
|
59
|
+
self.class.options(url)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Public: Sends a HEAD request the the resource url.
|
63
|
+
#
|
64
|
+
# Returns: A HTTParty::Response
|
65
|
+
def head
|
66
|
+
self.class.head(url)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Public: Sends a DELETE request the the resource url.
|
70
|
+
#
|
71
|
+
# Returns: A HTTParty::Response
|
72
|
+
def delete
|
73
|
+
self.class.delete(url)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
# Internal: Sets the authenitcation method for HTTParty.
|
78
|
+
#
|
79
|
+
# options - An options Hash to set the authentication options.
|
80
|
+
# :type - A String or Symbol to set the authentication type.
|
81
|
+
# Can be either :digest or :basic.
|
82
|
+
# :credentials - An Array of Strings with the user and password.
|
83
|
+
#
|
84
|
+
# Returns nothing.
|
85
|
+
def authenticate(options)
|
86
|
+
auth_method = options[:type].to_s + '_auth'
|
87
|
+
self.class.send(auth_method, *options[:credentials])
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'hyperclient/response'
|
2
|
+
require 'hyperclient/http'
|
3
|
+
|
4
|
+
module Hyperclient
|
5
|
+
# Public: Represents a resource from your API. Its responsability is to
|
6
|
+
# perform HTTP requests against itself and ease the way you access the
|
7
|
+
# resource's attributes, links and embedded resources.
|
8
|
+
class Resource
|
9
|
+
extend Forwardable
|
10
|
+
# Public: Delegate attributes and resources to the response.
|
11
|
+
def_delegators :response, :attributes, :resources, :links
|
12
|
+
|
13
|
+
# Public: Delegate all HTTP methods (get, post, put, delete, options and
|
14
|
+
# head) to Hyperclient::HTTP.
|
15
|
+
def_delegators :@http, :get, :post, :put, :delete, :options, :head
|
16
|
+
|
17
|
+
# Public: A String representing the Resource name.
|
18
|
+
attr_reader :name
|
19
|
+
|
20
|
+
# Public: Initializes a Resource.
|
21
|
+
#
|
22
|
+
# url - A String with the url of the resource. Can be either absolute or
|
23
|
+
# relative.
|
24
|
+
#
|
25
|
+
# options - An options Hash to initialize different values:
|
26
|
+
# :name - The String name of the resource.
|
27
|
+
# :response - An optional Hash representation of the resource's
|
28
|
+
# HTTP response.
|
29
|
+
# :http - An optional Hash to pass to the HTTP class.
|
30
|
+
def initialize(url, options = {})
|
31
|
+
@url = url
|
32
|
+
@name = options[:name]
|
33
|
+
@http = HTTP.new(self, options[:http])
|
34
|
+
initialize_response(options[:response])
|
35
|
+
end
|
36
|
+
|
37
|
+
# Public: Sets the entry point for all the resources in your API client.
|
38
|
+
#
|
39
|
+
# url - A String with the URL of your API entry point.
|
40
|
+
#
|
41
|
+
# Returns nothing.
|
42
|
+
def self.entry_point=(url)
|
43
|
+
@@entry_point = URI(url)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Public: Returns A String representing the resource url.
|
47
|
+
def url
|
48
|
+
begin
|
49
|
+
@@entry_point.merge(@url).to_s
|
50
|
+
rescue URI::InvalidURIError
|
51
|
+
@url
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Public: Gets a fresh response from the resource representation.
|
56
|
+
#
|
57
|
+
# Returns itself (this way you can chain method calls).
|
58
|
+
def reload
|
59
|
+
initialize_response(get)
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
# Internal: Initializes a Response
|
65
|
+
#
|
66
|
+
# raw_response - A Hash representing the HTTP response for the resource.
|
67
|
+
#
|
68
|
+
# Return nothing.
|
69
|
+
def initialize_response(raw_response)
|
70
|
+
if raw_response && raw_response.is_a?(Hash) && !raw_response.empty?
|
71
|
+
@response = Response.new(raw_response)
|
72
|
+
@url = @response.url if @response.url
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Internal: Returns the resource response.
|
77
|
+
def response
|
78
|
+
reload unless @response
|
79
|
+
@response
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'hyperclient/discoverer'
|
2
|
+
|
3
|
+
module Hyperclient
|
4
|
+
# Public: This class is responsible for parsing a response from the API
|
5
|
+
# and exposing some methods to access its values.
|
6
|
+
#
|
7
|
+
# It is mainly used by Hyperclient::Resource.
|
8
|
+
class Response
|
9
|
+
# Public: Initializes a Response.
|
10
|
+
#
|
11
|
+
# response - A Hash representing the response from the API.
|
12
|
+
def initialize(response)
|
13
|
+
@response = response
|
14
|
+
end
|
15
|
+
|
16
|
+
# Public: Returns a Discoverer for the _links section of the response. It
|
17
|
+
# can be used later to use the resources from this section.
|
18
|
+
def links
|
19
|
+
@links ||= Discoverer.new(@response['_links'])
|
20
|
+
end
|
21
|
+
|
22
|
+
# Public: Returns a Discoverer for the _embedded section of the response.
|
23
|
+
# It can be used later to use the resources from this section.
|
24
|
+
def resources
|
25
|
+
@embedded ||= Discoverer.new(@response['_embedded'])
|
26
|
+
end
|
27
|
+
|
28
|
+
# Public: Returns a Hash with the attributes of the resource.
|
29
|
+
def attributes
|
30
|
+
@attributes ||= @response.dup.delete_if {|key, value| key =~ /^_/}
|
31
|
+
end
|
32
|
+
|
33
|
+
# Public: Returns a String with the resource URL or nil of it does not have
|
34
|
+
# one.
|
35
|
+
def url
|
36
|
+
if @response && @response['_links'] && @response['_links']['self'] &&
|
37
|
+
(url = @response['_links']['self']['href'])
|
38
|
+
return url
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/hyperclient/version.rb
CHANGED
@@ -0,0 +1,34 @@
|
|
1
|
+
{
|
2
|
+
"_links": {
|
3
|
+
"self": { "href": "..." },
|
4
|
+
"filter": { "href": "...{?categories}" } // note: this is a URI template
|
5
|
+
},
|
6
|
+
"categories": ["Microsoft", "Ruby", "Javascript", "Mobile"],
|
7
|
+
"_embedded": {
|
8
|
+
"videos": [{
|
9
|
+
"_links": {
|
10
|
+
"self": { "href": "..." },
|
11
|
+
"episodes": { "href": "..." }
|
12
|
+
},
|
13
|
+
"title": "Real World ASP.NET MVC3",
|
14
|
+
"description": "In this advanced, somewhat-opinionated production you'll get your very own startup off the ground using ASP.NET MVC 3...",
|
15
|
+
"permitted": true
|
16
|
+
},{
|
17
|
+
"_links": {
|
18
|
+
"self": { "href": "..." },
|
19
|
+
"episodes": { "href": "..." }
|
20
|
+
},
|
21
|
+
"title": "Example Video 2",
|
22
|
+
"description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit",
|
23
|
+
"permitted": false
|
24
|
+
},{
|
25
|
+
"_links": {
|
26
|
+
"self": { "href": "..." },
|
27
|
+
"episodes": { "href": "..." }
|
28
|
+
},
|
29
|
+
"title": "Example Video 3",
|
30
|
+
"description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit",
|
31
|
+
"permitted": false
|
32
|
+
}]
|
33
|
+
}
|
34
|
+
}
|
@@ -0,0 +1,65 @@
|
|
1
|
+
{
|
2
|
+
"_links": {
|
3
|
+
"self": {
|
4
|
+
"href": "/productions/1"
|
5
|
+
},
|
6
|
+
"filter": {
|
7
|
+
"href": "/productions/1?categories="
|
8
|
+
}
|
9
|
+
},
|
10
|
+
"title": "Real World ASP.NET MVC3",
|
11
|
+
"description": "In this advanced, somewhat-opinionated production you'll get your very own startup off the ground using ASP.NET MVC 3...",
|
12
|
+
"permitted": true,
|
13
|
+
"_embedded": {
|
14
|
+
"author": {
|
15
|
+
"_links": {
|
16
|
+
"self": {
|
17
|
+
"href": "/authors/1"
|
18
|
+
}
|
19
|
+
},
|
20
|
+
"name": "Rob Conery"
|
21
|
+
},
|
22
|
+
"episodes": [
|
23
|
+
{
|
24
|
+
"_links": {
|
25
|
+
"self": {
|
26
|
+
"href": "/episodes/1"
|
27
|
+
},
|
28
|
+
"media": [
|
29
|
+
{
|
30
|
+
"type": "video/webm; codecs='vp8.0, vorbis'",
|
31
|
+
"href": "/media/1"
|
32
|
+
},
|
33
|
+
{
|
34
|
+
"type": "video/ogg; codecs='theora, vorbis'",
|
35
|
+
"href": "/media/2"
|
36
|
+
}
|
37
|
+
]
|
38
|
+
},
|
39
|
+
"title": "Foundations",
|
40
|
+
"description": "In this episode we talk about what it is we're doing: building our startup and getting ourselves off the ground. We take..",
|
41
|
+
"released": 1306972800
|
42
|
+
},
|
43
|
+
{
|
44
|
+
"_links": {
|
45
|
+
"self": {
|
46
|
+
"href": "/episodes/2"
|
47
|
+
},
|
48
|
+
"media": [
|
49
|
+
{
|
50
|
+
"type": "video/webm; codecs='vp8.0, vorbis'",
|
51
|
+
"href": "/media/3"
|
52
|
+
},
|
53
|
+
{
|
54
|
+
"type": "video/ogg; codecs='theora, vorbis'",
|
55
|
+
"href": "/media/4"
|
56
|
+
}
|
57
|
+
]
|
58
|
+
},
|
59
|
+
"title": "Membership",
|
60
|
+
"description": "In this episode Rob hooks up testing in an effort to deal with ASP.NET Membership. The team has decided..",
|
61
|
+
"released": 1306972800
|
62
|
+
}
|
63
|
+
]
|
64
|
+
}
|
65
|
+
}
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
require 'hyperclient/response'
|
3
|
+
|
4
|
+
module Hyperclient
|
5
|
+
describe Discoverer do
|
6
|
+
before do
|
7
|
+
Resource.entry_point = 'http://api.myexample.org/'
|
8
|
+
end
|
9
|
+
|
10
|
+
let (:response) do
|
11
|
+
JSON.parse(File.read('test/fixtures/element.json'))
|
12
|
+
end
|
13
|
+
|
14
|
+
describe 'each' do
|
15
|
+
it 'iterates between resources' do
|
16
|
+
discoverer = Discoverer.new(response['_links'])
|
17
|
+
|
18
|
+
discoverer.each do |resource|
|
19
|
+
resource.must_be_kind_of Resource
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '[]' do
|
25
|
+
it 'fetches a resource' do
|
26
|
+
discoverer = Discoverer.new(response['_links'])
|
27
|
+
|
28
|
+
discoverer['filter'].must_be_kind_of Resource
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'resources' do
|
33
|
+
it 'does not include self as a resource' do
|
34
|
+
discoverer = Discoverer.new(response['_links'])
|
35
|
+
|
36
|
+
lambda { discoverer.self }.must_raise NoMethodError
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'builds single resources' do
|
40
|
+
discoverer = Discoverer.new(response['_links'])
|
41
|
+
|
42
|
+
discoverer.filter.must_be_kind_of Resource
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'builds collection resources' do
|
46
|
+
discoverer = Discoverer.new(response['_embedded'])
|
47
|
+
|
48
|
+
discoverer.episodes.must_be_kind_of Array
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'also builds elements in collection resources' do
|
52
|
+
discoverer = Discoverer.new(response['_embedded'])
|
53
|
+
|
54
|
+
discoverer.episodes.first.must_be_kind_of Resource
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'initializes resources with its URL' do
|
58
|
+
discoverer = Discoverer.new(response['_links'])
|
59
|
+
|
60
|
+
discoverer.filter.url.wont_be_empty
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'initializes resources with the response' do
|
64
|
+
discoverer = Discoverer.new(response['_embedded'])
|
65
|
+
|
66
|
+
discoverer.author.attributes.wont_be_empty
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'initializes resources with its name' do
|
70
|
+
discoverer = Discoverer.new(response['_links'])
|
71
|
+
|
72
|
+
discoverer.filter.name.wont_be_empty
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
require 'hyperclient/http'
|
3
|
+
|
4
|
+
module Hyperclient
|
5
|
+
describe HTTP do
|
6
|
+
let (:resource) do
|
7
|
+
resource = MiniTest::Mock.new
|
8
|
+
resource.expect(:url, 'http://api.example.org/productions/1')
|
9
|
+
end
|
10
|
+
|
11
|
+
let (:http) do
|
12
|
+
HTTP.instance_variable_set("@default_options", {})
|
13
|
+
HTTP.new(resource)
|
14
|
+
end
|
15
|
+
|
16
|
+
describe 'authentication' do
|
17
|
+
it 'sets the authentication options' do
|
18
|
+
stub_request(:get, 'user:pass@api.example.org/productions/1').
|
19
|
+
to_return(body: 'This is the resource')
|
20
|
+
|
21
|
+
http = HTTP.new(resource, {auth: {type: :basic, credentials: ['user','pass']}})
|
22
|
+
http.get.must_equal 'This is the resource'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe 'get' do
|
27
|
+
it 'sends a GET request and returns the response body' do
|
28
|
+
stub_request(:get, 'api.example.org/productions/1').
|
29
|
+
to_return(body: 'This is the resource')
|
30
|
+
|
31
|
+
http.get.must_equal 'This is the resource'
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'returns the parsed response' do
|
35
|
+
stub_request(:get, 'api.example.org/productions/1').
|
36
|
+
to_return(body: '{"some_json": 12345 }', headers: {content_type: 'application/json'})
|
37
|
+
|
38
|
+
http.get.must_equal({'some_json' => 12345})
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe 'post' do
|
43
|
+
it 'sends a POST request' do
|
44
|
+
stub_request(:post, 'api.example.org/productions/1').
|
45
|
+
to_return(body: 'Posting like a big boy huh?', status: 201)
|
46
|
+
|
47
|
+
response = http.post({data: 'foo'})
|
48
|
+
response.code.must_equal 201
|
49
|
+
assert_requested :post, 'http://api.example.org/productions/1',
|
50
|
+
body: {data: 'foo'}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe 'put' do
|
55
|
+
it 'sends a PUT request' do
|
56
|
+
stub_request(:put, 'api.example.org/productions/1').
|
57
|
+
to_return(body: 'No changes were made', status: 204)
|
58
|
+
|
59
|
+
response = http.put({attribute: 'changed'})
|
60
|
+
response.code.must_equal 204
|
61
|
+
assert_requested :put, 'http://api.example.org/productions/1',
|
62
|
+
body: {attribute: 'changed'}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe 'options' do
|
67
|
+
it 'sends a OPTIONS request' do
|
68
|
+
stub_request(:options, 'api.example.org/productions/1').
|
69
|
+
to_return(status: 200, headers: {allow: 'GET, POST'})
|
70
|
+
|
71
|
+
response = http.options
|
72
|
+
response.headers.must_include 'allow'
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe 'head' do
|
77
|
+
it 'sends a HEAD request' do
|
78
|
+
stub_request(:head, 'api.example.org/productions/1').
|
79
|
+
to_return(status: 200, headers: {content_type: 'application/json'})
|
80
|
+
|
81
|
+
response = http.head
|
82
|
+
response.headers.must_include 'content-type'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe 'delete' do
|
87
|
+
it 'sends a DELETE request' do
|
88
|
+
stub_request(:delete, 'api.example.org/productions/1').
|
89
|
+
to_return(body: 'Resource deleted', status: 200)
|
90
|
+
|
91
|
+
response = http.delete
|
92
|
+
response.code.must_equal 200
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|