restfully 0.2.1
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/.document +5 -0
- data/.gitignore +5 -0
- data/LICENSE +20 -0
- data/README.rdoc +90 -0
- data/Rakefile +75 -0
- data/TODO.rdoc +3 -0
- data/VERSION +1 -0
- data/bin/restfully +80 -0
- data/examples/grid5000.rb +28 -0
- data/lib/restfully.rb +19 -0
- data/lib/restfully/collection.rb +63 -0
- data/lib/restfully/error.rb +4 -0
- data/lib/restfully/extensions.rb +41 -0
- data/lib/restfully/http.rb +9 -0
- data/lib/restfully/http/adapters/abstract_adapter.rb +30 -0
- data/lib/restfully/http/adapters/patron_adapter.rb +16 -0
- data/lib/restfully/http/adapters/rest_client_adapter.rb +31 -0
- data/lib/restfully/http/error.rb +20 -0
- data/lib/restfully/http/headers.rb +20 -0
- data/lib/restfully/http/request.rb +24 -0
- data/lib/restfully/http/response.rb +19 -0
- data/lib/restfully/link.rb +35 -0
- data/lib/restfully/parsing.rb +31 -0
- data/lib/restfully/resource.rb +117 -0
- data/lib/restfully/session.rb +61 -0
- data/lib/restfully/special_array.rb +5 -0
- data/lib/restfully/special_hash.rb +5 -0
- data/restfully.gemspec +99 -0
- data/spec/collection_spec.rb +93 -0
- data/spec/fixtures/grid5000-sites.json +489 -0
- data/spec/http/error_spec.rb +18 -0
- data/spec/http/headers_spec.rb +17 -0
- data/spec/http/request_spec.rb +45 -0
- data/spec/http/response_spec.rb +15 -0
- data/spec/http/rest_client_adapter_spec.rb +33 -0
- data/spec/link_spec.rb +58 -0
- data/spec/parsing_spec.rb +25 -0
- data/spec/resource_spec.rb +198 -0
- data/spec/restfully_spec.rb +13 -0
- data/spec/session_spec.rb +105 -0
- data/spec/spec_helper.rb +13 -0
- metadata +117 -0
| @@ -0,0 +1,30 @@ | |
| 1 | 
            +
            module Restfully
         | 
| 2 | 
            +
              module HTTP
         | 
| 3 | 
            +
                module Adapters
         | 
| 4 | 
            +
                  
         | 
| 5 | 
            +
                  class AbstractAdapter
         | 
| 6 | 
            +
                    attr_reader :logger, :options
         | 
| 7 | 
            +
                    def initialize(base_uri, options = {})
         | 
| 8 | 
            +
                      @options = options.symbolize_keys
         | 
| 9 | 
            +
                      @logger = @options.delete(:logger) || Restfully::NullLogger.new
         | 
| 10 | 
            +
                      @base_uri = base_uri
         | 
| 11 | 
            +
                      logger.debug "base_uri = #{base_uri.inspect}, options = #{options.inspect}."
         | 
| 12 | 
            +
                    end
         | 
| 13 | 
            +
                    
         | 
| 14 | 
            +
                    def get(request)
         | 
| 15 | 
            +
                      raise NotImplementedError, "GET is not supported by your adapter."
         | 
| 16 | 
            +
                    end
         | 
| 17 | 
            +
                    def post(request)
         | 
| 18 | 
            +
                      raise NotImplementedError, "POST is not supported by your adapter."
         | 
| 19 | 
            +
                    end
         | 
| 20 | 
            +
                    def put(request)
         | 
| 21 | 
            +
                      raise NotImplementedError, "PUT is not supported by your adapter."
         | 
| 22 | 
            +
                    end
         | 
| 23 | 
            +
                    def delete(request)
         | 
| 24 | 
            +
                      raise NotImplementedError, "DELETEis not supported by your adapter."
         | 
| 25 | 
            +
                    end
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
                  
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
              end
         | 
| 30 | 
            +
            end
         | 
| @@ -0,0 +1,16 @@ | |
| 1 | 
            +
            require 'restfully/http/adapters/abstract_adapter'
         | 
| 2 | 
            +
            require 'patron'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Restfully
         | 
| 5 | 
            +
              module HTTP
         | 
| 6 | 
            +
                module Adapters
         | 
| 7 | 
            +
                  class PatronAdapter < AbstractAdapter
         | 
| 8 | 
            +
                    
         | 
| 9 | 
            +
                    def initialize(base_url, options = {})
         | 
| 10 | 
            +
                      super(base_url, options)
         | 
| 11 | 
            +
                    end
         | 
| 12 | 
            +
                    
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
            end
         | 
| @@ -0,0 +1,31 @@ | |
| 1 | 
            +
            require 'restfully/http/adapters/abstract_adapter'
         | 
| 2 | 
            +
            require 'restclient'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Restfully
         | 
| 5 | 
            +
              module HTTP
         | 
| 6 | 
            +
                module Adapters
         | 
| 7 | 
            +
                  class RestClientAdapter < AbstractAdapter
         | 
| 8 | 
            +
                    def initialize(base_uri, options = {})
         | 
| 9 | 
            +
                      super(base_uri, options)
         | 
| 10 | 
            +
                      @options[:user] = @options.delete(:username)
         | 
| 11 | 
            +
                    end
         | 
| 12 | 
            +
                    def get(request)
         | 
| 13 | 
            +
                      begin
         | 
| 14 | 
            +
                        resource = RestClient::Resource.new(request.uri.to_s, @options)
         | 
| 15 | 
            +
                        response = resource.get(request.headers)
         | 
| 16 | 
            +
                        headers = response.headers
         | 
| 17 | 
            +
                        body = response.to_s
         | 
| 18 | 
            +
                        headers.delete(:status)
         | 
| 19 | 
            +
                        status = response.code
         | 
| 20 | 
            +
                      rescue RestClient::ExceptionWithResponse => e
         | 
| 21 | 
            +
                        body = e.http_body
         | 
| 22 | 
            +
                        headers = e.response.to_hash
         | 
| 23 | 
            +
                        status = e.http_code
         | 
| 24 | 
            +
                      end
         | 
| 25 | 
            +
                      Response.new(status, headers, body)
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
                
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
            end
         | 
| @@ -0,0 +1,20 @@ | |
| 1 | 
            +
            module Restfully
         | 
| 2 | 
            +
              module HTTP
         | 
| 3 | 
            +
                class Error < Restfully::Error
         | 
| 4 | 
            +
                  attr_reader :response
         | 
| 5 | 
            +
                  def initialize(response)
         | 
| 6 | 
            +
                    @response = response
         | 
| 7 | 
            +
                    if response.body.kind_of?(Hash)
         | 
| 8 | 
            +
                      message = "#{response.status} #{response.body['title']}. #{response.body['message']}"
         | 
| 9 | 
            +
                    else
         | 
| 10 | 
            +
                      message = response.body
         | 
| 11 | 
            +
                    end
         | 
| 12 | 
            +
                    super(message)
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
                class ClientError < Restfully::HTTP::Error; end
         | 
| 16 | 
            +
                class ServerError < Restfully::HTTP::Error; end
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
            end
         | 
| 19 | 
            +
                  
         | 
| 20 | 
            +
                  
         | 
| @@ -0,0 +1,20 @@ | |
| 1 | 
            +
            module Restfully
         | 
| 2 | 
            +
              module HTTP
         | 
| 3 | 
            +
                module Headers
         | 
| 4 | 
            +
                  def sanitize_http_headers(headers = {})
         | 
| 5 | 
            +
                    sanitized_headers = {}
         | 
| 6 | 
            +
                    headers.each do |key, value|
         | 
| 7 | 
            +
                      sanitized_key = key.to_s.downcase.gsub(/[_-]/, ' ').split(' ').map{|word| word.capitalize}.join("-")
         | 
| 8 | 
            +
                      sanitized_value = case value
         | 
| 9 | 
            +
                      when Array 
         | 
| 10 | 
            +
                        value.join(", ")
         | 
| 11 | 
            +
                      else 
         | 
| 12 | 
            +
                        value
         | 
| 13 | 
            +
                      end
         | 
| 14 | 
            +
                      sanitized_headers[sanitized_key] = sanitized_value
         | 
| 15 | 
            +
                    end
         | 
| 16 | 
            +
                    sanitized_headers
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
            end
         | 
| @@ -0,0 +1,24 @@ | |
| 1 | 
            +
            require 'uri'
         | 
| 2 | 
            +
            module Restfully
         | 
| 3 | 
            +
              module HTTP
         | 
| 4 | 
            +
                class Request
         | 
| 5 | 
            +
                  include Headers
         | 
| 6 | 
            +
                  attr_reader :headers, :body, :uri
         | 
| 7 | 
            +
                  attr_accessor :retries
         | 
| 8 | 
            +
                  def initialize(url, options = {})
         | 
| 9 | 
            +
                    options = options.symbolize_keys
         | 
| 10 | 
            +
                    @uri = url.kind_of?(URI) ? url : URI.parse(url)
         | 
| 11 | 
            +
                    @headers = sanitize_http_headers(options.delete(:headers) || {})
         | 
| 12 | 
            +
                    if query = options.delete(:query)
         | 
| 13 | 
            +
                      @uri.query = [@uri.query, query.to_params].compact.join("&")
         | 
| 14 | 
            +
                    end
         | 
| 15 | 
            +
                    @body = body
         | 
| 16 | 
            +
                    @retries = 0
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
                  
         | 
| 19 | 
            +
                  def add_headers(headers = {})
         | 
| 20 | 
            +
                    @headers.merge!(sanitize_http_headers(headers || {}))
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
            end
         | 
| @@ -0,0 +1,19 @@ | |
| 1 | 
            +
            module Restfully
         | 
| 2 | 
            +
              module HTTP
         | 
| 3 | 
            +
                # Container for an HTTP Response. Has <tt>status</tt>, <tt>headers</tt> and <tt>body</tt> properties.
         | 
| 4 | 
            +
                # The body is automatically parsed into a ruby object based on the response's <tt>Content-Type</tt> header.
         | 
| 5 | 
            +
                class Response
         | 
| 6 | 
            +
                  include Headers, Restfully::Parsing
         | 
| 7 | 
            +
                  attr_reader :status, :headers
         | 
| 8 | 
            +
                  def initialize(status, headers, body)
         | 
| 9 | 
            +
                    @status = status.to_i
         | 
| 10 | 
            +
                    @headers = sanitize_http_headers(headers)
         | 
| 11 | 
            +
                    @body = (body.nil? || body.empty?) ? nil : body.to_s
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
                  
         | 
| 14 | 
            +
                  def body
         | 
| 15 | 
            +
                    @unserialized_body ||= unserialize(@body, :content_type => @headers['Content-Type']) unless @body.nil?
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
            end
         | 
| @@ -0,0 +1,35 @@ | |
| 1 | 
            +
            module Restfully
         | 
| 2 | 
            +
              class Link
         | 
| 3 | 
            +
                
         | 
| 4 | 
            +
                VALID_RELATIONSHIPS = %w{member parent collection self alternate}
         | 
| 5 | 
            +
                RELATIONSHIPS_REQUIRING_TITLE = %w{collection member}
         | 
| 6 | 
            +
                
         | 
| 7 | 
            +
                attr_reader :rel, :title, :href, :errors
         | 
| 8 | 
            +
                
         | 
| 9 | 
            +
                def initialize(attributes = {})
         | 
| 10 | 
            +
                  @rel = attributes['rel']
         | 
| 11 | 
            +
                  @title = attributes['title']
         | 
| 12 | 
            +
                  @href = attributes['href']
         | 
| 13 | 
            +
                  @resolvable = attributes['resolvable'] || false
         | 
| 14 | 
            +
                  @resolved = attributes['resolved'] || false
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
                
         | 
| 17 | 
            +
                def resolvable?; @resolvable == true; end
         | 
| 18 | 
            +
                def resolved?; @resolved == true; end
         | 
| 19 | 
            +
                def self?; @rel == 'self'; end
         | 
| 20 | 
            +
                
         | 
| 21 | 
            +
                def valid?
         | 
| 22 | 
            +
                  @errors = []
         | 
| 23 | 
            +
                  if href.nil? || href.empty?
         | 
| 24 | 
            +
                    errors << "href cannot be empty."
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
                  unless VALID_RELATIONSHIPS.include?(rel)
         | 
| 27 | 
            +
                    errors << "#{rel} is not a valid link relationship."
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
                  if (!title || title.empty?) && RELATIONSHIPS_REQUIRING_TITLE.include?(rel)
         | 
| 30 | 
            +
                    errors << "#{rel} #{href} has no title."
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
                  errors.empty?
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
            end
         | 
| @@ -0,0 +1,31 @@ | |
| 1 | 
            +
            require 'json'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Restfully
         | 
| 4 | 
            +
              
         | 
| 5 | 
            +
              module Parsing
         | 
| 6 | 
            +
              
         | 
| 7 | 
            +
                class ParserNotFound < Restfully::Error; end
         | 
| 8 | 
            +
                def unserialize(object, options = {})
         | 
| 9 | 
            +
                  content_type = options[:content_type]
         | 
| 10 | 
            +
                  content_type ||= object.headers['Content-Type'] if object.respond_to?(:headers)
         | 
| 11 | 
            +
                  case content_type
         | 
| 12 | 
            +
                  when /^application\/json/i
         | 
| 13 | 
            +
                    JSON.parse(object)
         | 
| 14 | 
            +
                  else
         | 
| 15 | 
            +
                    raise ParserNotFound.new("Content-Type '#{content_type}' is not supported. Cannot parse the given object.")
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              
         | 
| 19 | 
            +
                def serialize(object, options = {})
         | 
| 20 | 
            +
                  content_type = options[:content_type]
         | 
| 21 | 
            +
                  content_type ||= object.headers['Content-Type'] if object.respond_to?(:headers)
         | 
| 22 | 
            +
                  case content_type
         | 
| 23 | 
            +
                  when /^application\/json/i
         | 
| 24 | 
            +
                    JSON.dump(object)
         | 
| 25 | 
            +
                  else
         | 
| 26 | 
            +
                    raise ParserNotFound, [object, content_type]
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
            end
         | 
| @@ -0,0 +1,117 @@ | |
| 1 | 
            +
            require 'delegate'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Restfully
         | 
| 4 | 
            +
              
         | 
| 5 | 
            +
              # Suppose that the load method has been called on the resource before trying to access its attributes or associations
         | 
| 6 | 
            +
              
         | 
| 7 | 
            +
              class Resource < DelegateClass(Hash)
         | 
| 8 | 
            +
                
         | 
| 9 | 
            +
                undef :type
         | 
| 10 | 
            +
                attr_reader :uri, :session, :state, :raw, :uid, :associations, :type
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def initialize(uri, session, options = {})
         | 
| 13 | 
            +
                  options = options.symbolize_keys
         | 
| 14 | 
            +
                  @uri = uri
         | 
| 15 | 
            +
                  @session = session
         | 
| 16 | 
            +
                  @raw = options[:raw]
         | 
| 17 | 
            +
                  @state = :unloaded
         | 
| 18 | 
            +
                  @attributes = {}
         | 
| 19 | 
            +
                  super(@attributes)
         | 
| 20 | 
            +
                  @associations = {}
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
                
         | 
| 23 | 
            +
                def loaded?;    @state == :loaded;    end
         | 
| 24 | 
            +
                
         | 
| 25 | 
            +
                def method_missing(method, *args)
         | 
| 26 | 
            +
                  if association = @associations[method.to_s]
         | 
| 27 | 
            +
                    session.logger.debug "Loading association #{method}, args=#{args.inspect}"
         | 
| 28 | 
            +
                    association.load(*args)
         | 
| 29 | 
            +
                  else
         | 
| 30 | 
            +
                    super(method, *args)
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
              
         | 
| 34 | 
            +
                def load(options = {})
         | 
| 35 | 
            +
                  options = options.symbolize_keys
         | 
| 36 | 
            +
                  force_reload = !!options.delete(:reload) || options.has_key?(:query)
         | 
| 37 | 
            +
                  if loaded? && !force_reload
         | 
| 38 | 
            +
                    self
         | 
| 39 | 
            +
                  else  
         | 
| 40 | 
            +
                    @associations.clear
         | 
| 41 | 
            +
                    @attributes.clear
         | 
| 42 | 
            +
                    if raw.nil? || force_reload
         | 
| 43 | 
            +
                      response = session.get(uri, options) 
         | 
| 44 | 
            +
                      @raw = response.body
         | 
| 45 | 
            +
                    end
         | 
| 46 | 
            +
                    (raw['links'] || []).each{|link| define_link(Link.new(link))}
         | 
| 47 | 
            +
                    raw.each do |key, value|
         | 
| 48 | 
            +
                      case key
         | 
| 49 | 
            +
                      when 'links'  then  next
         | 
| 50 | 
            +
                      when 'uid'    then  @uid = value
         | 
| 51 | 
            +
                      when 'type'   then  @type = value
         | 
| 52 | 
            +
                      else
         | 
| 53 | 
            +
                        case value
         | 
| 54 | 
            +
                        when Hash
         | 
| 55 | 
            +
                          @attributes.store(key, SpecialHash.new.replace(value)) unless @associations.has_key?(key)
         | 
| 56 | 
            +
                        when Array
         | 
| 57 | 
            +
                          @attributes.store(key, SpecialArray.new(value))
         | 
| 58 | 
            +
                        else
         | 
| 59 | 
            +
                          @attributes.store(key, value)
         | 
| 60 | 
            +
                        end
         | 
| 61 | 
            +
                      end
         | 
| 62 | 
            +
                    end
         | 
| 63 | 
            +
                    @state = :loaded
         | 
| 64 | 
            +
                    self
         | 
| 65 | 
            +
                  end
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
                
         | 
| 68 | 
            +
                def respond_to?(method, *args)
         | 
| 69 | 
            +
                  @associations.has_key?(method.to_s) || super(method, *args)
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
                
         | 
| 72 | 
            +
                def inspect(options = {:space => "\t"})
         | 
| 73 | 
            +
                  output = "#<#{self.class}:0x#{self.object_id.to_s(16)}"
         | 
| 74 | 
            +
                  if loaded?
         | 
| 75 | 
            +
                    output += "\n#{options[:space]}------------ META ------------"
         | 
| 76 | 
            +
                    output += "\n#{options[:space]}@uri: #{uri.inspect}"
         | 
| 77 | 
            +
                    output += "\n#{options[:space]}@uid: #{uid.inspect}"
         | 
| 78 | 
            +
                    output += "\n#{options[:space]}@type: #{type.inspect}"
         | 
| 79 | 
            +
                    @associations.each do |title, assoc|
         | 
| 80 | 
            +
                      output += "\n#{options[:space]}@#{title}: #{assoc.class.name}"
         | 
| 81 | 
            +
                    end
         | 
| 82 | 
            +
                    unless @attributes.empty?
         | 
| 83 | 
            +
                      output += "\n#{options[:space]}------------ PROPERTIES ------------"
         | 
| 84 | 
            +
                      @attributes.each do |key, value|
         | 
| 85 | 
            +
                        output += "\n#{options[:space]}#{key.inspect} => #{value.inspect}"
         | 
| 86 | 
            +
                      end
         | 
| 87 | 
            +
                    end
         | 
| 88 | 
            +
                  end
         | 
| 89 | 
            +
                  output += ">"
         | 
| 90 | 
            +
                end    
         | 
| 91 | 
            +
                
         | 
| 92 | 
            +
                protected
         | 
| 93 | 
            +
                def define_link(link)
         | 
| 94 | 
            +
                  if link.valid?
         | 
| 95 | 
            +
                    case link.rel
         | 
| 96 | 
            +
                    when 'parent'
         | 
| 97 | 
            +
                      @associations['parent'] = Resource.new(link.href, session)
         | 
| 98 | 
            +
                    when 'collection'
         | 
| 99 | 
            +
                      raw_included = link.resolved? ? raw[link.title] : nil
         | 
| 100 | 
            +
                      @associations[link.title] = Collection.new(link.href, session, 
         | 
| 101 | 
            +
                        :raw => raw_included,
         | 
| 102 | 
            +
                        :title => link.title)
         | 
| 103 | 
            +
                    when 'member'
         | 
| 104 | 
            +
                      raw_included = link.resolved? ? raw[link.title] : nil
         | 
| 105 | 
            +
                      @associations[link.title] = Resource.new(link.href, session, 
         | 
| 106 | 
            +
                        :title => link.title,
         | 
| 107 | 
            +
                        :raw => raw_included)
         | 
| 108 | 
            +
                    when 'self'
         | 
| 109 | 
            +
                      # we do nothing
         | 
| 110 | 
            +
                    end
         | 
| 111 | 
            +
                  else 
         | 
| 112 | 
            +
                    session.logger.warn link.errors.join("\n")
         | 
| 113 | 
            +
                  end
         | 
| 114 | 
            +
                end
         | 
| 115 | 
            +
                
         | 
| 116 | 
            +
              end # class Resource
         | 
| 117 | 
            +
            end # module Restfully
         | 
| @@ -0,0 +1,61 @@ | |
| 1 | 
            +
            require 'uri'
         | 
| 2 | 
            +
            require 'logger'
         | 
| 3 | 
            +
            require 'restfully/parsing'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Restfully
         | 
| 6 | 
            +
              class NullLogger
         | 
| 7 | 
            +
                def method_missing(method, *args)
         | 
| 8 | 
            +
                  nil
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
              class Session
         | 
| 12 | 
            +
                include Parsing
         | 
| 13 | 
            +
                attr_reader :base_uri, :root_path, :logger, :connection, :root, :default_headers
         | 
| 14 | 
            +
                
         | 
| 15 | 
            +
                # TODO: use CacheableResource
         | 
| 16 | 
            +
                def initialize(base_uri, options = {})
         | 
| 17 | 
            +
                  options = options.symbolize_keys
         | 
| 18 | 
            +
                  @base_uri = base_uri
         | 
| 19 | 
            +
                  @root_path = options.delete(:root_path) || '/'
         | 
| 20 | 
            +
                  @logger = options.delete(:logger) || NullLogger.new
         | 
| 21 | 
            +
                  @default_headers = options.delete(:default_headers) || {'Accept' => 'application/json'}
         | 
| 22 | 
            +
                  @connection = Restfully.adapter.new(@base_uri, options.merge(:logger => @logger))
         | 
| 23 | 
            +
                  @root = Resource.new(URI.parse(@root_path), self)
         | 
| 24 | 
            +
                  yield @root.load, self if block_given?
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
                
         | 
| 27 | 
            +
                # TODO: inspect response headers to determine which methods are available
         | 
| 28 | 
            +
                def get(path, options = {})
         | 
| 29 | 
            +
                  path = path.to_s
         | 
| 30 | 
            +
                  options = options.symbolize_keys
         | 
| 31 | 
            +
                  uri = URI.parse(base_uri)
         | 
| 32 | 
            +
                  path_uri = URI.parse(path)
         | 
| 33 | 
            +
                  # if the given path is complete URL, forget the base_uri, else append the path to the base_uri
         | 
| 34 | 
            +
                  unless path_uri.scheme.nil?
         | 
| 35 | 
            +
                    uri = path_uri
         | 
| 36 | 
            +
                  else  
         | 
| 37 | 
            +
                    uri.path << path
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
                  request = HTTP::Request.new(uri, :headers => options.delete(:headers) || {}, :query => options.delete(:query) || {})
         | 
| 40 | 
            +
                  request.add_headers(@default_headers) unless @default_headers.empty?
         | 
| 41 | 
            +
                  logger.info "GET #{request.uri}, #{request.headers.inspect}"
         | 
| 42 | 
            +
                  response = connection.get(request)
         | 
| 43 | 
            +
                  logger.debug "Response to GET #{request.uri}: #{response.headers.inspect}"
         | 
| 44 | 
            +
                  response = deal_with_eventual_errors(response, request)
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
                
         | 
| 47 | 
            +
                protected
         | 
| 48 | 
            +
                def deal_with_eventual_errors(response, request)
         | 
| 49 | 
            +
                  case response.status
         | 
| 50 | 
            +
                  when 400..499
         | 
| 51 | 
            +
                    # should retry on 406 with another Accept header
         | 
| 52 | 
            +
                    raise Restfully::HTTP::ClientError, response
         | 
| 53 | 
            +
                  when 500..599  
         | 
| 54 | 
            +
                    raise Restfully::HTTP::ServerError, response
         | 
| 55 | 
            +
                  else
         | 
| 56 | 
            +
                    response
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
              end
         | 
| 61 | 
            +
            end
         |