hyperclient 0.0.8 → 0.1.0

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.
@@ -1,5 +1,5 @@
1
1
  require 'httparty'
2
- require 'forwardable'
2
+ require 'json'
3
3
 
4
4
  # Public: A parser for HTTParty that understand the mime application/hal+json.
5
5
  class JSONHalParser < HTTParty::Parser
@@ -10,30 +10,45 @@ module Hyperclient
10
10
  # Internal: This class wrapps HTTParty and performs the HTTP requests for a
11
11
  # resource.
12
12
  class HTTP
13
- extend Forwardable
14
13
  include HTTParty
15
-
16
14
  parser JSONHalParser
17
15
 
18
- # Private: Delegate the url to the resource.
19
- def_delegators :@resource, :url
20
-
21
- # Public: Initializes a HTTP agent.
16
+ # Public: Initializes the HTTP agent.
22
17
  #
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
- headers(options[:headers]) if options && options.include?(:headers)
29
- enable_debug(options[:debug]) if options && options.include?(:debug)
18
+ # url - A String to send the HTTP requests.
19
+ # config - A Hash with the configuration of the HTTP connection.
20
+ # :headers - The Hash with the headers of the connection.
21
+ # :auth - The Hash with the authentication options:
22
+ # :type - A String or Symbol to set the authentication type.
23
+ # Allowed values are :digest or :basic.
24
+ # :user - A String with the user.
25
+ # :password - A String with the user.
26
+ # :debug - The flag (true/false) to debug the HTTP connections.
27
+ #
28
+ def initialize(url, config)
29
+ @url = url
30
+ @config = config
31
+ @base_uri = config.fetch(:base_uri)
32
+
33
+ authenticate!
34
+ toggle_debug! if @config[:debug]
35
+
36
+ self.class.headers(@config[:headers]) if @config.include?(:headers)
37
+ end
38
+
39
+ def url
40
+ begin
41
+ URI.parse(@base_uri).merge(@url).to_s
42
+ rescue URI::InvalidURIError
43
+ @url
44
+ end
30
45
  end
31
46
 
32
47
  # Public: Sends a GET request the the resource url.
33
48
  #
34
49
  # Returns: The parsed response.
35
50
  def get
36
- self.class.get(url).parsed_response
51
+ JSON.parse(self.class.get(url).response.body)
37
52
  end
38
53
 
39
54
  # Public: Sends a POST request the the resource url.
@@ -79,34 +94,20 @@ module Hyperclient
79
94
  # Internal: Sets the authentication method for HTTParty.
80
95
  #
81
96
  # options - An options Hash to set the authentication options.
82
- # :type - A String or Symbol to set the authentication type.
83
- # Can be either :digest or :basic.
84
- # :credentials - An Array of Strings with the user and password.
85
97
  #
86
98
  # Returns nothing.
87
- def authenticate(options)
88
- auth_method = options[:type].to_s + '_auth'
89
- self.class.send(auth_method, *options[:credentials])
90
- end
91
-
92
- # Internal: Adds default headers for all the requests.
93
- #
94
- # headers - A Hash with the header.
95
- #
96
- # Example:
97
- # headers({'accept-encoding' => 'deflate, gzip'})
98
- #
99
- # Returns nothing.
100
- def headers(headers)
101
- self.class.send(:headers, headers)
99
+ def authenticate!
100
+ if (options = @config[:auth])
101
+ auth_method = options.delete(:type).to_s + '_auth'
102
+ self.class.send(auth_method, options[:user], options[:password])
103
+ end
102
104
  end
103
105
 
104
106
  # Internal: Enables HTTP debugging.
105
107
  #
106
- # stream - An object to stream the HTTP out to or just a truthy value. If
107
- # it's truthy it will output to $stderr.
108
- def enable_debug(stream)
109
- return unless stream
108
+ # stream - An object to stream the HTTP out to or just a truthy value.
109
+ def toggle_debug!
110
+ stream = @config[:debug]
110
111
 
111
112
  if stream.respond_to?(:<<)
112
113
  self.class.debug_output(stream)
@@ -0,0 +1,93 @@
1
+ require 'hyperclient/http'
2
+ require 'hyperclient/resource'
3
+ require 'uri_template'
4
+
5
+ module Hyperclient
6
+ # Internal: The Link is used to let a Resource interact with the API.
7
+ #
8
+ class Link
9
+ extend Forwardable
10
+ # Public: Delegate all HTTP methods (get, post, put, delete, options and
11
+ # head) to the http connection.
12
+ def_delegators :http, :get, :post, :put, :delete, :options, :head
13
+
14
+ # Public: Initializes a new Link.
15
+ #
16
+ # link - The String with the URI of the link.
17
+ # entry_point - The EntryPoint object to inject the cofnigutation.
18
+ # uri_variables - The optional Hash with the variables to expand the link
19
+ # if it is templated.
20
+ def initialize(link, entry_point, uri_variables = nil)
21
+ @link = link
22
+ @entry_point = entry_point
23
+ @uri_variables = uri_variables
24
+ end
25
+
26
+ # Public: Returns the Resource which the Link is pointing to.
27
+ def resource
28
+ @resource ||=Resource.new(http.get, @entry_point)
29
+ end
30
+
31
+ # Public: Indicates if the link is an URITemplate or a regular URI.
32
+ #
33
+ # Returns true if it is templated.
34
+ # Returns false if it nos templated.
35
+ def templated?
36
+ !!@link['templated']
37
+ end
38
+
39
+ # Public: Expands the Link when is templated with the given variables.
40
+ #
41
+ # uri_variables - The Hash with the variables to expand the URITemplate.
42
+ #
43
+ # Returns a new Link with the expanded variables.
44
+ def expand(uri_variables)
45
+ self.class.new(@link, @entry_point, uri_variables)
46
+ end
47
+
48
+ # Public: Returns the url of the Link.
49
+ #
50
+ # Raises MissingURITemplateVariables if the Link is templated but there are
51
+ # no uri variables to expand it.
52
+ def url
53
+ return @link['href'] unless templated?
54
+ raise MissingURITemplateVariablesException if @uri_variables == nil
55
+
56
+ @url ||= URITemplate.new(@link['href']).expand(@uri_variables)
57
+ end
58
+
59
+ private
60
+ # Internal: Returns the HTTP client used to interact with the API.
61
+ def http
62
+ @http ||= HTTP.new(url, @entry_point.config)
63
+ end
64
+
65
+ # Internal: Delegate the method to the API if it exists.
66
+ #
67
+ # This allows `api.links.posts.embedded` instead of
68
+ # `api.links.posts.resource.embedded`
69
+ def method_missing(method, *args, &block)
70
+ if resource.respond_to?(method)
71
+ resource.send(method, *args, &block)
72
+ else
73
+ super
74
+ end
75
+ end
76
+
77
+ # Internal: Accessory method to allow the link respond to the
78
+ # methods that will hit method_missing.
79
+ def respond_to_missing?(method, include_private = false)
80
+ resource.respond_to?(method.to_s)
81
+ end
82
+ end
83
+
84
+ # Public: Exception that is raised when building a templated Link without uri
85
+ # variables.
86
+ class MissingURITemplateVariablesException < StandardError
87
+
88
+ # Public: Returns a String with the exception message.
89
+ def message
90
+ "The URL to this links is templated, but no variables where given."
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,24 @@
1
+ require 'hyperclient/collection'
2
+ require 'hyperclient/link'
3
+
4
+ module Hyperclient
5
+ # Public: A wrapper class to easily acces the links in a Resource.
6
+ #
7
+ # Examples
8
+ #
9
+ # resource.links['author']
10
+ # resource.links.author
11
+ #
12
+ class LinkCollection < Collection
13
+ # Public: Initializes a LinkCollection.
14
+ #
15
+ # collection - The Hash with the links.
16
+ # entry_point - The EntryPoint object to inject the cofnigutation.
17
+ #
18
+ def initialize(collection, entry_point)
19
+ @collection = (collection || {}).inject({}) do |hash, (name, link)|
20
+ hash.update(name => Link.new(link, entry_point))
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,81 +1,42 @@
1
- require 'hyperclient/representation'
2
- require 'hyperclient/http'
1
+ require 'hyperclient/attributes'
2
+ require 'hyperclient/link_collection'
3
+ require 'hyperclient/resource_collection'
3
4
 
4
5
  module Hyperclient
5
6
  # 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.
7
+ # ease the way you access its attributes, links and embedded resources.
8
8
  class Resource
9
9
  extend Forwardable
10
- # Public: Delegate attributes and resources to the representation.
11
- def_delegators :representation, :attributes, :embedded, :links
12
10
 
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
11
+ # Public: Returns the attributes of the Resource as Attributes.
12
+ attr_reader :attributes
16
13
 
17
- # Public: A String representing the Resource name.
18
- attr_reader :name
14
+ # Public: Returns the links of the Resource as a LinkCollection.
15
+ attr_reader :links
19
16
 
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
- # :representation - An optional Hash representation of the resource's
28
- # HTTP representation.
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_representation(options[:representation])
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
17
+ # Public: Returns the embedded resource of the Resource as a
18
+ # ResourceCollection.
19
+ attr_reader :embedded
45
20
 
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
21
+ # Public: Delegate all HTTP methods (get, post, put, delete, options and
22
+ # head) to its Link.
23
+ def_delegators :self_link, :get, :post, :put, :delete, :options, :head
54
24
 
55
- # Public: Gets a fresh representation from the resource representation.
56
- #
57
- # Returns itself (this way you can chain method calls).
58
- def reload
59
- initialize_representation(get)
60
- self
25
+ # Public: Initializes a Resource.
26
+ # representation - The hash with the HAL representation of the Resource.
27
+ # entry_point - The EntryPoint object to inject the cofnigutation.
28
+ def initialize(representation, entry_point)
29
+ @links = LinkCollection.new(representation['_links'], entry_point)
30
+ @embedded = ResourceCollection.new(representation['_embedded'], entry_point)
31
+ @attributes = Attributes.new(representation)
32
+ @entry_point = entry_point
61
33
  end
62
34
 
63
35
  private
64
- # Internal: Initializes a Representation
65
- #
66
- # raw_representation - A Hash representing the HTTP representation for the resource.
67
- #
68
- # Return nothing.
69
- def initialize_representation(raw_representation)
70
- if raw_representation && !raw_representation.empty?
71
- @representation = Representation.new(raw_representation)
72
- end
73
- end
74
-
75
- # Internal: Returns the resource representation.
76
- def representation
77
- reload unless @representation
78
- @representation
36
+ # Internal: Returns the self Link of the Resource. Used to handle the HTTP
37
+ # connections.
38
+ def self_link
39
+ @links['self']
79
40
  end
80
41
  end
81
42
  end
@@ -0,0 +1,33 @@
1
+ require 'hyperclient/collection'
2
+ require 'hyperclient/resource'
3
+
4
+ module Hyperclient
5
+ # Public: A wrapper class to easily acces the embedded resources in a
6
+ # Resource.
7
+ #
8
+ # Examples
9
+ #
10
+ # resource.embedded['comments']
11
+ # resource.embedded.comments
12
+ #
13
+ class ResourceCollection < Collection
14
+ # Public: Initializes a ResourceCollection.
15
+ #
16
+ # collection - The Hash with the embedded resources.
17
+ # entry_point - The EntryPoint object to inject the cofnigutation.
18
+ #
19
+ def initialize(collection, entry_point)
20
+ @entry_point = entry_point
21
+ @collection = (collection || {}).inject({}) do |hash, (name, resource)|
22
+ hash.update(name => build_resource(resource))
23
+ end
24
+ end
25
+
26
+ private
27
+ def build_resource(representation)
28
+ return representation.map(&method(:build_resource)) if representation.is_a?(Array)
29
+
30
+ Resource.new(representation, @entry_point)
31
+ end
32
+ end
33
+ end
@@ -1,3 +1,3 @@
1
1
  module Hyperclient
2
- VERSION = "0.0.8"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -4,7 +4,8 @@
4
4
  "href": "/productions/1"
5
5
  },
6
6
  "filter": {
7
- "href": "/productions/1?categories="
7
+ "href": "/productions/1?categories={filter}",
8
+ "templated": true
8
9
  }
9
10
  },
10
11
  "title": "Real World ASP.NET MVC3",
@@ -25,16 +26,10 @@
25
26
  "self": {
26
27
  "href": "/episodes/1"
27
28
  },
28
- "media": [
29
- {
29
+ "media": {
30
30
  "type": "video/webm; codecs='vp8.0, vorbis'",
31
31
  "href": "/media/1"
32
- },
33
- {
34
- "type": "video/ogg; codecs='theora, vorbis'",
35
- "href": "/media/2"
36
32
  }
37
- ]
38
33
  },
39
34
  "title": "Foundations",
40
35
  "description": "In this episode we talk about what it is we're doing: building our startup and getting ourselves off the ground. We take..",
@@ -45,16 +40,10 @@
45
40
  "self": {
46
41
  "href": "/episodes/2"
47
42
  },
48
- "media": [
49
- {
50
- "type": "video/webm; codecs='vp8.0, vorbis'",
51
- "href": "/media/3"
52
- },
53
- {
43
+ "media": {
54
44
  "type": "video/ogg; codecs='theora, vorbis'",
55
45
  "href": "/media/4"
56
46
  }
57
- ]
58
47
  },
59
48
  "title": "Membership",
60
49
  "description": "In this episode Rob hooks up testing in an effort to deal with ASP.NET Membership. The team has decided..",
@@ -0,0 +1,26 @@
1
+ require_relative '../test_helper'
2
+ require 'hyperclient/attributes'
3
+
4
+ module Hyperclient
5
+ describe Attributes do
6
+ let(:representation) do
7
+ JSON.parse( File.read('test/fixtures/element.json'))
8
+ end
9
+
10
+ let(:attributes) do
11
+ Attributes.new(representation)
12
+ end
13
+
14
+ it 'does not set _links as an attribute' do
15
+ attributes.wont_respond_to :_links
16
+ end
17
+
18
+ it 'does not set _embedded as an attribute' do
19
+ attributes.wont_respond_to :_embedded
20
+ end
21
+
22
+ it 'is a collection' do
23
+ Attributes.ancestors.must_include Collection
24
+ end
25
+ end
26
+ end