restfully 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +5 -0
- data/LICENSE +20 -0
- data/README.rdoc +90 -0
- data/Rakefile +75 -0
- data/TODO.rdoc +3 -0
- data/VERSION +1 -0
- data/bin/restfully +80 -0
- data/examples/grid5000.rb +28 -0
- data/lib/restfully.rb +19 -0
- data/lib/restfully/collection.rb +63 -0
- data/lib/restfully/error.rb +4 -0
- data/lib/restfully/extensions.rb +41 -0
- data/lib/restfully/http.rb +9 -0
- data/lib/restfully/http/adapters/abstract_adapter.rb +30 -0
- data/lib/restfully/http/adapters/patron_adapter.rb +16 -0
- data/lib/restfully/http/adapters/rest_client_adapter.rb +31 -0
- data/lib/restfully/http/error.rb +20 -0
- data/lib/restfully/http/headers.rb +20 -0
- data/lib/restfully/http/request.rb +24 -0
- data/lib/restfully/http/response.rb +19 -0
- data/lib/restfully/link.rb +35 -0
- data/lib/restfully/parsing.rb +31 -0
- data/lib/restfully/resource.rb +117 -0
- data/lib/restfully/session.rb +61 -0
- data/lib/restfully/special_array.rb +5 -0
- data/lib/restfully/special_hash.rb +5 -0
- data/restfully.gemspec +99 -0
- data/spec/collection_spec.rb +93 -0
- data/spec/fixtures/grid5000-sites.json +489 -0
- data/spec/http/error_spec.rb +18 -0
- data/spec/http/headers_spec.rb +17 -0
- data/spec/http/request_spec.rb +45 -0
- data/spec/http/response_spec.rb +15 -0
- data/spec/http/rest_client_adapter_spec.rb +33 -0
- data/spec/link_spec.rb +58 -0
- data/spec/parsing_spec.rb +25 -0
- data/spec/resource_spec.rb +198 -0
- data/spec/restfully_spec.rb +13 -0
- data/spec/session_spec.rb +105 -0
- data/spec/spec_helper.rb +13 -0
- metadata +117 -0
data/.document
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Cyril Rohr
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
= restfully
|
2
|
+
|
3
|
+
An attempt at dynamically providing "clever" wrappers on top of RESTful APIs that follow the principle of Hyperlinks As The Engine Of Application State (HATEOAS). For it to work, the API must follow certain conventions (to be explained later).
|
4
|
+
|
5
|
+
Alpha work.
|
6
|
+
|
7
|
+
== Installation
|
8
|
+
$ gem install restfully
|
9
|
+
|
10
|
+
== Usage
|
11
|
+
=== Command line
|
12
|
+
$ resfully api_base_uri relative_root_uri [-u username] [-p password]
|
13
|
+
|
14
|
+
e.g., for the Grid5000 API:
|
15
|
+
$ restfully https://api.grid5000.fr/sid /grid5000 -u username -p password
|
16
|
+
|
17
|
+
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
|
+
e.g.:
|
19
|
+
irb(main):005:0> root
|
20
|
+
=> #<Restfully::Resource:0x90bdd4
|
21
|
+
------------ META ------------
|
22
|
+
@uri: "/grid5000"
|
23
|
+
@uid: "grid5000"
|
24
|
+
@type: "grid"
|
25
|
+
@environments: Restfully::Collection
|
26
|
+
@sites: Restfully::Collection
|
27
|
+
@version: Restfully::Resource
|
28
|
+
@versions: Restfully::Collection
|
29
|
+
------------ PROPERTIES ------------
|
30
|
+
"version" => "ee1f8111c8835496a9e4a6f7c9491c0238ee9827">
|
31
|
+
irb(main):006:0> root.uri
|
32
|
+
=> "/grid5000"
|
33
|
+
irb(main):007:0> root.sites
|
34
|
+
=> #<Restfully::Collection:0x8faaf2
|
35
|
+
------------ META ------------
|
36
|
+
@uri: "/grid5000/sites"
|
37
|
+
------------ PROPERTIES ------------
|
38
|
+
"lille" => Restfully::Resource
|
39
|
+
"grenoble" => Restfully::Resource
|
40
|
+
"toulouse" => Restfully::Resource
|
41
|
+
"sophia" => Restfully::Resource
|
42
|
+
"rennes" => Restfully::Resource
|
43
|
+
"lyon" => Restfully::Resource
|
44
|
+
"nancy" => Restfully::Resource
|
45
|
+
"bordeaux" => Restfully::Resource
|
46
|
+
"orsay" => Restfully::Resource>
|
47
|
+
irb(main):008:0> root['version']
|
48
|
+
=> "ee1f8111c8835496a9e4a6f7c9491c0238ee9827"
|
49
|
+
irb(main):009:0> root.version
|
50
|
+
=> #<Restfully::Resource:0x8facf0
|
51
|
+
------------ META ------------
|
52
|
+
@uri: "/grid5000/versions/ee1f8111c8835496a9e4a6f7c9491c0238ee9827"
|
53
|
+
@uid: "ee1f8111c8835496a9e4a6f7c9491c0238ee9827"
|
54
|
+
@type: "version"
|
55
|
+
@parent: Restfully::Resource
|
56
|
+
------------ PROPERTIES ------------
|
57
|
+
"author" => "Cyril Constantin <>"
|
58
|
+
"date" => "Fri, 11 Sep 2009 14:32:48 GMT"
|
59
|
+
"message" => "[environments] update of environments on sites">
|
60
|
+
|
61
|
+
You may also prefer to use a configuration file to avoid entering the command line options:
|
62
|
+
$ echo '
|
63
|
+
base_uri: https://api.grid5000.fr/sid
|
64
|
+
relative_root_uri: /grid5000
|
65
|
+
username: MYLOGIN
|
66
|
+
password: MYPASSWORD
|
67
|
+
' > ~/somewhere/api.grid5000.fr.yml
|
68
|
+
|
69
|
+
And then:
|
70
|
+
$ restfully -c ~/somewhere/api.grid5000.fr.yml
|
71
|
+
|
72
|
+
=== As a library
|
73
|
+
See the +examples+ directory for examples.
|
74
|
+
|
75
|
+
== Note on Patches/Pull Requests
|
76
|
+
|
77
|
+
* Fork the project.
|
78
|
+
* Make your feature addition or bug fix.
|
79
|
+
* Add tests for it. This is important so I don't break it in a future version unintentionally.
|
80
|
+
* Commit, do not mess with rakefile, version, or history (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull).
|
81
|
+
* Send me a pull request. Bonus points for topic branches.
|
82
|
+
|
83
|
+
== Testing
|
84
|
+
|
85
|
+
* rake spec, or
|
86
|
+
* run autotest in the project directory.
|
87
|
+
|
88
|
+
== Copyright
|
89
|
+
|
90
|
+
Copyright (c) 2009 Cyril Rohr. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "restfully"
|
8
|
+
gem.summary = %Q{Experimental code for auto-generation of wrappers on top of RESTful APIs that follow some specific conventions.}
|
9
|
+
gem.description = %Q{Experimental code for auto-generation of wrappers on top of RESTful APIs that follow HATEOAS principles and provide OPTIONS methods and/or Allow headers.}
|
10
|
+
gem.email = "cyril.rohr@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/crohr/restfully"
|
12
|
+
gem.authors = ["Cyril Rohr"]
|
13
|
+
gem.add_dependency "rest-client", '>= 1.0'
|
14
|
+
gem.rubyforge_project = 'restfully'
|
15
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
16
|
+
end
|
17
|
+
|
18
|
+
rescue LoadError
|
19
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
20
|
+
end
|
21
|
+
|
22
|
+
require 'spec/rake/spectask'
|
23
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
24
|
+
spec.libs << 'lib' << 'spec'
|
25
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
26
|
+
end
|
27
|
+
|
28
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
29
|
+
spec.libs << 'lib' << 'spec'
|
30
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
31
|
+
spec.rcov = true
|
32
|
+
end
|
33
|
+
|
34
|
+
# These are new tasks
|
35
|
+
begin
|
36
|
+
require 'rake/contrib/sshpublisher'
|
37
|
+
namespace :rubyforge do
|
38
|
+
|
39
|
+
desc "Release gem and RDoc documentation to RubyForge"
|
40
|
+
task :release => ["rubyforge:release:gem", "rubyforge:release:docs"]
|
41
|
+
|
42
|
+
namespace :release do
|
43
|
+
desc "Publish RDoc to RubyForge."
|
44
|
+
task :docs => [:rdoc] do
|
45
|
+
config = YAML.load(
|
46
|
+
File.read(File.expand_path('~/.rubyforge/user-config.yml'))
|
47
|
+
)
|
48
|
+
|
49
|
+
host = "#{config['username']}@rubyforge.org"
|
50
|
+
remote_dir = "/var/www/gforge-projects/the-perfect-gem/"
|
51
|
+
local_dir = 'rdoc'
|
52
|
+
|
53
|
+
Rake::SshDirPublisher.new(host, remote_dir, local_dir).upload
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
rescue LoadError
|
58
|
+
puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured."
|
59
|
+
end
|
60
|
+
|
61
|
+
Jeweler::RubyforgeTasks.new do |rubyforge|
|
62
|
+
rubyforge.doc_task = "rdoc"
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
task :default => :spec
|
67
|
+
|
68
|
+
require 'rake/rdoctask'
|
69
|
+
Rake::RDocTask.new do |rdoc|
|
70
|
+
rdoc.rdoc_dir = 'rdoc'
|
71
|
+
rdoc.title = 'restfully'
|
72
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
73
|
+
rdoc.rdoc_files.include('README*')
|
74
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
75
|
+
end
|
data/TODO.rdoc
ADDED
@@ -0,0 +1,3 @@
|
|
1
|
+
* Detects methods allowed on a resource using the Allow HTTP header, and generate corresponding wrappers.
|
2
|
+
* On a 406, detects accepted content based on a (custom) HTTP header, and retry (if possible) with another Accept header for which a parser is available.
|
3
|
+
* Investigate the possibility of using a :include => {} when loading resources or collection so that Restfully prefetches the included associations (using threads or events).
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.1
|
data/bin/restfully
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# The command line Restfully client
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
5
|
+
|
6
|
+
require 'restfully'
|
7
|
+
require 'optparse'
|
8
|
+
require 'logger'
|
9
|
+
require 'pp'
|
10
|
+
|
11
|
+
@options = {}
|
12
|
+
option_parser = OptionParser.new do |opts|
|
13
|
+
opts.banner = "Usage: restfully base_uri [root_path] [options]"
|
14
|
+
|
15
|
+
opts.on("-u=", "--username=", "Sets the username") do |u|
|
16
|
+
@options['username'] = u
|
17
|
+
end
|
18
|
+
opts.on("-p=", "--password=", "Sets the user password") do |p|
|
19
|
+
@options['password'] = p
|
20
|
+
end
|
21
|
+
opts.on("-c=", "--config=", "Sets the various options based on a custom YAML configuration file") do |v|
|
22
|
+
@options['configuration_file'] = v
|
23
|
+
end
|
24
|
+
opts.on("-v", "--verbose", "Run verbosely") do |v|
|
25
|
+
@options['verbose'] = v
|
26
|
+
end
|
27
|
+
opts.on("--log=", "Outputs log messages to the given file. Defaults to stdout") do |v|
|
28
|
+
@options['log'] = v
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
32
|
+
puts opts
|
33
|
+
exit
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
option_parser.parse!
|
39
|
+
|
40
|
+
@base_uri = ARGV.shift
|
41
|
+
@root_path = ARGV.shift || "/"
|
42
|
+
|
43
|
+
if (config_filename = @options.delete('configuration_file')) && File.exists?(File.expand_path(config_filename))
|
44
|
+
config = YAML.load_file(File.expand_path(config_filename))
|
45
|
+
@base_uri = config.delete('base_uri') || @base_uri
|
46
|
+
@root_path = config.delete('root_path') || @root_path
|
47
|
+
@options.merge!(config)
|
48
|
+
end
|
49
|
+
|
50
|
+
unless @base_uri
|
51
|
+
$stderr.puts option_parser.help
|
52
|
+
exit(-1)
|
53
|
+
else
|
54
|
+
if (log_file=@options.delete('log'))
|
55
|
+
@logger = Logger.new(File.expand_path(log_file))
|
56
|
+
else
|
57
|
+
@logger = Logger.new(STDOUT)
|
58
|
+
end
|
59
|
+
if @options.delete('verbose')
|
60
|
+
@logger.level = Logger::DEBUG
|
61
|
+
else
|
62
|
+
@logger.level = Logger::WARN
|
63
|
+
end
|
64
|
+
|
65
|
+
def session
|
66
|
+
@session ||= Restfully::Session.new(@base_uri, @options.merge('root_path' => @root_path, 'logger' => @logger))
|
67
|
+
end
|
68
|
+
def root
|
69
|
+
@root ||= Restfully::Resource.new(session.root_path, session).load
|
70
|
+
end
|
71
|
+
|
72
|
+
root # preloads
|
73
|
+
|
74
|
+
require 'irb'
|
75
|
+
require 'irb/completion'
|
76
|
+
|
77
|
+
ARGV.clear
|
78
|
+
IRB.start
|
79
|
+
exit!
|
80
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
require 'pp'
|
4
|
+
|
5
|
+
require File.dirname(__FILE__)+'/../lib/restfully'
|
6
|
+
|
7
|
+
logger = Logger.new(STDOUT)
|
8
|
+
logger.level = Logger::INFO
|
9
|
+
|
10
|
+
# Restfully.adapter = Restfully::HTTP::RestClientAdapter
|
11
|
+
# Restfully.adapter = Patron::Session
|
12
|
+
RestClient.log = 'stdout'
|
13
|
+
Restfully::Session.new('https://localhost:3443/sid', 'root_path' => '/grid5000', 'logger' => logger) do |grid, session|
|
14
|
+
grid_stats = {'hardware' => {}, 'system' => {}}
|
15
|
+
grid.sites.each do |site_uid, site|
|
16
|
+
site_stats = site.status.inject({'hardware' => {}, 'system' => {}}) {|accu, (node_uid, node_status)|
|
17
|
+
accu['hardware'][node_status['hardware_state']] = (accu['hardware'][node_status['hardware_state']] || 0) + 1
|
18
|
+
accu['system'][node_status['system_state']] = (accu['system'][node_status['system_state']] || 0) + 1
|
19
|
+
accu
|
20
|
+
}
|
21
|
+
grid_stats['hardware'].merge!(site_stats['hardware']) { |key,oldval,newval| oldval+newval }
|
22
|
+
grid_stats['system'].merge!(site_stats['system']) { |key,oldval,newval| oldval+newval }
|
23
|
+
p [site_uid, site_stats]
|
24
|
+
end
|
25
|
+
p [:total, grid_stats]
|
26
|
+
puts "Getting status of a few nodes in rennes:"
|
27
|
+
pp grid.sites['rennes'].status(:query => {:only => ['paradent-1', 'paradent-10', 'paramount-3']})
|
28
|
+
end
|
data/lib/restfully.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'restfully/error'
|
2
|
+
require 'restfully/parsing'
|
3
|
+
require 'restfully/http'
|
4
|
+
require 'restfully/http/adapters/rest_client_adapter'
|
5
|
+
require 'restfully/extensions'
|
6
|
+
require 'restfully/session'
|
7
|
+
require 'restfully/special_hash'
|
8
|
+
require 'restfully/special_array'
|
9
|
+
require 'restfully/link'
|
10
|
+
require 'restfully/resource'
|
11
|
+
require 'restfully/collection'
|
12
|
+
|
13
|
+
|
14
|
+
module Restfully
|
15
|
+
class << self
|
16
|
+
attr_accessor :adapter
|
17
|
+
end
|
18
|
+
self.adapter = Restfully::HTTP::Adapters::RestClientAdapter
|
19
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module Restfully
|
4
|
+
class Collection < DelegateClass(Hash)
|
5
|
+
|
6
|
+
attr_reader :state, :raw, :uri, :session, :title
|
7
|
+
|
8
|
+
def initialize(uri, session, options = {})
|
9
|
+
options = options.symbolize_keys
|
10
|
+
@uri = uri
|
11
|
+
@title = options[:title]
|
12
|
+
@session = session
|
13
|
+
@raw = options[:raw]
|
14
|
+
@state = :unloaded
|
15
|
+
@items = {}
|
16
|
+
super(@items)
|
17
|
+
end
|
18
|
+
|
19
|
+
def loaded?; @state == :loaded; end
|
20
|
+
|
21
|
+
def load(options = {})
|
22
|
+
options = options.symbolize_keys
|
23
|
+
force_reload = !!options.delete(:reload) || options.has_key?(:query)
|
24
|
+
if loaded? && !force_reload
|
25
|
+
self
|
26
|
+
else
|
27
|
+
@items.clear
|
28
|
+
if raw.nil? || force_reload
|
29
|
+
response = session.get(uri, options)
|
30
|
+
@raw = response.body
|
31
|
+
end
|
32
|
+
raw.each do |key, value|
|
33
|
+
next if key == 'links'
|
34
|
+
self_link = (value['links'] || []).map{|link| Link.new(link)}.detect{|link| link.self?}
|
35
|
+
if self_link && self_link.valid?
|
36
|
+
@items.store(key, Resource.new(self_link.href, session, :raw => value).load)
|
37
|
+
else
|
38
|
+
session.logger.warn "Resource #{key} does not have a 'self' link. skipped."
|
39
|
+
end
|
40
|
+
end
|
41
|
+
@state = :loaded
|
42
|
+
self
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def inspect(options = {:space => "\t"})
|
47
|
+
output = "#<#{self.class}:0x#{self.object_id.to_s(16)}"
|
48
|
+
if loaded?
|
49
|
+
output += "\n#{options[:space]}------------ META ------------"
|
50
|
+
output += "\n#{options[:space]}@uri: #{uri.inspect}"
|
51
|
+
unless @items.empty?
|
52
|
+
output += "\n#{options[:space]}------------ PROPERTIES ------------"
|
53
|
+
@items.each do |key, value|
|
54
|
+
output += "\n#{options[:space]}#{key.inspect} => #{value.class.name}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
output += ">"
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class BasicObject #:nodoc:
|
2
|
+
instance_methods.each { |m| undef_method m unless m =~ /^__|instance_eval/ }
|
3
|
+
end unless defined?(BasicObject)
|
4
|
+
|
5
|
+
# monkey patching:
|
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
|
+
# This is by no means the standard way to transform ruby objects into query parameters
|
25
|
+
# but it is targeted at our own needs
|
26
|
+
def to_params
|
27
|
+
params = ''
|
28
|
+
|
29
|
+
each do |k, v|
|
30
|
+
if v.is_a?(Array)
|
31
|
+
params << "#{k}=#{v.join(",")}&"
|
32
|
+
else
|
33
|
+
params << "#{k}=#{v.to_s}&"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
params.chop!
|
38
|
+
params
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|