restfully 0.2.3 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +1 -0
- data/README.rdoc +67 -29
- data/VERSION +1 -1
- data/bin/restfully +68 -46
- data/examples/grid5000.rb +6 -6
- data/lib/restfully.rb +2 -1
- data/lib/restfully/collection.rb +98 -15
- data/lib/restfully/parsing.rb +2 -2
- data/lib/restfully/resource.rb +5 -5
- data/lib/restfully/session.rb +11 -6
- data/restfully.gemspec +3 -4
- data/spec/collection_spec.rb +44 -39
- data/spec/fixtures/grid5000-sites.json +538 -487
- data/spec/http/error_spec.rb +1 -1
- data/spec/http/headers_spec.rb +1 -1
- data/spec/http/request_spec.rb +1 -1
- data/spec/http/response_spec.rb +1 -1
- data/spec/http/rest_client_adapter_spec.rb +1 -1
- data/spec/link_spec.rb +1 -1
- data/spec/parsing_spec.rb +1 -1
- data/spec/resource_spec.rb +3 -3
- data/spec/restfully_spec.rb +1 -1
- data/spec/session_spec.rb +23 -19
- metadata +3 -3
data/LICENSE
CHANGED
data/README.rdoc
CHANGED
@@ -9,7 +9,8 @@ Alpha work.
|
|
9
9
|
|
10
10
|
== Usage
|
11
11
|
=== Command line
|
12
|
-
$
|
12
|
+
$ export RUBYOPT="-rubygems"
|
13
|
+
$ restfully base_uri [root_path] [-u username] [-p password]
|
13
14
|
|
14
15
|
e.g., for the Grid5000 API:
|
15
16
|
$ restfully https://api.grid5000.fr/sid /grid5000 -u username -p password
|
@@ -17,37 +18,74 @@ e.g., for the Grid5000 API:
|
|
17
18
|
If the connection was successful, you should get a prompt. Call the +root+ function to see what are the available resources. <tt>@property</tt> means that you can call the +property+ method on the object. Other properties are available via the <tt>[]</tt> function.
|
18
19
|
e.g.:
|
19
20
|
irb(main):005:0> root
|
20
|
-
=> #<Restfully::Resource:
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
21
|
+
=> #<Restfully::Resource:0x8848de
|
22
|
+
------------ META ------------
|
23
|
+
@uri: #<URI::Generic:0x11091a8 URL:/grid5000>
|
24
|
+
@uid: "grid5000"
|
25
|
+
@type: "grid"
|
26
|
+
@environments: Restfully::Collection
|
27
|
+
@sites: Restfully::Collection
|
28
|
+
@version: Restfully::Resource
|
29
|
+
@versions: Restfully::Collection
|
30
|
+
------------ PROPERTIES ------------
|
31
|
+
"version" => "4fe96b25d2cbfee16abe5a4fb999c82dbafc2ee8">
|
31
32
|
|
32
33
|
irb(main):006:0> root.uri
|
33
|
-
=>
|
34
|
+
=> #<URI::Generic:0x11091a8 URL:/grid5000>
|
34
35
|
|
35
36
|
irb(main):007:0> root.sites
|
36
|
-
=> #<Restfully::Collection:
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
37
|
+
=> #<Restfully::Collection:0x881f6c
|
38
|
+
------------ META ------------
|
39
|
+
@uri: "/grid5000/sites"
|
40
|
+
@offset: 0
|
41
|
+
@total: 9
|
42
|
+
@version: Restfully::Resource
|
43
|
+
@versions: Restfully::Collection
|
44
|
+
------------ PROPERTIES ------------
|
45
|
+
"version" => "4fe96b25d2cbfee16abe5a4fb999c82dbafc2ee8"
|
46
|
+
------------ ITEMS ------------
|
47
|
+
Restfully::Resource
|
48
|
+
Restfully::Resource
|
49
|
+
Restfully::Resource
|
50
|
+
Restfully::Resource
|
51
|
+
Restfully::Resource
|
52
|
+
Restfully::Resource
|
53
|
+
Restfully::Resource
|
54
|
+
Restfully::Resource
|
55
|
+
Restfully::Resource>
|
56
|
+
|
57
|
+
irb(main):008:0> root.sites.by_uid('rennes').clusters.by_uid('paradent').nodes.by_uid('paradent-1').metrics.by_uid('mem_free', 'cpu_idle', 'bytes_in')
|
58
|
+
=> [#<Restfully::Resource:0x4259a
|
59
|
+
------------ META ------------
|
60
|
+
@uri: "/grid5000/sites/rennes/clusters/paradent/nodes/paradent-1/metrics/mem_free"
|
61
|
+
@uid: "mem_free"
|
62
|
+
@type: "metric"
|
63
|
+
@parent: Restfully::Resource
|
64
|
+
@timeseries: Restfully::Resource
|
65
|
+
------------ PROPERTIES ------------
|
66
|
+
"last_update" => -901639018
|
67
|
+
"step" => 15
|
68
|
+
"timeseries" => [{"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>1}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>24}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>168}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>672}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>374, "pdp_per_row"=>5760}]>, #<Restfully::Resource:0x12444
|
69
|
+
------------ META ------------
|
70
|
+
@uri: "/grid5000/sites/rennes/clusters/paradent/nodes/paradent-1/metrics/cpu_idle"
|
71
|
+
@uid: "cpu_idle"
|
72
|
+
@type: "metric"
|
73
|
+
@parent: Restfully::Resource
|
74
|
+
@timeseries: Restfully::Resource
|
75
|
+
------------ PROPERTIES ------------
|
76
|
+
"last_update" => -901639018
|
77
|
+
"step" => 15
|
78
|
+
"timeseries" => [{"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>1}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>24}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>168}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>672}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>374, "pdp_per_row"=>5760}]>, #<Restfully::Resource:0xe4c870
|
79
|
+
------------ META ------------
|
80
|
+
@uri: "/grid5000/sites/rennes/clusters/paradent/nodes/paradent-1/metrics/bytes_in"
|
81
|
+
@uid: "bytes_in"
|
82
|
+
@type: "metric"
|
83
|
+
@parent: Restfully::Resource
|
84
|
+
@timeseries: Restfully::Resource
|
85
|
+
------------ PROPERTIES ------------
|
86
|
+
"last_update" => -901639018
|
87
|
+
"step" => 15
|
88
|
+
"timeseries" => [{"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>1}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>24}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>168}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>672}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>374, "pdp_per_row"=>5760}]>]
|
51
89
|
|
52
90
|
irb(main):009:0> root.version
|
53
91
|
=> #<Restfully::Resource:0x8facf0
|
@@ -90,4 +128,4 @@ See the +examples+ directory for examples.
|
|
90
128
|
|
91
129
|
== Copyright
|
92
130
|
|
93
|
-
Copyright (c) 2009 Cyril Rohr. See LICENSE for details.
|
131
|
+
Copyright (c) 2009 Cyril Rohr, INRIA Rennes - Bretagne Atlantique. See LICENSE for details.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.3.0
|
data/bin/restfully
CHANGED
@@ -9,26 +9,31 @@ require 'logger'
|
|
9
9
|
require 'pp'
|
10
10
|
require 'yaml'
|
11
11
|
|
12
|
-
|
12
|
+
|
13
|
+
logger = Logger.new(STDOUT)
|
14
|
+
logger.level = Logger::WARN
|
15
|
+
@options = {:logger => logger}
|
13
16
|
option_parser = OptionParser.new do |opts|
|
14
17
|
opts.banner = "Usage: restfully base_uri [root_path] [options]"
|
15
18
|
|
16
19
|
opts.on("-u=", "--username=", "Sets the username") do |u|
|
17
|
-
@options[
|
20
|
+
@options[:username] = u
|
18
21
|
end
|
19
22
|
opts.on("-p=", "--password=", "Sets the user password") do |p|
|
20
|
-
@options[
|
23
|
+
@options[:password] = p
|
21
24
|
end
|
22
25
|
opts.on("-c=", "--config=", "Sets the various options based on a custom YAML configuration file") do |v|
|
23
|
-
@options[
|
24
|
-
end
|
25
|
-
opts.on("-v", "--verbose", "Run verbosely") do |v|
|
26
|
-
@options['verbose'] = v
|
26
|
+
@options[:configuration_file] = v
|
27
27
|
end
|
28
28
|
opts.on("--log=", "Outputs log messages to the given file. Defaults to stdout") do |v|
|
29
|
-
|
29
|
+
original_logger_level = logger.level
|
30
|
+
logger = Logger.new(File.expand_path(v))
|
31
|
+
logger.level = original_logger_level
|
32
|
+
@options[:logger] = logger
|
33
|
+
end
|
34
|
+
opts.on("-v", "--verbose", "Run verbosely") do |v|
|
35
|
+
@options[:logger].level = Logger::DEBUG
|
30
36
|
end
|
31
|
-
|
32
37
|
opts.on_tail("-h", "--help", "Show this message") do
|
33
38
|
puts opts
|
34
39
|
exit
|
@@ -38,44 +43,61 @@ end
|
|
38
43
|
|
39
44
|
option_parser.parse!
|
40
45
|
|
41
|
-
@base_uri = ARGV.shift
|
42
|
-
@root_path = ARGV.shift || "/"
|
46
|
+
@options[:base_uri] = ARGV.shift
|
47
|
+
@options[:root_path] = ARGV.shift || "/"
|
43
48
|
|
44
|
-
|
45
|
-
|
46
|
-
@base_uri = config.delete('base_uri') || @base_uri
|
47
|
-
@root_path = config.delete('root_path') || @root_path
|
48
|
-
@options.merge!(config)
|
49
|
+
def session
|
50
|
+
@session ||= Restfully::Session.new(@options)
|
49
51
|
end
|
50
52
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
if (log_file=@options.delete('log'))
|
56
|
-
@logger = Logger.new(File.expand_path(log_file))
|
57
|
-
else
|
58
|
-
@logger = Logger.new(STDOUT)
|
59
|
-
end
|
60
|
-
if @options.delete('verbose')
|
61
|
-
@logger.level = Logger::DEBUG
|
62
|
-
else
|
63
|
-
@logger.level = Logger::WARN
|
64
|
-
end
|
65
|
-
|
66
|
-
def session
|
67
|
-
@session ||= Restfully::Session.new(@base_uri, @options.merge('root_path' => @root_path, 'logger' => @logger))
|
68
|
-
end
|
69
|
-
def root
|
70
|
-
@root ||= Restfully::Resource.new(session.root_path, session).load
|
71
|
-
end
|
72
|
-
|
73
|
-
root # preloads
|
74
|
-
|
75
|
-
require 'irb'
|
76
|
-
require 'irb/completion'
|
77
|
-
|
78
|
-
ARGV.clear
|
79
|
-
IRB.start
|
80
|
-
exit!
|
53
|
+
def root
|
54
|
+
@root ||= session.root.load
|
55
|
+
rescue Restfully::HTTP::Error => e
|
56
|
+
puts "#{e.class.name}: #{e.message}"
|
81
57
|
end
|
58
|
+
|
59
|
+
require 'irb'
|
60
|
+
require 'irb/completion'
|
61
|
+
ARGV.clear
|
62
|
+
IRB.start
|
63
|
+
exit!
|
64
|
+
|
65
|
+
|
66
|
+
# if (config_filename = @options.delete('configuration_file')) && File.exists?(File.expand_path(config_filename))
|
67
|
+
# config = YAML.load_file(File.expand_path(config_filename))
|
68
|
+
# @base_uri = config.delete('base_uri') || @base_uri
|
69
|
+
# @root_path = config.delete('root_path') || @root_path
|
70
|
+
# @options.merge!(config)
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# unless @base_uri
|
74
|
+
# $stderr.puts option_parser.help
|
75
|
+
# exit(-1)
|
76
|
+
# else
|
77
|
+
# if (log_file=@options.delete('log'))
|
78
|
+
# @logger = Logger.new(File.expand_path(log_file))
|
79
|
+
# else
|
80
|
+
# @logger = Logger.new(STDOUT)
|
81
|
+
# end
|
82
|
+
# if @options.delete('verbose')
|
83
|
+
# @logger.level = Logger::DEBUG
|
84
|
+
# else
|
85
|
+
# @logger.level = Logger::WARN
|
86
|
+
# end
|
87
|
+
#
|
88
|
+
# def session
|
89
|
+
# @session ||= Restfully::Session.new(@base_uri, @options.merge('root_path' => @root_path, 'logger' => @logger))
|
90
|
+
# end
|
91
|
+
# def root
|
92
|
+
# @root ||= Restfully::Resource.new(session.root_path, session).load
|
93
|
+
# end
|
94
|
+
#
|
95
|
+
# root # preloads
|
96
|
+
#
|
97
|
+
# require 'irb'
|
98
|
+
# require 'irb/completion'
|
99
|
+
#
|
100
|
+
# ARGV.clear
|
101
|
+
# IRB.start
|
102
|
+
# exit!
|
103
|
+
# end
|
data/examples/grid5000.rb
CHANGED
@@ -10,19 +10,19 @@ logger.level = Logger::INFO
|
|
10
10
|
# Restfully.adapter = Restfully::HTTP::RestClientAdapter
|
11
11
|
# Restfully.adapter = Patron::Session
|
12
12
|
RestClient.log = 'stdout'
|
13
|
-
Restfully::Session.new('
|
13
|
+
Restfully::Session.new(:base_uri => 'http://api.local/sid', :root_path => '/grid5000', :logger => logger) do |grid, session|
|
14
14
|
grid_stats = {'hardware' => {}, 'system' => {}}
|
15
|
-
grid.sites.each do |
|
16
|
-
site_stats = site.status.inject({'hardware' => {}, 'system' => {}}) {|accu,
|
15
|
+
grid.sites.each do |site|
|
16
|
+
site_stats = site.status.inject({'hardware' => {}, 'system' => {}}) {|accu, node_status|
|
17
17
|
accu['hardware'][node_status['hardware_state']] = (accu['hardware'][node_status['hardware_state']] || 0) + 1
|
18
18
|
accu['system'][node_status['system_state']] = (accu['system'][node_status['system_state']] || 0) + 1
|
19
19
|
accu
|
20
|
-
}
|
20
|
+
} rescue {'hardware' => {}, 'system' => {}}
|
21
21
|
grid_stats['hardware'].merge!(site_stats['hardware']) { |key,oldval,newval| oldval+newval }
|
22
22
|
grid_stats['system'].merge!(site_stats['system']) { |key,oldval,newval| oldval+newval }
|
23
|
-
p [
|
23
|
+
p [site.uid, site_stats]
|
24
24
|
end
|
25
25
|
p [:total, grid_stats]
|
26
26
|
puts "Getting status of a few nodes in rennes:"
|
27
|
-
pp grid.sites
|
27
|
+
pp grid.sites.by_uid('rennes').status(:query => {:only => ['paradent-1', 'paradent-10', 'paramount-3']})
|
28
28
|
end
|
data/lib/restfully.rb
CHANGED
data/lib/restfully/collection.rb
CHANGED
@@ -1,41 +1,87 @@
|
|
1
1
|
require 'delegate'
|
2
2
|
|
3
3
|
module Restfully
|
4
|
-
|
4
|
+
# Resource and Collection classes should clearly include a common module to deal with links, attributes, associations and loading
|
5
|
+
class Collection < DelegateClass(Array)
|
5
6
|
|
6
|
-
attr_reader :state, :raw, :uri, :session, :title
|
7
|
+
attr_reader :state, :raw, :uri, :session, :title, :offset, :total
|
7
8
|
|
8
9
|
def initialize(uri, session, options = {})
|
9
10
|
options = options.symbolize_keys
|
10
11
|
@uri = uri
|
11
12
|
@title = options[:title]
|
12
13
|
@session = session
|
13
|
-
@raw = options[:raw]
|
14
14
|
@state = :unloaded
|
15
|
-
@items =
|
15
|
+
@items = []
|
16
|
+
@indexes = {}
|
17
|
+
@associations = {}
|
18
|
+
@attributes = {}
|
16
19
|
super(@items)
|
17
20
|
end
|
18
21
|
|
19
22
|
def loaded?; @state == :loaded; end
|
20
23
|
|
24
|
+
def method_missing(method, *args)
|
25
|
+
load
|
26
|
+
if association = @associations[method.to_s]
|
27
|
+
session.logger.debug "Loading association #{method}, args=#{args.inspect}"
|
28
|
+
association.load(*args)
|
29
|
+
elsif method.to_s =~ /^by_(.+)$/
|
30
|
+
key = $1
|
31
|
+
@indexes[method.to_s] ||= @items.inject({}) { |accu, item|
|
32
|
+
accu[item.has_key?(key) ? item[key] : item.send(key.to_sym)] = item
|
33
|
+
accu
|
34
|
+
}
|
35
|
+
if args.empty?
|
36
|
+
@indexes[method.to_s]
|
37
|
+
elsif args.length == 1
|
38
|
+
@indexes[method.to_s][args.first]
|
39
|
+
else
|
40
|
+
@indexes[method.to_s].values_at(*args)
|
41
|
+
end
|
42
|
+
else
|
43
|
+
super(method, *args)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
21
47
|
def load(options = {})
|
22
48
|
options = options.symbolize_keys
|
23
49
|
force_reload = !!options.delete(:reload) || options.has_key?(:query)
|
24
|
-
if loaded? && !force_reload
|
50
|
+
if loaded? && !force_reload && options[:raw].nil?
|
25
51
|
self
|
26
|
-
else
|
52
|
+
else
|
53
|
+
@raw = options[:raw]
|
54
|
+
@associations.clear
|
55
|
+
@attributes.clear
|
27
56
|
@items.clear
|
28
57
|
if raw.nil? || force_reload
|
29
58
|
response = session.get(uri, options)
|
30
59
|
@raw = response.body
|
31
60
|
end
|
32
61
|
raw.each do |key, value|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
62
|
+
case key
|
63
|
+
when "total", "offset"
|
64
|
+
instance_variable_set("@#{key}".to_sym, value)
|
65
|
+
when "links"
|
66
|
+
value.each{|link| define_link(Link.new(link))}
|
67
|
+
when "items"
|
68
|
+
value.each do |item|
|
69
|
+
self_link = (item['links'] || []).map{|link| Link.new(link)}.detect{|link| link.self?}
|
70
|
+
if self_link && self_link.valid?
|
71
|
+
@items << Resource.new(self_link.href, session).load(:raw => item)
|
72
|
+
else
|
73
|
+
session.logger.warn "Resource #{key} does not have a 'self' link. skipped."
|
74
|
+
end
|
75
|
+
end
|
37
76
|
else
|
38
|
-
|
77
|
+
case value
|
78
|
+
when Hash
|
79
|
+
@attributes.store(key, SpecialHash.new.replace(value)) unless @associations.has_key?(key)
|
80
|
+
when Array
|
81
|
+
@attributes.store(key, SpecialArray.new(value))
|
82
|
+
else
|
83
|
+
@attributes.store(key, value)
|
84
|
+
end
|
39
85
|
end
|
40
86
|
end
|
41
87
|
@state = :loaded
|
@@ -43,21 +89,58 @@ module Restfully
|
|
43
89
|
end
|
44
90
|
end
|
45
91
|
|
92
|
+
def respond_to?(method, *args)
|
93
|
+
@associations.has_key?(method.to_s) || super(method, *args)
|
94
|
+
end
|
95
|
+
|
46
96
|
def inspect(options = {:space => "\t"})
|
47
97
|
output = "#<#{self.class}:0x#{self.object_id.to_s(16)}"
|
48
98
|
if loaded?
|
49
99
|
output += "\n#{options[:space]}------------ META ------------"
|
50
100
|
output += "\n#{options[:space]}@uri: #{uri.inspect}"
|
51
|
-
|
101
|
+
output += "\n#{options[:space]}@offset: #{offset.inspect}"
|
102
|
+
output += "\n#{options[:space]}@total: #{total.inspect}"
|
103
|
+
@associations.each do |title, assoc|
|
104
|
+
output += "\n#{options[:space]}@#{title}: #{assoc.class.name}"
|
105
|
+
end
|
106
|
+
unless @attributes.empty?
|
52
107
|
output += "\n#{options[:space]}------------ PROPERTIES ------------"
|
53
|
-
@
|
54
|
-
output += "\n#{options[:space]}#{key.inspect} => #{value.
|
108
|
+
@attributes.each do |key, value|
|
109
|
+
output += "\n#{options[:space]}#{key.inspect} => #{value.inspect}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
unless @items.empty?
|
113
|
+
output += "\n#{options[:space]}------------ ITEMS ------------"
|
114
|
+
@items.each do |value|
|
115
|
+
output += "\n#{options[:space]}#{value.class.name}"
|
55
116
|
end
|
56
117
|
end
|
57
118
|
end
|
58
119
|
output += ">"
|
59
120
|
end
|
60
121
|
|
61
|
-
|
122
|
+
protected
|
123
|
+
def define_link(link)
|
124
|
+
if link.valid?
|
125
|
+
case link.rel
|
126
|
+
when 'parent'
|
127
|
+
@associations['parent'] = Resource.new(link.href, session)
|
128
|
+
when 'collection'
|
129
|
+
raw_included = link.resolved? ? raw[link.title] : nil
|
130
|
+
@associations[link.title] = Collection.new(link.href, session,
|
131
|
+
:raw => raw_included,
|
132
|
+
:title => link.title)
|
133
|
+
when 'member'
|
134
|
+
raw_included = link.resolved? ? raw[link.title] : nil
|
135
|
+
@associations[link.title] = Resource.new(link.href, session,
|
136
|
+
:title => link.title,
|
137
|
+
:raw => raw_included)
|
138
|
+
when 'self'
|
139
|
+
# we do nothing
|
140
|
+
end
|
141
|
+
else
|
142
|
+
session.logger.warn link.errors.join("\n")
|
143
|
+
end
|
144
|
+
end
|
62
145
|
end
|
63
146
|
end
|