restfully 0.6.3 → 0.7.0.pre

Sign up to get free protection for your applications and to get access to all the features.
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