restfully 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,23 +4,6 @@ end unless defined?(BasicObject)
4
4
 
5
5
  # monkey patching:
6
6
  class Hash
7
- # Taken from ActiveSupport
8
- def symbolize_keys
9
- inject({}) do |options, (key, value)|
10
- options[(key.to_sym rescue key) || key] = value
11
- options
12
- end
13
- end
14
-
15
- # Taken from ActiveSupport
16
- def stringify_keys
17
- inject({}) do |options, (key, value)|
18
- options[key.to_s] = value
19
- options
20
- end
21
- end
22
-
23
-
24
7
  # This is by no means the standard way to transform ruby objects into query parameters
25
8
  # but it is targeted at our own needs
26
9
  def to_params
@@ -21,7 +21,7 @@ module Restfully
21
21
  raise NotImplementedError, "PUT is not supported by your adapter."
22
22
  end
23
23
  def delete(request)
24
- raise NotImplementedError, "DELETEis not supported by your adapter."
24
+ raise NotImplementedError, "DELETE is not supported by your adapter."
25
25
  end
26
26
  end
27
27
 
@@ -5,14 +5,35 @@ module Restfully
5
5
  module HTTP
6
6
  module Adapters
7
7
  class RestClientAdapter < AbstractAdapter
8
+
8
9
  def initialize(base_uri, options = {})
9
10
  super(base_uri, options)
10
11
  @options[:user] = @options.delete(:username)
11
- end
12
+ end # def initialize
13
+
14
+ def head(request)
15
+ in_order_to_get_the_response_to(request) do |resource|
16
+ resource.head(request.headers)
17
+ end
18
+ end # def get
19
+
12
20
  def get(request)
21
+ in_order_to_get_the_response_to(request) do |resource|
22
+ resource.get(request.headers)
23
+ end
24
+ end # def get
25
+
26
+ def post(request)
27
+ in_order_to_get_the_response_to(request) do |resource|
28
+ resource.post(request.raw_body, request.headers)
29
+ end
30
+ end # def post
31
+
32
+ protected
33
+ def in_order_to_get_the_response_to(request, &block)
13
34
  begin
14
35
  resource = RestClient::Resource.new(request.uri.to_s, @options)
15
- response = resource.get(request.headers)
36
+ response = block.call(resource)
16
37
  headers = response.headers
17
38
  body = response.to_s
18
39
  headers.delete(:status)
@@ -23,7 +44,8 @@ module Restfully
23
44
  status = e.http_code
24
45
  end
25
46
  Response.new(status, headers, body)
26
- end
47
+ end # def in_order_to_get_the_response_to
48
+
27
49
  end
28
50
 
29
51
  end
@@ -2,8 +2,8 @@ require 'uri'
2
2
  module Restfully
3
3
  module HTTP
4
4
  class Request
5
- include Headers
6
- attr_reader :headers, :body, :uri
5
+ include Headers, Restfully::Parsing
6
+ attr_reader :headers, :uri
7
7
  attr_accessor :retries
8
8
  def initialize(url, options = {})
9
9
  options = options.symbolize_keys
@@ -12,10 +12,27 @@ module Restfully
12
12
  if query = options.delete(:query)
13
13
  @uri.query = [@uri.query, query.to_params].compact.join("&")
14
14
  end
15
- @body = body
15
+ @body = options.delete(:body)
16
16
  @retries = 0
17
17
  end
18
18
 
19
+
20
+ def body
21
+ if @body.kind_of?(String)
22
+ @unserialized_body ||= unserialize(@body, :content_type => @headers['Content-Type'])
23
+ else
24
+ @body
25
+ end
26
+ end
27
+
28
+ def raw_body
29
+ if @body.kind_of?(String)
30
+ @body
31
+ else
32
+ @serialized_body ||= serialize(@body, :content_type => @headers['Content-Type'])
33
+ end
34
+ end
35
+
19
36
  def add_headers(headers = {})
20
37
  @headers.merge!(sanitize_http_headers(headers || {}))
21
38
  end
@@ -1,19 +1,37 @@
1
1
  module Restfully
2
2
  module HTTP
3
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
4
  class Response
6
5
  include Headers, Restfully::Parsing
7
6
  attr_reader :status, :headers
7
+
8
+ # <tt>body</tt>:: may be a string (that will be parsed when calling the #body function), or an object (that will be returned as is when calling the #body function)
8
9
  def initialize(status, headers, body)
9
10
  @status = status.to_i
10
11
  @headers = sanitize_http_headers(headers)
11
- @body = (body.nil? || body.empty?) ? nil : body.to_s
12
+ @body = body
12
13
  end
13
14
 
14
15
  def body
15
- @unserialized_body ||= unserialize(@body, :content_type => @headers['Content-Type']) unless @body.nil?
16
+ if @body.kind_of?(String)
17
+ if @body.empty?
18
+ nil
19
+ else
20
+ @unserialized_body ||= unserialize(@body, :content_type => @headers['Content-Type'])
21
+ end
22
+ else
23
+ @body
24
+ end
25
+ end
26
+
27
+ def raw_body
28
+ if @body.kind_of?(String)
29
+ @body
30
+ else
31
+ @serialized_body ||= serialize(@body, :content_type => @headers['Content-Type']) unless @body.nil?
32
+ end
16
33
  end
34
+
17
35
  end
18
36
  end
19
37
  end
@@ -1,3 +1,4 @@
1
+ require 'uri'
1
2
  module Restfully
2
3
  class Link
3
4
 
@@ -9,7 +10,7 @@ module Restfully
9
10
  def initialize(attributes = {})
10
11
  @rel = attributes['rel']
11
12
  @title = attributes['title']
12
- @href = attributes['href']
13
+ @href = URI.parse(attributes['href'].to_s)
13
14
  @resolvable = attributes['resolvable'] || false
14
15
  @resolved = attributes['resolved'] || false
15
16
  end
@@ -20,8 +21,8 @@ module Restfully
20
21
 
21
22
  def valid?
22
23
  @errors = []
23
- if href.nil? || href.empty?
24
- errors << "href cannot be empty."
24
+ if href.nil?
25
+ errors << "href cannot be nil."
25
26
  end
26
27
  unless VALID_RELATIONSHIPS.include?(rel)
27
28
  errors << "#{rel} is not a valid link relationship."
@@ -1,111 +1,180 @@
1
- require 'delegate'
2
-
3
1
  module Restfully
4
2
 
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)
3
+ # This class represents a Resource, which can be accessed and manipulated
4
+ # via HTTP methods.
5
+ #
6
+ # The <tt>#load</tt> method must have been called on the resource before
7
+ # trying to access its attributes or links.
8
+ #
9
+ class Resource
8
10
 
9
- undef :type if self.respond_to? :type
10
- attr_reader :uri, :session, :state, :raw, :associations
11
-
11
+ attr_reader :uri,
12
+ :session,
13
+ :links,
14
+ :title,
15
+ :properties,
16
+ :executed_requests
17
+
18
+ # == Description
19
+ # Creates a new Resource.
20
+ # <tt>uri</tt>:: a URI object representing the URI of the resource
21
+ # (complete, absolute or relative URI)
22
+ # <tt>session</tt>:: an instantiated Restfully::Session object
23
+ # <tt>options</tt>:: a hash of options (see below)
24
+ # == Options
25
+ # <tt>:title</tt>:: an optional title for the resource
12
26
  def initialize(uri, session, options = {})
13
27
  options = options.symbolize_keys
14
- @uri = uri
28
+ @uri = uri.kind_of?(URI) ? uri : URI.parse(uri.to_s)
15
29
  @session = session
16
- @state = :unloaded
17
- @attributes = {}
18
- super(@attributes)
19
- @associations = {}
30
+ @title = options[:title]
31
+ reset
20
32
  end
21
33
 
22
- def loaded?; @state == :loaded; end
34
+ # Resets all the inner objects of the resource
35
+ # (you must call <tt>#load</tt> if you want to repopulate the resource).
36
+ def reset
37
+ @executed_requests = Hash.new
38
+ @links = Hash.new
39
+ @properties = Hash.new
40
+ self
41
+ end
42
+
43
+ # == Description
44
+ # Returns the value corresponding to the specified key,
45
+ # among the list of resource properties
46
+ #
47
+ # == Usage
48
+ # resource["uid"]
49
+ # => "rennes"
50
+ def [](key)
51
+ @properties[key]
52
+ end
23
53
 
24
54
  def method_missing(method, *args)
25
- if association = @associations[method.to_s]
26
- session.logger.debug "Loading association #{method}, args=#{args.inspect}"
27
- association.load(*args)
55
+ if link = @links[method.to_s]
56
+ session.logger.debug "Loading link #{method}, args=#{args.inspect}"
57
+ link.load(*args)
28
58
  else
29
59
  super(method, *args)
30
60
  end
31
61
  end
32
-
62
+
63
+ # == Description
64
+ # Executes a GET request on the resource, and populate the list of its
65
+ # properties and links
66
+ # <tt>options</tt>:: list of options to pass to the request (see below)
67
+ # == Options
68
+ # <tt>:reload</tt>:: if set to true, a GET request will be triggered even if the resource has already been loaded [default=false]
69
+ # <tt>:query</tt>:: a hash of query parameters to pass along the request. E.g. : resource.load(:query => {:from => (Time.now-3600).to_i, :to => Time.now.to_i})
70
+ # <tt>:headers</tt>:: a hash of HTTP headers to pass along the request. E.g. : resource.load(:headers => {'Accept' => 'application/json'})
71
+ # <tt>:body</tt>:: if you already have the unserialized response body of this resource, you may pass it so that the GET request is not triggered.
33
72
  def load(options = {})
34
73
  options = options.symbolize_keys
35
- force_reload = !!options.delete(:reload) || options.has_key?(:query)
36
- if loaded? && !force_reload && options[:raw].nil?
74
+ force_reload = !!options.delete(:reload)
75
+ if !force_reload && (request = executed_requests['GET']) && request['options'] == options && request['body']
37
76
  self
38
77
  else
39
- @associations.clear
40
- @attributes.clear
41
- @raw = options[:raw]
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 "uid", "type"
50
- # instance_variable_set "@#{key}".to_sym, value
51
- when 'links' then next
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
78
+ reset
79
+ executed_requests['GET'] = {
80
+ 'options' => options,
81
+ 'body' => options[:body] || session.get(uri, options).body
82
+ }
83
+ executed_requests['GET']['body'].each do |key, value|
84
+ populate_object(key, value)
62
85
  end
63
- @state = :loaded
64
86
  self
65
87
  end
66
88
  end
89
+
90
+ def submit(payload, options = {})
91
+ options = options.symbolize_keys
92
+ raise NotImplementedError, "The POST method is not allowed for this resource." unless http_methods.include?('POST')
93
+ raise ArgumentError, "You must pass a payload string" unless payload.kind_of?(String)
94
+ session.post(payload, options)
95
+ end
96
+
97
+ # == Description
98
+ # Returns the list of allowed HTTP methods on the resource.
99
+ # == Usage
100
+ # resource.http_methods
101
+ # => ['GET', 'POST']
102
+ #
103
+ def http_methods
104
+ if executed_requests['HEAD'].nil?
105
+ response = session.head(uri)
106
+ executed_requests['HEAD'] = {'headers' => response.headers}
107
+ end
108
+ (executed_requests['HEAD']['headers']['Allow'] || "GET").split(/,\s*/)
109
+ end
67
110
 
68
111
  def respond_to?(method, *args)
69
- @associations.has_key?(method.to_s) || super(method, *args)
112
+ @links.has_key?(method.to_s) || super(method, *args)
70
113
  end
71
114
 
72
- # Removed: use `y resource` to get pretty output
73
- # def inspect(options = {:space => "\t"})
74
- # output = "#<#{self.class}:0x#{self.object_id.to_s(16)}"
75
- # if loaded?
76
- # output += "\n#{options[:space]}------------ META ------------"
77
- # output += "\n#{options[:space]}@uri: #{uri.inspect}"
78
- # output += "\n#{options[:space]}@uid: #{uid.inspect}"
79
- # output += "\n#{options[:space]}@type: #{type.inspect}"
80
- # @associations.each do |title, assoc|
81
- # output += "\n#{options[:space]}@#{title}: #{assoc.class.name}"
82
- # end
83
- # unless @attributes.empty?
84
- # output += "\n#{options[:space]}------------ PROPERTIES ------------"
85
- # @attributes.each do |key, value|
86
- # output += "\n#{options[:space]}#{key.inspect} => #{value.inspect}"
87
- # end
88
- # end
89
- # end
90
- # output += ">"
91
- # end
115
+ def inspect(*args)
116
+ @properties.inspect(*args)
117
+ end
118
+
119
+ def pretty_print(pp)
120
+ pp.text "#<#{self.class}:0x#{self.object_id.to_s(16)}"
121
+ pp.nest 2 do
122
+ pp.breakable
123
+ pp.text "@uri="
124
+ uri.pretty_print(pp)
125
+ if @links.length > 0
126
+ pp.breakable
127
+ pp.text "LINKS"
128
+ pp.nest 2 do
129
+ @links.to_a.each_with_index do |(key, value), i|
130
+ pp.breakable
131
+ pp.text "@#{key}=#<#{value.class}:0x#{value.object_id.to_s(16)}>"
132
+ pp.text "," if i < @links.length-1
133
+ end
134
+ end
135
+ end
136
+ if @properties.length > 0
137
+ pp.breakable
138
+ pp.text "PROPERTIES"
139
+ pp.nest 2 do
140
+ @properties.to_a.each_with_index do |(key, value), i|
141
+ pp.breakable
142
+ pp.text "#{key.inspect}=>"
143
+ value.pretty_print(pp)
144
+ pp.text "," if i < @properties.length-1
145
+ end
146
+ end
147
+ end
148
+ yield pp if block_given?
149
+ end
150
+ pp.text ">"
151
+ end
92
152
 
93
153
  protected
154
+ def populate_object(key, value)
155
+ case key
156
+ when "links"
157
+ value.each{|link| define_link(Link.new(link))}
158
+ else
159
+ case value
160
+ when Hash
161
+ @properties.store(key, SpecialHash.new.replace(value)) unless @links.has_key?(key)
162
+ when Array
163
+ @properties.store(key, SpecialArray.new(value))
164
+ else
165
+ @properties.store(key, value)
166
+ end
167
+ end
168
+ end
94
169
  def define_link(link)
95
170
  if link.valid?
96
171
  case link.rel
97
172
  when 'parent'
98
- @associations['parent'] = Resource.new(link.href, session)
173
+ @links['parent'] = Resource.new(uri.merge(link.href), session)
99
174
  when 'collection'
100
- raw_included = link.resolved? ? raw[link.title] : nil
101
- @associations[link.title] = Collection.new(link.href, session,
102
- :raw => raw_included,
103
- :title => link.title)
175
+ @links[link.title] = Collection.new(uri.merge(link.href), session, :title => link.title)
104
176
  when 'member'
105
- raw_included = link.resolved? ? raw[link.title] : nil
106
- @associations[link.title] = Resource.new(link.href, session,
107
- :title => link.title,
108
- :raw => raw_included)
177
+ @links[link.title] = Resource.new(uri.merge(link.href), session, :title => link.title)
109
178
  when 'self'
110
179
  # we do nothing
111
180
  end
@@ -113,6 +182,7 @@ module Restfully
113
182
  session.logger.warn link.errors.join("\n")
114
183
  end
115
184
  end
185
+
116
186
 
117
187
  end # class Resource
118
188
  end # module Restfully