hyperclient 0.0.1 → 0.0.2
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/.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
|