restfully 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/.document +5 -0
  2. data/.gitignore +5 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +90 -0
  5. data/Rakefile +75 -0
  6. data/TODO.rdoc +3 -0
  7. data/VERSION +1 -0
  8. data/bin/restfully +80 -0
  9. data/examples/grid5000.rb +28 -0
  10. data/lib/restfully.rb +19 -0
  11. data/lib/restfully/collection.rb +63 -0
  12. data/lib/restfully/error.rb +4 -0
  13. data/lib/restfully/extensions.rb +41 -0
  14. data/lib/restfully/http.rb +9 -0
  15. data/lib/restfully/http/adapters/abstract_adapter.rb +30 -0
  16. data/lib/restfully/http/adapters/patron_adapter.rb +16 -0
  17. data/lib/restfully/http/adapters/rest_client_adapter.rb +31 -0
  18. data/lib/restfully/http/error.rb +20 -0
  19. data/lib/restfully/http/headers.rb +20 -0
  20. data/lib/restfully/http/request.rb +24 -0
  21. data/lib/restfully/http/response.rb +19 -0
  22. data/lib/restfully/link.rb +35 -0
  23. data/lib/restfully/parsing.rb +31 -0
  24. data/lib/restfully/resource.rb +117 -0
  25. data/lib/restfully/session.rb +61 -0
  26. data/lib/restfully/special_array.rb +5 -0
  27. data/lib/restfully/special_hash.rb +5 -0
  28. data/restfully.gemspec +99 -0
  29. data/spec/collection_spec.rb +93 -0
  30. data/spec/fixtures/grid5000-sites.json +489 -0
  31. data/spec/http/error_spec.rb +18 -0
  32. data/spec/http/headers_spec.rb +17 -0
  33. data/spec/http/request_spec.rb +45 -0
  34. data/spec/http/response_spec.rb +15 -0
  35. data/spec/http/rest_client_adapter_spec.rb +33 -0
  36. data/spec/link_spec.rb +58 -0
  37. data/spec/parsing_spec.rb +25 -0
  38. data/spec/resource_spec.rb +198 -0
  39. data/spec/restfully_spec.rb +13 -0
  40. data/spec/session_spec.rb +105 -0
  41. data/spec/spec_helper.rb +13 -0
  42. metadata +117 -0
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
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,4 @@
1
+
2
+ module Restfully
3
+ class Error < StandardError; end
4
+ 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