hyperclient 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Hyperclient
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -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,6 @@
1
+ {
2
+ "_links": {
3
+ "self": { "href": "..." },
4
+ "productions": { "href": "..." }
5
+ }
6
+ }
@@ -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