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.
@@ -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