restfully 0.3.2 → 0.4.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.
@@ -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