restfully 0.7.0.pre → 0.7.1.pre

Sign up to get free protection for your applications and to get access to all the features.
data/bin/restfully CHANGED
@@ -9,6 +9,13 @@ require 'optparse'
9
9
  require 'logger'
10
10
  require 'pp'
11
11
 
12
+ # Behaviour of pp in IRB is different on ruby1.9:
13
+ # * pp(object) returns object#inspect.
14
+ # * we prefer the behaviour of ruby1.8 where pp returns nil.
15
+ alias :old_pp :pp
16
+ def pp(*args)
17
+ old_pp(*args); nil
18
+ end
12
19
 
13
20
  logger = Logger.new(STDERR)
14
21
  logger.level = Logger::WARN
@@ -67,13 +74,12 @@ end
67
74
  if given_uri = ARGV.shift
68
75
  @options["uri"] = given_uri
69
76
  end
70
- # p @options
77
+
71
78
  # Compatibility with restfully < 0.6
72
79
  @options["uri"] ||= @options.delete("base_uri")
73
80
 
74
81
  @session = Restfully::Session.new(@options)
75
82
 
76
-
77
83
  def session
78
84
  @session
79
85
  end
@@ -45,6 +45,7 @@ module Restfully
45
45
  end
46
46
  block.call @items[hash]
47
47
  end
48
+ self
48
49
  end
49
50
 
50
51
  def length
@@ -79,12 +80,7 @@ module Restfully
79
80
  # resource.uri == uri_to_find
80
81
  # }
81
82
  # end
82
-
83
- protected
84
- def reload_if_empty(resource)
85
- resource.reload if resource && !resource.media_type.complete?
86
- resource
87
- end
83
+
88
84
 
89
85
  end
90
86
 
@@ -49,10 +49,36 @@ module Restfully
49
49
  end
50
50
  end
51
51
 
52
+ def inspect
53
+ [
54
+ "#{method.to_s.upcase} #{uri.to_s}",
55
+ head.map{|(k,v)| "#{k}: #{v}"}.join("\n"),
56
+ body
57
+ ].compact.join("\n")
58
+ end
59
+
52
60
  def no_cache?
53
61
  head['Cache-Control'] && head['Cache-Control'].include?('no-cache')
54
62
  end
55
63
 
64
+ def no_cache!
65
+ @forced_cache = true
66
+ head['Cache-Control'] = 'no-cache'
67
+ end
68
+
69
+ def forced_cache?
70
+ !!@forced_cache
71
+ end
72
+
73
+ def remove_no_cache!
74
+ @forced_cache = false
75
+ if head['Cache-Control']
76
+ head['Cache-Control'] = head['Cache-Control'].split(/\s+,\s+/).reject{|v|
77
+ v =~ /no-cache/i
78
+ }.join(",")
79
+ end
80
+ end
81
+
56
82
  protected
57
83
  def build_head(options = {})
58
84
  sanitize_head(
@@ -67,7 +93,7 @@ module Restfully
67
93
  type = MediaType.find('application/x-www-form-urlencoded')
68
94
  options[:head]['Content-Type'] = type.default_type
69
95
  end
70
- type.serialize(options[:body], :uri => options[:uri])
96
+ type.serialize(options[:body], :serialization => options[:serialization])
71
97
  else
72
98
  nil
73
99
  end
@@ -17,19 +17,30 @@ module Restfully
17
17
  io = io.read
18
18
  end
19
19
  xml = XML::Document.string(io.to_s)
20
- load_xml(xml.root).merge(HIDDEN_TYPE_KEY => xml.root.name)
20
+ h = load_xml(xml.root)
21
+ h[HIDDEN_TYPE_KEY] = if h['items']
22
+ if xml.root.attributes["href"]
23
+ xml.root.attributes["href"].
24
+ split("/").last.gsub(/s$/,'')+"_collection"
25
+ elsif h['items'].length > 0
26
+ h['items'][0][HIDDEN_TYPE_KEY]
27
+ else
28
+ nil
29
+ end
30
+ else
31
+ xml.root.name
32
+ end
33
+ h
21
34
  end
22
35
 
23
36
 
24
37
  def dump(object, opts = {})
25
- root_name = if object[HIDDEN_TYPE_KEY]
38
+ root_name = if opts[:serialization]
39
+ opts[:serialization][HIDDEN_TYPE_KEY].gsub(/\_collection$/,'')
40
+ elsif object[HIDDEN_TYPE_KEY]
26
41
  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
42
  end
43
+ fail "Can't infer a root element name for object: #{object.inspect}" if root_name.nil?
33
44
  xml = XML::Document.new
34
45
  xml.root = XML::Node.new(root_name)
35
46
  xml.root["xmlns"] = NS
@@ -41,6 +52,8 @@ module Restfully
41
52
 
42
53
  def load_xml(element)
43
54
  h = {}
55
+
56
+
44
57
  element.each_element do |e|
45
58
  next if e.empty?
46
59
  if e.name == 'items' && element.name == 'collection'
@@ -66,7 +79,8 @@ module Restfully
66
79
  h
67
80
  end
68
81
 
69
- # We use <tt>HIDDEN_TYPE_KEY</tt> property to keep track of the root name.
82
+ # We use <tt>HIDDEN_TYPE_KEY</tt> property to keep track of the root
83
+ # name.
70
84
  def load_xml_element(element, h)
71
85
  if element.attributes? && href = element.attributes.find{|attr|
72
86
  attr.name == "href"
@@ -75,7 +89,20 @@ module Restfully
75
89
  #build_resource(href.value))
76
90
  else
77
91
  if element.children.any?(&:element?)
78
- single_or_array(h, element.name, load_xml(element))
92
+ # This includes ["computes", "networks", "storages"] section
93
+ # of an experiment.
94
+ if element.children.select(&:element?).map{|c|
95
+ c.name
96
+ }.all?{|n|
97
+ "#{n}s" == element.name
98
+ }
99
+ element.each_element {|e|
100
+ e.name = element.name
101
+ load_xml_element(e, h)
102
+ }
103
+ else
104
+ single_or_array(h, element.name, load_xml(element))
105
+ end
79
106
  else
80
107
  value = element.content.strip
81
108
  unless value.empty?
@@ -92,7 +119,7 @@ module Restfully
92
119
  else
93
120
  h[key] = [h[key], value]
94
121
  end
95
- elsif ["disk", "nic", "link"].include?(key)
122
+ elsif ["disk", "nic", "link", "computes", "networks", "storages"].include?(key)
96
123
  h[key] = [value]
97
124
  else
98
125
  h[key] = value
@@ -8,6 +8,8 @@ module Restfully
8
8
  class Resource
9
9
  attr_reader :response, :request, :session
10
10
 
11
+ HIDDEN_PROPERTIES_REGEXP = /^\_\_(.+)\_\_$/
12
+
11
13
  def initialize(session, response, request)
12
14
  @session = session
13
15
  @response = response
@@ -23,6 +25,7 @@ module Restfully
23
25
  # resource["uid"]
24
26
  # => "rennes"
25
27
  def [](key)
28
+ reload_if_empty(self)
26
29
  media_type.property(key)
27
30
  end
28
31
 
@@ -53,6 +56,7 @@ module Restfully
53
56
  # Send a GET request only if given a different set of options
54
57
  if @request.update!(options) || @request.no_cache?
55
58
  @response = session.execute(@request)
59
+ @request.remove_no_cache! if @request.forced_cache?
56
60
  if session.process(@response, @request)
57
61
  @associations.clear
58
62
  else
@@ -64,22 +68,29 @@ module Restfully
64
68
  end
65
69
 
66
70
  def relationships
67
- @associations.keys
71
+ response.links.map(&:id).sort
68
72
  end
69
73
 
70
74
  def properties
71
75
  media_type.property.reject{|k,v|
72
76
  # do not return keys used for internal use
73
- k.to_s =~ /^\_\_(.+)\_\_$/
77
+ k.to_s =~ HIDDEN_PROPERTIES_REGEXP
74
78
  }
75
79
  end
76
80
 
81
+ # For the following methods, maybe it's better to always go through the
82
+ # cache instead of explicitly saying @request.no_cache! (and update the
83
+ # #load method accordingly)
84
+
85
+ # Force reloading of the request
77
86
  def reload
78
- load(:head => {'Cache-Control' => 'no-cache'})
87
+ @request.no_cache!
88
+ load
79
89
  end
80
90
 
81
91
  def submit(*args)
82
92
  if allow?(:post)
93
+ @request.no_cache!
83
94
  payload, options = extract_payload_from_args(args)
84
95
  session.post(request.uri, payload, options)
85
96
  else
@@ -89,6 +100,7 @@ module Restfully
89
100
 
90
101
  def delete(options = {})
91
102
  if allow?(:delete)
103
+ @request.no_cache!
92
104
  session.delete(request.uri)
93
105
  else
94
106
  raise MethodNotAllowed
@@ -97,6 +109,7 @@ module Restfully
97
109
 
98
110
  def update(*args)
99
111
  if allow?(:put)
112
+ @request.no_cache!
100
113
  payload, options = extract_payload_from_args(args)
101
114
  session.put(request.uri, payload, options)
102
115
  else
@@ -150,26 +163,26 @@ module Restfully
150
163
  yield pp if block_given?
151
164
  end
152
165
  pp.text ">"
166
+ nil
153
167
  end
154
168
 
169
+
170
+
155
171
  def build
172
+ metaclass = class << self; self; end
156
173
  # only build once
157
- if @associations.empty?
174
+ # if @associations.empty?
158
175
  extend Collection if collection?
159
176
 
160
177
  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
178
+ metaclass.send(:define_method, link.id.to_sym) do |*args|
179
+ session.get(link.href, :head => {
180
+ 'Accept' => link.type
181
+ }).load(*args)
169
182
  end
170
183
 
171
184
  end
172
- end
185
+ # end
173
186
  self
174
187
  end
175
188
 
@@ -182,12 +195,18 @@ module Restfully
182
195
  payload = args.shift || options
183
196
 
184
197
  options = {
185
- :head => head, :query => query
198
+ :head => head, :query => query,
199
+ :serialization => media_type.property.reject{|k,v|
200
+ k !~ HIDDEN_PROPERTIES_REGEXP
201
+ }
186
202
  }
187
203
 
188
204
  [payload, options]
189
205
  end
190
206
 
191
-
207
+ def reload_if_empty(resource)
208
+ resource.reload if resource && !resource.media_type.complete?
209
+ resource
210
+ end
192
211
  end
193
212
  end
@@ -115,6 +115,7 @@ module Restfully
115
115
  :headers => request.head
116
116
  )
117
117
 
118
+ logger.debug request.inspect
118
119
  code, head, body = resource.send(request.method, request.body || {})
119
120
 
120
121
  response = Restfully::HTTP::Response.new(self, code, head, body)
@@ -1,3 +1,3 @@
1
1
  module Restfully
2
- VERSION = "0.7.0.pre"
2
+ VERSION = "0.7.1.pre"
3
3
  end
@@ -1,4 +1,6 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
- <collection xmlns="http://api.bonfire-project.eu/doc/schemas/occi">
3
- <items/>
2
+ <collection xmlns="http://api.bonfire-project.eu/doc/schemas/occi" href="/experiments/12/networks">
3
+ <items offset="0" total="0">
4
+ </items>
5
+ <link href="/experiments/12" rel="parent" type="application/vnd.bonfire+xml"/>
4
6
  </collection>
@@ -0,0 +1,23 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <experiment href="/experiments/12" xmlns="http://api.bonfire-project.eu/doc/schemas/occi">
3
+ <id>12</id>
4
+ <description>description expe1</description>
5
+ <name>expe1</name>
6
+ <walltime>2011-04-28T16:16:34Z</walltime>
7
+ <user_id>crohr</user_id>
8
+ <status>running</status>
9
+ <networks>
10
+ </networks>
11
+ <computes>
12
+ <compute href="/locations/fr-inria/computes/76" name="compute expe1"/>
13
+ <compute href="/locations/uk-epcc/computes/47" name="compute2 expe1"/>
14
+ <compute href="/locations/uk-epcc/computes/48" name="compute2 expe1"/>
15
+ </computes>
16
+ <storages>
17
+ <storage href="/locations/uk-epcc/storages/44" name="data"/>
18
+ </storages>
19
+ <link rel="parent" href="/"/>
20
+ <link rel="storages" href="/experiments/12/storages"/>
21
+ <link rel="networks" href="/experiments/12/networks"/>
22
+ <link rel="computes" href="/experiments/12/computes"/>
23
+ </experiment>
@@ -38,6 +38,7 @@ describe Restfully::Collection do
38
38
  items = []
39
39
  @resource.load.each{|i| items << i}
40
40
  items.all?{|i| i.kind_of?(Restfully::Resource)}.should be_true
41
+ pp items
41
42
  items[0].relationships.map(&:to_s).sort.should == ["parent", "self"]
42
43
  items[0]['uid'].should == 376505
43
44
  end
@@ -92,6 +92,15 @@ describe Restfully::HTTP::Request do
92
92
  should == 'application/json'
93
93
  request.body.should == @options[:body]
94
94
  end
95
+
96
+ it "should correctly build the request [body as Hash, content-type=xml]" do
97
+ Restfully::MediaType.register Restfully::MediaType::ApplicationVndBonfireXml
98
+ @options[:body] = {"name" => "whatever"}
99
+ @options[:head][:content_type] = 'application/vnd.bonfire+xml'
100
+ @options[:serialization] = {"__type__" => "network"}
101
+ request = Restfully::HTTP::Request.new(@session, :post, "/path", @options)
102
+ request.body.should == "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<network xmlns=\"http://api.bonfire-project.eu/doc/schemas/occi\">\n <name>whatever</name>\n</network>\n"
103
+ end
95
104
  end
96
105
 
97
106
  end
@@ -28,11 +28,18 @@ describe Restfully::MediaType::ApplicationVndBonfireXml do
28
28
  )
29
29
  media.property.should == {"name"=>"Private LAN", "public"=>"YES", "id"=>"29", "__type__"=>"network", "link"=>[{"href"=>"/locations/de-hlrs/networks/29", "rel"=>"self"}]}
30
30
  end
31
+
32
+ it "should build the Hash from the XML doc [resource] 3" do
33
+ media = Restfully::MediaType::ApplicationVndBonfireXml.new(
34
+ fixture("bonfire-experiment.xml"), @session
35
+ )
36
+ media.property.should == {"name"=>"expe1", "walltime"=>"2011-04-28T16:16:34Z", "id"=>"12", "__type__"=>"experiment", "user_id"=>"crohr", "storages"=>[{"name"=>"data", "href"=>"/locations/uk-epcc/storages/44"}], "description"=>"description expe1", "computes"=>[{"name"=>"compute expe1", "href"=>"/locations/fr-inria/computes/76"}, {"name"=>"compute2 expe1", "href"=>"/locations/uk-epcc/computes/47"}, {"name"=>"compute2 expe1", "href"=>"/locations/uk-epcc/computes/48"}], "link"=>[{"href"=>"/", "rel"=>"parent"}, {"href"=>"/experiments/12/storages", "rel"=>"storages"}, {"href"=>"/experiments/12/networks", "rel"=>"networks"}, {"href"=>"/experiments/12/computes", "rel"=>"computes"}, {"href"=>"/experiments/12", "rel"=>"self"}], "status"=>"running"}
37
+ end
31
38
 
32
39
  it "should build the Hash from the XML doc [collection]" do
33
40
  media = Restfully::MediaType::ApplicationVndBonfireXml.new(@collection, @session)
34
41
  media.should be_collection
35
- media.property.should == {"items"=>[{"address"=>"10.0.0.1", "name"=>"Network1", "size"=>"C", "__type__"=>"network", "description"=>"Network1 description", "link"=>[{"href"=>"/locations/fr-inria", "rel"=>"location"}, {"href"=>"/locations/fr-inria/networks/1", "rel"=>"self"}], "visibility"=>"public"}, {"address"=>"10.0.0.1", "name"=>"Network2", "size"=>"C", "__type__"=>"network", "description"=>"Network2 description", "link"=>[{"href"=>"/locations/fr-inria", "rel"=>"location"}, {"href"=>"/locations/fr-inria/networks/2", "rel"=>"self"}], "visibility"=>"private"}], "total"=>2, "offset"=>0, "__type__"=>"collection", "link"=>[{"href"=>"/locations/fr-inria/networks?limit=20", "rel"=>"top", "type"=>"application/vnd.bonfire+xml"}, {"href"=>"/locations/fr-inria", "rel"=>"parent", "type"=>"application/vnd.bonfire+xml"}, {"href"=>"/locations/fr-inria/networks", "rel"=>"self"}]}
42
+ media.property.should == {"items"=>[{"address"=>"10.0.0.1", "name"=>"Network1", "size"=>"C", "__type__"=>"network", "description"=>"Network1 description", "link"=>[{"href"=>"/locations/fr-inria", "rel"=>"location"}, {"href"=>"/locations/fr-inria/networks/1", "rel"=>"self"}], "visibility"=>"public"}, {"address"=>"10.0.0.1", "name"=>"Network2", "size"=>"C", "__type__"=>"network", "description"=>"Network2 description", "link"=>[{"href"=>"/locations/fr-inria", "rel"=>"location"}, {"href"=>"/locations/fr-inria/networks/2", "rel"=>"self"}], "visibility"=>"private"}], "total"=>2, "offset"=>0, "__type__"=>"network_collection", "link"=>[{"href"=>"/locations/fr-inria/networks?limit=20", "rel"=>"top", "type"=>"application/vnd.bonfire+xml"}, {"href"=>"/locations/fr-inria", "rel"=>"parent", "type"=>"application/vnd.bonfire+xml"}, {"href"=>"/locations/fr-inria/networks", "rel"=>"self"}]}
36
43
  end
37
44
 
38
45
  it "should build the Hash from the XML doc [empty collection]" do
@@ -145,7 +152,7 @@ describe Restfully::MediaType::ApplicationVndBonfireXml do
145
152
  it "should correctly serialize a resource" do
146
153
  serialized = Restfully::MediaType::ApplicationVndBonfireXml.serialize(
147
154
  @expected_compute_hash,
148
- :uri => Addressable::URI.parse('http://path/to/computes')
155
+ :serialization => {"__type__" => "compute"}
149
156
  )
150
157
  serialized.should == "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<compute xmlns=\"http://api.bonfire-project.eu/doc/schemas/occi\">\n <startup href=\"file:///path/to/startup-script/sh\"/>\n <name>Compute name</name>\n <instance_type>small</instance_type>\n <link href=\"/locations/fr-inria\" rel=\"location\" type=\"application/vnd.bonfire+xml\"/>\n <link href=\"/locations/uk-epcc/computes/1\" rel=\"self\"/>\n <context>\n <bonfire_credentials>crohr:p4ssw0rd</bonfire_credentials>\n <monitoring_ip>123.123.123.2</monitoring_ip>\n </context>\n <nic>\n <device>eth0</device>\n <mac>AA:AA:AA:AA</mac>\n <network href=\"/locations/fr-inria/networks/1\"/>\n <ip>123.123.123.123</ip>\n </nic>\n <nic>\n <device>eth1</device>\n <mac>BB:BB:BB:BB</mac>\n <network href=\"/locations/fr-inria/networks/2\"/>\n <ip>123.123.124.2</ip>\n </nic>\n <description>Compute description</description>\n <disk>\n <type>OS</type>\n <storage href=\"/locations/fr-inria/storages/1\"/>\n <target>sda</target>\n </disk>\n <disk>\n <type>CDROM</type>\n <storage href=\"/locations/fr-inria/storages/2\"/>\n <target>sdc</target>\n </disk>\n <state>ACTIVE</state>\n</compute>\n"
151
158
  end
@@ -56,7 +56,8 @@ describe Restfully::Resource do
56
56
  @resource.uri,
57
57
  'some payload',
58
58
  :head => {'Content-Type' => 'text/plain'},
59
- :query => {:k1 => 'v1'}
59
+ :query => {:k1 => 'v1'},
60
+ :serialization => {}
60
61
  )
61
62
  @resource.submit(
62
63
  'some payload',
@@ -70,7 +71,8 @@ describe Restfully::Resource do
70
71
  @resource.uri,
71
72
  {:key => 'value'},
72
73
  :head => {'Content-Type' => 'text/plain'},
73
- :query => {:k1 => 'v1'}
74
+ :query => {:k1 => 'v1'},
75
+ :serialization => {}
74
76
  )
75
77
  @resource.submit(
76
78
  :key => 'value',
@@ -78,10 +80,37 @@ describe Restfully::Resource do
78
80
  :query => {:k1 => 'v1'}
79
81
  )
80
82
  end
83
+ it "should pass hidden properties as serialization parameters" do
84
+ Restfully::MediaType.register Restfully::MediaType::ApplicationVndBonfireXml
85
+ @request = Restfully::HTTP::Request.new(
86
+ @session, :get, "/locations/de-hlrs/networks/29",
87
+ :head => {'Accept' => 'application/vnd.bonfire+xml'}
88
+ )
89
+ @response = Restfully::HTTP::Response.new(
90
+ @session, 200, {
91
+ 'Content-Type' => 'application/vnd.bonfire+xml; charset=utf-8',
92
+ 'Allow' => 'GET'
93
+ }, fixture('bonfire-network-existing.xml')
94
+ )
95
+ @resource = Restfully::Resource.new(@session, @response, @request).load
96
+ @resource.should_receive(:allow?).with(:put).and_return(true)
97
+ @session.should_receive(:put).with(
98
+ @resource.uri,
99
+ {:key => 'value'},
100
+ :head => {'Content-Type' => 'text/plain'},
101
+ :query => {:k1 => 'v1'},
102
+ :serialization => {"__type__"=>"network"}
103
+ )
104
+ @resource.update(
105
+ :key => 'value',
106
+ :headers => {'Content-Type' => 'text/plain'},
107
+ :query => {:k1 => 'v1'}
108
+ )
109
+ end
81
110
 
82
111
  it "should reload the resource" do
112
+ @request.should_receive(:no_cache!)
83
113
  @resource.should_receive(:load).
84
- with(:head => {'Cache-Control' => 'no-cache'}).
85
114
  and_return(@resource)
86
115
  @resource.reload.should == @resource
87
116
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: restfully
3
3
  version: !ruby/object:Gem::Version
4
- hash: 961916028
4
+ hash: 961916024
5
5
  prerelease: 6
6
6
  segments:
7
7
  - 0
8
8
  - 7
9
- - 0
9
+ - 1
10
10
  - pre
11
- version: 0.7.0.pre
11
+ version: 0.7.1.pre
12
12
  platform: ruby
13
13
  authors:
14
14
  - Cyril Rohr
@@ -213,6 +213,7 @@ files:
213
213
  - spec/fixtures/bonfire-compute-existing.xml
214
214
  - spec/fixtures/bonfire-empty-collection.xml
215
215
  - spec/fixtures/bonfire-experiment-collection.xml
216
+ - spec/fixtures/bonfire-experiment.xml
216
217
  - spec/fixtures/bonfire-network-collection.xml
217
218
  - spec/fixtures/bonfire-network-existing.xml
218
219
  - spec/fixtures/bonfire-root.xml
@@ -272,6 +273,7 @@ test_files:
272
273
  - spec/fixtures/bonfire-compute-existing.xml
273
274
  - spec/fixtures/bonfire-empty-collection.xml
274
275
  - spec/fixtures/bonfire-experiment-collection.xml
276
+ - spec/fixtures/bonfire-experiment.xml
275
277
  - spec/fixtures/bonfire-network-collection.xml
276
278
  - spec/fixtures/bonfire-network-existing.xml
277
279
  - spec/fixtures/bonfire-root.xml