restfully 0.6.3 → 0.7.0.pre

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.
Files changed (73) hide show
  1. data/README.md +166 -0
  2. data/Rakefile +35 -35
  3. data/bin/restfully +68 -10
  4. data/lib/restfully.rb +8 -14
  5. data/lib/restfully/collection.rb +70 -90
  6. data/lib/restfully/error.rb +2 -0
  7. data/lib/restfully/http.rb +3 -3
  8. data/lib/restfully/http/error.rb +1 -20
  9. data/lib/restfully/http/helper.rb +49 -0
  10. data/lib/restfully/http/request.rb +60 -24
  11. data/lib/restfully/http/response.rb +55 -24
  12. data/lib/restfully/link.rb +32 -24
  13. data/lib/restfully/media_type.rb +70 -0
  14. data/lib/restfully/media_type/abstract_media_type.rb +162 -0
  15. data/lib/restfully/media_type/application_json.rb +21 -0
  16. data/lib/restfully/media_type/application_vnd_bonfire_xml.rb +177 -0
  17. data/lib/restfully/media_type/application_x_www_form_urlencoded.rb +33 -0
  18. data/lib/restfully/media_type/grid5000.rb +67 -0
  19. data/lib/restfully/media_type/wildcard.rb +27 -0
  20. data/lib/restfully/rack.rb +1 -0
  21. data/lib/restfully/rack/basic_auth.rb +26 -0
  22. data/lib/restfully/resource.rb +134 -197
  23. data/lib/restfully/session.rb +127 -70
  24. data/lib/restfully/version.rb +3 -0
  25. data/spec/fixtures/bonfire-collection-with-fragments.xml +6 -0
  26. data/spec/fixtures/bonfire-compute-existing.xml +43 -0
  27. data/spec/fixtures/bonfire-empty-collection.xml +4 -0
  28. data/spec/fixtures/bonfire-experiment-collection.xml +51 -0
  29. data/spec/fixtures/bonfire-network-collection.xml +35 -0
  30. data/spec/fixtures/bonfire-network-existing.xml +6 -0
  31. data/spec/fixtures/bonfire-root.xml +5 -0
  32. data/spec/fixtures/grid5000-rennes-jobs.json +988 -146
  33. data/spec/fixtures/grid5000-rennes.json +63 -0
  34. data/spec/restfully/collection_spec.rb +87 -0
  35. data/spec/restfully/http/helper_spec.rb +18 -0
  36. data/spec/restfully/http/request_spec.rb +97 -0
  37. data/spec/restfully/http/response_spec.rb +53 -0
  38. data/spec/restfully/link_spec.rb +80 -0
  39. data/spec/restfully/media_type/application_vnd_bonfire_xml_spec.rb +153 -0
  40. data/spec/restfully/media_type_spec.rb +117 -0
  41. data/spec/restfully/resource_spec.rb +109 -0
  42. data/spec/restfully/session_spec.rb +229 -0
  43. data/spec/spec_helper.rb +10 -9
  44. metadata +162 -83
  45. data/.document +0 -5
  46. data/CHANGELOG +0 -62
  47. data/README.rdoc +0 -146
  48. data/TODO.rdoc +0 -3
  49. data/VERSION +0 -1
  50. data/examples/grid5000.rb +0 -33
  51. data/examples/scratch.rb +0 -37
  52. data/lib/restfully/extensions.rb +0 -34
  53. data/lib/restfully/http/adapters/abstract_adapter.rb +0 -29
  54. data/lib/restfully/http/adapters/patron_adapter.rb +0 -16
  55. data/lib/restfully/http/adapters/rest_client_adapter.rb +0 -75
  56. data/lib/restfully/http/headers.rb +0 -20
  57. data/lib/restfully/parsing.rb +0 -66
  58. data/lib/restfully/special_array.rb +0 -5
  59. data/lib/restfully/special_hash.rb +0 -5
  60. data/restfully.gemspec +0 -114
  61. data/spec/collection_spec.rb +0 -120
  62. data/spec/fixtures/configuration_file.yml +0 -4
  63. data/spec/fixtures/grid5000-sites.json +0 -540
  64. data/spec/http/error_spec.rb +0 -18
  65. data/spec/http/headers_spec.rb +0 -17
  66. data/spec/http/request_spec.rb +0 -49
  67. data/spec/http/response_spec.rb +0 -19
  68. data/spec/http/rest_client_adapter_spec.rb +0 -35
  69. data/spec/link_spec.rb +0 -61
  70. data/spec/parsing_spec.rb +0 -40
  71. data/spec/resource_spec.rb +0 -320
  72. data/spec/restfully_spec.rb +0 -16
  73. data/spec/session_spec.rb +0 -171
@@ -0,0 +1,21 @@
1
+ require 'json'
2
+
3
+ module Restfully
4
+ module MediaType
5
+
6
+ class ApplicationJson < AbstractMediaType
7
+ class JSONParser
8
+ def self.load(io, *args)
9
+ JSON.load(io)
10
+ end
11
+ def self.dump(object, *args)
12
+ JSON.dump(object)
13
+ end
14
+ end
15
+ set :signature, "application/json"
16
+ set :parser, JSONParser
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,177 @@
1
+ require 'xml'
2
+
3
+ module Restfully
4
+ module MediaType
5
+ class ApplicationVndBonfireXml < AbstractMediaType
6
+ NS = "http://api.bonfire-project.eu/doc/schemas/occi"
7
+ HIDDEN_TYPE_KEY = "__type__"
8
+
9
+ def collection?
10
+ !!(property("items") && property("total") && property("offset"))
11
+ end
12
+
13
+ class Parser
14
+ class << self
15
+ def load(io, *args)
16
+ if io.respond_to?(:read)
17
+ io = io.read
18
+ end
19
+ xml = XML::Document.string(io.to_s)
20
+ load_xml(xml.root).merge(HIDDEN_TYPE_KEY => xml.root.name)
21
+ end
22
+
23
+
24
+ def dump(object, opts = {})
25
+ root_name = if object[HIDDEN_TYPE_KEY]
26
+ object[HIDDEN_TYPE_KEY]
27
+ elsif opts[:uri]
28
+ # OK, this is ugly
29
+ opts[:uri].path.to_s.split("/").last.gsub(/s$/,'')
30
+ else
31
+ fail "Can't infer a name for the root element for object: #{object.inspect}"
32
+ end
33
+ xml = XML::Document.new
34
+ xml.root = XML::Node.new(root_name)
35
+ xml.root["xmlns"] = NS
36
+ dump_object(object, xml.root)
37
+ xml.to_s
38
+ end
39
+
40
+ protected
41
+
42
+ def load_xml(element)
43
+ h = {}
44
+ element.each_element do |e|
45
+ next if e.empty?
46
+ if e.name == 'items' && element.name == 'collection'
47
+ h['total'] = e.attributes['total'].to_i
48
+ h['offset'] = e.attributes['offset'].to_i
49
+ h['items'] = []
50
+ e.each_element do |e2|
51
+ h['items'] << load_xml(e2).merge(HIDDEN_TYPE_KEY => e2.name)
52
+ end
53
+ # if no total specified, total equals the number of items
54
+ h['total'] = h['items'].length if h['total'] == 0
55
+ else
56
+ load_xml_element(e, h)
57
+ end
58
+ end
59
+ if element.attributes["href"]
60
+ h["link"] ||= []
61
+ h["link"].push({
62
+ "href" => element.attributes["href"],
63
+ "rel" => "self"
64
+ })
65
+ end
66
+ h
67
+ end
68
+
69
+ # We use <tt>HIDDEN_TYPE_KEY</tt> property to keep track of the root name.
70
+ def load_xml_element(element, h)
71
+ if element.attributes? && href = element.attributes.find{|attr|
72
+ attr.name == "href"
73
+ }
74
+ single_or_array(h, element.name, element.attributes.inject({}) {|memo, attr| memo[attr.name] = attr.value; memo})
75
+ #build_resource(href.value))
76
+ else
77
+ if element.children.any?(&:element?)
78
+ single_or_array(h, element.name, load_xml(element))
79
+ else
80
+ value = element.content.strip
81
+ unless value.empty?
82
+ single_or_array(h, element.name, value)
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ def single_or_array(h, key, value)
89
+ if h.has_key?(key)
90
+ if h[key].kind_of?(Array)
91
+ h[key].push(value)
92
+ else
93
+ h[key] = [h[key], value]
94
+ end
95
+ elsif ["disk", "nic", "link"].include?(key)
96
+ h[key] = [value]
97
+ else
98
+ h[key] = value
99
+ end
100
+ end
101
+
102
+ # def build_resource(uri)
103
+ # uri
104
+ # end
105
+
106
+ def dump_object(object, parent)
107
+ case object
108
+ when Restfully::Resource
109
+ dump_object({"href" => object.uri.to_s}, parent)
110
+ when Hash
111
+ if object.has_key?("href")
112
+ # only attributes
113
+ object.each{|kattr,vattr|
114
+ parent.attributes[kattr.to_s] = vattr
115
+ }
116
+ else
117
+ object.each do |k,v|
118
+ next if k == HIDDEN_TYPE_KEY
119
+ node = XML::Node.new(k.to_s)
120
+ parent << node
121
+ dump_object(v, node)
122
+ end
123
+ end
124
+ when Array
125
+ dump_object(object[0], parent)
126
+ object[1..-1].each{|item|
127
+ node = XML::Node.new(parent.name)
128
+ dump_object(item, node)
129
+ parent.parent << node
130
+ }
131
+ else
132
+ parent << object.to_s
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ set :signature, "application/vnd.bonfire+xml"
139
+ set :parser, Parser
140
+
141
+ def extract_links
142
+ (property.delete("link") || []).map do |link|
143
+ l = Link.new(
144
+ :rel => link['rel'],
145
+ :type => link['type'] || self.class.default_type,
146
+ :href => link['href'],
147
+ :title => link["title"],
148
+ :id => link["title"]
149
+ )
150
+ end
151
+ end
152
+
153
+ def represents?(id)
154
+ property("id") == id.to_s || property("name") == id.to_s
155
+ end
156
+
157
+ def complete?
158
+ if property.reject{|k,v| k==HIDDEN_TYPE_KEY}.empty? && links.find(&:self?)
159
+ false
160
+ else
161
+ true
162
+ end
163
+ end
164
+
165
+ # Only for collections
166
+ def each(*args, &block)
167
+ @items ||= (property("items") || []).map{|i|
168
+ self.class.new(self.class.serialize(i), @session)
169
+ }
170
+ @items.each(*args, &block)
171
+ end
172
+
173
+ end
174
+
175
+ register ApplicationVndBonfireXml
176
+ end
177
+ end
@@ -0,0 +1,33 @@
1
+ require 'rack/utils'
2
+
3
+ module Restfully
4
+ module MediaType
5
+
6
+ class ApplicationXWwwFormUrlencoded < AbstractMediaType
7
+
8
+ class Parser
9
+ def self.load(object, *args)
10
+ if object.respond_to? :to_str
11
+ object = object.to_str
12
+ elsif object.respond_to? :to_io
13
+ object = object.to_io.read
14
+ else
15
+ object = object.read
16
+ end
17
+ ::Rack::Utils.parse_nested_query(object)
18
+ end
19
+
20
+ def self.dump(object, *args)
21
+ ::Rack::Utils.build_nested_query(object)
22
+ end
23
+ end
24
+
25
+ set :signature, "application/x-www-form-urlencoded"
26
+ set :parser, Parser
27
+ end
28
+
29
+ register ApplicationXWwwFormUrlencoded
30
+
31
+ end
32
+
33
+ end
@@ -0,0 +1,67 @@
1
+ require 'json'
2
+
3
+ module Restfully
4
+ module MediaType
5
+ class Grid5000 < AbstractMediaType
6
+
7
+ set :signature, %w{
8
+ grid
9
+ site
10
+ cluster
11
+ node
12
+ nodeStatus
13
+ version
14
+ collection
15
+ timeseries
16
+ versions
17
+ user
18
+ metric
19
+ job
20
+ deployment
21
+ notification
22
+ }.map{|n|
23
+ "application/vnd.fr.grid5000.api.#{n}+json"
24
+ }.push(
25
+ "application/vnd.grid5000+json"
26
+ )
27
+ set :parser, ApplicationJson::JSONParser
28
+
29
+ def extract_links
30
+ (property.delete("links") || []).map do |link|
31
+ l = Link.new(
32
+ :rel => link['rel'],
33
+ :type => link['type'],
34
+ :href => link['href'],
35
+ :title => link["title"],
36
+ :id => link["title"]
37
+ )
38
+ end
39
+ end
40
+
41
+ def collection?
42
+ !!(property("items") && property("total") && property("offset"))
43
+ end
44
+
45
+ def meta
46
+ if collection?
47
+ property("items")
48
+ else
49
+ property
50
+ end
51
+ end
52
+
53
+ def represents?(id)
54
+ property("uid") == id.to_s || property("uid") == id.to_i
55
+ end
56
+
57
+ # Only for collections
58
+ def each(*args, &block)
59
+ (property("items") || []).map{|i|
60
+ self.class.new(self.class.serialize(i), @session)
61
+ }.each(*args, &block)
62
+ end
63
+
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,27 @@
1
+ module Restfully
2
+ module MediaType
3
+
4
+ class Wildcard < AbstractMediaType
5
+ class IdentityParser
6
+ def self.load(object, *args)
7
+ if object.respond_to? :to_str
8
+ object = object.to_str
9
+ elsif object.respond_to? :to_io
10
+ object = object.to_io.read
11
+ else
12
+ object = object.read
13
+ end
14
+ end
15
+
16
+ def self.dump(object, *args)
17
+ object
18
+ end
19
+ end
20
+
21
+ set :signature, "*/*"
22
+ set :parser, IdentityParser
23
+ end
24
+
25
+ end
26
+
27
+ end
@@ -0,0 +1 @@
1
+ require 'restfully/rack/basic_auth'
@@ -0,0 +1,26 @@
1
+ require 'base64'
2
+
3
+ module Restfully
4
+ module Rack
5
+ class BasicAuth
6
+ def initialize(app, username, password)
7
+ @app = app
8
+ @username = username
9
+ @password = password
10
+ end
11
+
12
+ def call(env)
13
+ env['HTTP_AUTHORIZATION'] = [
14
+ "Basic",
15
+ Base64.encode64([
16
+ @username,
17
+ @password
18
+ ].join(":"))
19
+ ].join(" ")
20
+
21
+ @app.call(env)
22
+ end
23
+
24
+ end # class BasicAuth
25
+ end # module Rack
26
+ end
@@ -1,256 +1,193 @@
1
1
  module Restfully
2
-
3
2
  # This class represents a Resource, which can be accessed and manipulated
4
3
  # via HTTP methods.
5
- #
4
+ #
6
5
  # The <tt>#load</tt> method must have been called on the resource before
7
6
  # trying to access its attributes or links.
8
- #
7
+ #
9
8
  class Resource
10
-
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
26
- def initialize(uri, session, options = {})
27
- options = options.symbolize_keys
28
- @uri = uri.kind_of?(URI) ? uri : URI.parse(uri.to_s)
9
+ attr_reader :response, :request, :session
10
+
11
+ def initialize(session, response, request)
29
12
  @session = session
30
- @title = options[:title]
31
- reset
13
+ @response = response
14
+ @request = request
15
+ @associations = {}
32
16
  end
33
-
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
- @status = :stale
41
- self
42
- end
43
-
17
+
44
18
  # == Description
45
- # Returns the value corresponding to the specified key,
19
+ # Returns the value corresponding to the specified key,
46
20
  # among the list of resource properties
47
- #
21
+ #
48
22
  # == Usage
49
23
  # resource["uid"]
50
24
  # => "rennes"
51
25
  def [](key)
52
- @properties[key]
26
+ media_type.property(key)
53
27
  end
54
-
55
- def respond_to?(method, *args)
56
- @links.has_key?(method.to_s) || super(method, *args)
28
+
29
+ def uri
30
+ request.uri
31
+ end
32
+
33
+ def media_type
34
+ response.media_type
35
+ end
36
+
37
+ def collection?
38
+ media_type.collection?
57
39
  end
58
40
 
59
- def method_missing(method, *args)
60
- if link = @links[method.to_s]
61
- session.logger.debug "Loading link #{method}, args=#{args.inspect}"
62
- link.load(*args)
63
- else
64
- super(method, *args)
65
- end
41
+ def kind
42
+ collection? ? "Collection" : "Resource"
66
43
  end
67
44
 
68
- # == Description
69
- # Executes a GET request on the resource, and populate the list of its
70
- # properties and links
71
- # <tt>options</tt>:: list of options to pass to the request (see below)
72
- # == Options
73
- # <tt>:reload</tt>:: if set to true, a GET request will be triggered
74
- # even if the resource has already been loaded [default=false]
75
- # <tt>:query</tt>:: a hash of query parameters to pass along the request.
76
- # E.g. : resource.load(:query => {:from => (Time.now-3600).to_i, :to => Time.now.to_i})
77
- # <tt>:headers</tt>:: a hash of HTTP headers to pass along the request.
78
- # E.g. : resource.load(:headers => {'Accept' => 'application/json'})
79
- # <tt>:body</tt>:: if you already have the unserialized response body of this resource,
80
- # you may pass it so that the GET request is not triggered.
45
+ def signature(closed=true)
46
+ s = "#<#{kind}:0x#{object_id.to_s(16)}"
47
+ s += " uri=#{uri.to_s}"
48
+ s += ">" if closed
49
+ s
50
+ end
51
+
81
52
  def load(options = {})
82
- options = options.symbolize_keys
83
- force_reload = !!options.delete(:reload)
84
- stale! unless !force_reload && (request = executed_requests['GET']) && request['options'] == options && request['body']
85
- if stale?
86
- reset
87
- if !force_reload && options[:body]
88
- body = options[:body]
89
- headers = {}
53
+ # Send a GET request only if given a different set of options
54
+ if @request.update!(options) || @request.no_cache?
55
+ @response = session.execute(@request)
56
+ if session.process(@response, @request)
57
+ @associations.clear
90
58
  else
91
- response = session.get(uri, options)
92
- body = response.body
93
- headers = response.headers
94
- end
95
- executed_requests['GET'] = {
96
- 'options' => options,
97
- 'body' => body,
98
- 'headers' => headers
99
- }
100
- executed_requests['GET']['body'].each do |key, value|
101
- populate_object(key, value)
59
+ raise Error, "Cannot reload the resource"
102
60
  end
103
- @status = :loaded
104
61
  end
105
- self
62
+
63
+ build
64
+ end
65
+
66
+ def relationships
67
+ @associations.keys
68
+ end
69
+
70
+ def properties
71
+ media_type.property.reject{|k,v|
72
+ # do not return keys used for internal use
73
+ k.to_s =~ /^\_\_(.+)\_\_$/
74
+ }
106
75
  end
107
76
 
108
- # Convenience function to make a resource.load(:reload => true)
109
77
  def reload
110
- current_options = executed_requests['GET']['options'] rescue {}
111
- stale!
112
- self.load(current_options.merge(:reload => true))
78
+ load(:head => {'Cache-Control' => 'no-cache'})
113
79
  end
114
-
115
- # == Description
116
- # Executes a POST request on the resource, reload it and returns self if successful.
117
- # If the response status is different from 2xx, raises a HTTP::ClientError or HTTP::ServerError.
118
- # <tt>payload</tt>:: the input body of the request.
119
- # It may be a serialized string, or a ruby object
120
- # (that will be serialized according to the given or default content-type).
121
- # <tt>options</tt>:: list of options to pass to the request (see below)
122
- # == Options
123
- # <tt>:query</tt>:: a hash of query parameters to pass along the request.
124
- # E.g. : resource.submit("body", :query => {:param1 => "value1"})
125
- # <tt>:headers</tt>:: a hash of HTTP headers to pass along the request.
126
- # E.g. : resource.submit("body", :headers => {:accept => 'application/json', :content_type => 'application/json'})
127
- def submit(payload, options = {})
128
- options = options.symbolize_keys
129
- raise NotImplementedError, "The POST method is not allowed for this resource." unless http_methods.include?('POST')
130
- raise ArgumentError, "You must pass a payload" if payload.nil?
131
- headers = {
132
- :content_type => (executed_requests['GET']['headers']['Content-Type'] || "application/x-www-form-urlencoded").split(/,/).sort{|a,b| a.length <=> b.length}[0],
133
- :accept => (executed_requests['GET']['headers']['Content-Type'] || "text/plain")
134
- }.merge(options[:headers] || {})
135
- options = {:headers => headers}
136
- options.merge!(:query => options[:query]) unless options[:query].nil?
137
- response = session.post(self.uri, payload, options) # raises an exception if there is an error
138
- stale!
139
- if [201, 202].include?(response.status)
140
- Resource.new(uri_for(response.headers['Location']), session).load
80
+
81
+ def submit(*args)
82
+ if allow?(:post)
83
+ payload, options = extract_payload_from_args(args)
84
+ session.post(request.uri, payload, options)
141
85
  else
142
- reload
86
+ raise MethodNotAllowed
143
87
  end
144
88
  end
145
-
146
- # == Description
147
- # Executes a DELETE request on the resource, and returns true if successful.
148
- # If the response status is different from 2xx or 3xx, raises an HTTP::ClientError or HTTP::ServerError.
149
- # <tt>options</tt>:: list of options to pass to the request (see below)
150
- # == Options
151
- # <tt>:query</tt>:: a hash of query parameters to pass along the request.
152
- # E.g. : resource.delete(:query => {:param1 => "value1"})
153
- # <tt>:headers</tt>:: a hash of HTTP headers to pass along the request.
154
- # E.g. : resource.delete(:headers => {:accept => 'application/json'})
89
+
155
90
  def delete(options = {})
156
- options = options.symbolize_keys
157
- raise NotImplementedError, "The DELETE method is not allowed for this resource." unless http_methods.include?('DELETE')
158
- response = session.delete(self.uri, options) # raises an exception if there is an error
159
- stale!
160
- (200..399).include?(response.status)
91
+ if allow?(:delete)
92
+ session.delete(request.uri)
93
+ else
94
+ raise MethodNotAllowed
95
+ end
161
96
  end
162
-
163
-
164
- def stale!; @status = :stale; end
165
- def stale?; @status == :stale; end
166
97
 
167
-
168
- # == Description
169
- # Returns the list of allowed HTTP methods on the resource.
170
- # == Usage
171
- # resource.http_methods
172
- # => ['GET', 'POST']
173
- #
174
- def http_methods
175
- reload if executed_requests['GET'].nil? || executed_requests['GET']['headers'].nil? || executed_requests['GET']['headers'].empty?
176
- (executed_requests['GET']['headers']['Allow'] || "GET").split(/,\s*/)
98
+ def update(*args)
99
+ if allow?(:put)
100
+ payload, options = extract_payload_from_args(args)
101
+ session.put(request.uri, payload, options)
102
+ else
103
+ raise MethodNotAllowed
104
+ end
177
105
  end
178
-
179
- def uri_for(path)
180
- uri.merge(URI.parse(path.to_s))
106
+
107
+ def allow?(method)
108
+ response.allow?(method)
181
109
  end
182
-
183
- def inspect(*args)
184
- @properties.inspect(*args)
110
+
111
+ def inspect
112
+ if media_type.complete?
113
+ properties.inspect
114
+ else
115
+ "{...}"
116
+ end
185
117
  end
186
118
 
187
119
  def pretty_print(pp)
188
- pp.text "#<#{self.class}:0x#{self.object_id.to_s(16)}"
189
- pp.text " uid=#{self['uid'].inspect}" if self.class == Resource
120
+ pp.text signature(false)
190
121
  pp.nest 2 do
191
- pp.breakable
192
- pp.text "@uri="
193
- uri.pretty_print(pp)
194
- if @links.length > 0
122
+ if relationships.length > 0
195
123
  pp.breakable
196
- pp.text "LINKS"
124
+ pp.text "RELATIONSHIPS"
197
125
  pp.nest 2 do
198
- @links.to_a.each_with_index do |(key, value), i|
126
+ pp.breakable
127
+ pp.text "#{relationships.join(", ")}"
128
+ end
129
+ end
130
+ pp.breakable
131
+ if collection?
132
+ # display items
133
+ pp.text "ITEMS (#{offset}..#{offset+length})/#{total}"
134
+ pp.nest 2 do
135
+ self.each do |item|
199
136
  pp.breakable
200
- pp.text "@#{key}=#<#{value.class}:0x#{value.object_id.to_s(16)}>"
201
- pp.text "," if i < @links.length-1
137
+ pp.text item.signature(true)
202
138
  end
203
- end
204
- end
205
- if @properties.length > 0
206
- pp.breakable
139
+ end
140
+ else
207
141
  pp.text "PROPERTIES"
208
142
  pp.nest 2 do
209
- @properties.to_a.each_with_index do |(key, value), i|
143
+ properties.each do |key, value|
210
144
  pp.breakable
211
145
  pp.text "#{key.inspect}=>"
212
146
  value.pretty_print(pp)
213
- pp.text "," if i < @properties.length-1
214
147
  end
215
148
  end
216
149
  end
217
150
  yield pp if block_given?
218
151
  end
219
152
  pp.text ">"
220
- end
221
-
222
- protected
223
- def populate_object(key, value)
224
- case key
225
- when "links"
226
- value.each{|link| define_link(Link.new(link))}
227
- else
228
- case value
229
- when Hash
230
- @properties.store(key, SpecialHash.new.replace(value)) unless @links.has_key?(key)
231
- when Array
232
- @properties.store(key, SpecialArray.new(value))
233
- else
234
- @properties.store(key, value)
235
- end
236
- end
237
153
  end
238
- def define_link(link)
239
- if link.valid?
240
- case link.rel
241
- when 'parent'
242
- @links['parent'] = Resource.new(uri.merge(link.href), session)
243
- when 'collection'
244
- @links[link.title] = Collection.new(uri.merge(link.href), session, :title => link.title)
245
- when 'member'
246
- @links[link.title] = Resource.new(uri.merge(link.href), session, :title => link.title)
247
- when 'self'
248
- # we do nothing
154
+
155
+ def build
156
+ # only build once
157
+ if @associations.empty?
158
+ extend Collection if collection?
159
+
160
+ response.links.each do |link|
161
+ @associations[link.id] = nil
162
+
163
+ self.class.class_eval do
164
+ define_method link.id do |*args|
165
+ @associations[link.id] ||= session.get(link.href, :head => {
166
+ 'Accept' => link.type
167
+ }).load(*args)
168
+ end
169
+ end
170
+
249
171
  end
250
- else
251
- session.logger.warn link.errors.join("\n")
252
172
  end
173
+ self
174
+ end
175
+
176
+ protected
177
+ def extract_payload_from_args(args)
178
+ options = args.extract_options!
179
+ head = options.delete(:headers) || options.delete(:head)
180
+ query = options.delete(:query)
181
+
182
+ payload = args.shift || options
183
+
184
+ options = {
185
+ :head => head, :query => query
186
+ }
187
+
188
+ [payload, options]
253
189
  end
254
190
 
255
- end # class Resource
256
- end # module Restfully
191
+
192
+ end
193
+ end