crohr-restfully 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,22 @@
1
+ = restfully
2
+
3
+ An attempt at dynamically providing "clever" wrappers on top of RESTful APIs that follow the HATEOAS principle. For it to work, the API must follow certain conventions (to be explained later).
4
+
5
+ Alpha work, will probably only work against my API of reference :-)
6
+
7
+ == Note on Patches/Pull Requests
8
+
9
+ * Fork the project.
10
+ * Make your feature addition or bug fix.
11
+ * Add tests for it. This is important so I don't break it in a future version unintentionally.
12
+ * 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).
13
+ * Send me a pull request. Bonus points for topic branches.
14
+
15
+ == Testing
16
+
17
+ * rake spec, or
18
+ * run autotest in the project directory.
19
+
20
+ == Copyright
21
+
22
+ Copyright (c) 2009 Cyril Rohr. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,45 @@
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/cryx/restfully"
12
+ gem.authors = ["Cyril Rohr"]
13
+ gem.add_dependency "rest-client"
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
19
+ end
20
+
21
+ require 'spec/rake/spectask'
22
+ Spec::Rake::SpecTask.new(:spec) do |spec|
23
+ spec.libs << 'lib' << 'spec'
24
+ spec.spec_files = FileList['spec/**/*_spec.rb']
25
+ end
26
+
27
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
28
+ spec.libs << 'lib' << 'spec'
29
+ spec.pattern = 'spec/**/*_spec.rb'
30
+ spec.rcov = true
31
+ end
32
+
33
+
34
+
35
+
36
+ task :default => :spec
37
+
38
+ begin
39
+ require 'yard'
40
+ YARD::Rake::YardocTask.new
41
+ rescue LoadError
42
+ task :yardoc do
43
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
44
+ end
45
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0
@@ -0,0 +1,30 @@
1
+ require 'rubygems'
2
+ require 'pp'
3
+
4
+ require File.dirname(__FILE__)+'/../lib/restfully'
5
+
6
+ logger = Logger.new(STDOUT)
7
+ logger.level = Logger::INFO
8
+
9
+ # Restfully.adapter = Restfully::HTTP::RestClientAdapter
10
+ # Restfully.adapter = Patron::Session
11
+ RestClient.log = 'stdout'
12
+ Restfully::Session.new('https://localhost:3443/sid', 'root' => '/versions/current', 'logger' => logger) do |grid, session|
13
+ grid_stats = {'hardware' => {}, 'system' => {}}
14
+ grid.sites.each do |site_uid, site|
15
+ site_stats = site.status.inject({'hardware' => {}, 'system' => {}}) {|accu, (node_uid, node_status)|
16
+ accu['hardware'][node_status['hardware_state']] = (accu['hardware'][node_status['hardware_state']] || 0) + 1
17
+ accu['system'][node_status['system_state']] = (accu['system'][node_status['system_state']] || 0) + 1
18
+ accu
19
+ }
20
+ grid_stats['hardware'].merge!(site_stats['hardware']) { |key,oldval,newval| oldval+newval }
21
+ grid_stats['system'].merge!(site_stats['system']) { |key,oldval,newval| oldval+newval }
22
+ p [site_uid, site_stats]
23
+ end
24
+ p [:total, grid_stats]
25
+ puts "Getting status of a few nodes in rennes:"
26
+ pp grid.sites['rennes'].status(:query => {:only => ['paradent-1', 'paradent-10', 'paramount-3']})
27
+
28
+ # TODO: Auto discovery of allowed HTTP methods: create raises an error if not available, otherwise POSTs the given object (and auto-select the content-type)
29
+ # grid.sites["rennes"].jobs.create({:walltime => 120, :whatever => ''})
30
+ end
@@ -0,0 +1,51 @@
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
+ @uri = uri
10
+ @title = options['title']
11
+ @session = session
12
+ @raw = options['raw']
13
+ @state = :unloaded
14
+ @items = {}
15
+ super(@items)
16
+ end
17
+
18
+ def loaded?; @state == :loaded; end
19
+
20
+ def load(options = {})
21
+ options = options.symbolize_keys
22
+ force_reload = options.delete(:reload) || false
23
+ path = uri
24
+ if options.has_key?(:query)
25
+ path, query_string = uri.split("?")
26
+ query_string ||= ""
27
+ query_string.concat(options.delete(:query).to_params)
28
+ path = "#{path}?#{query_string}"
29
+ force_reload = true
30
+ end
31
+ if loaded? && force_reload == false
32
+ self
33
+ else
34
+ @raw = session.get(path, options) if raw.nil? || force_reload
35
+ raw.each do |key, value|
36
+ next if key == 'links'
37
+ self_link = (value['links'] || []).map{|link| Link.new(link)}.detect{|link| link.self?}
38
+ if self_link && self_link.valid?
39
+ @items.store(key, Resource.new(self_link.href, session, 'raw' => value).load)
40
+ else
41
+ session.logger.warn "Resource #{key} does not have a 'self' link. skipped."
42
+ end
43
+ end
44
+ @state = :loaded
45
+ self
46
+ end
47
+ end
48
+
49
+
50
+ end
51
+ end
@@ -0,0 +1,35 @@
1
+ module Restfully
2
+ class Link
3
+
4
+ VALID_RELATIONSHIPS = %w{member parent collection self}
5
+ RELATIONSHIPS_REQUIRING_TITLE = %w{collection member}
6
+
7
+ attr_reader :rel, :title, :href, :errors
8
+
9
+ def initialize(attributes = {})
10
+ @rel = attributes['rel']
11
+ @title = attributes['title']
12
+ @href = attributes['href']
13
+ @resolvable = attributes['resolvable'] || false
14
+ @resolved = attributes['resolved'] || false
15
+ end
16
+
17
+ def resolvable?; @resolvable == true; end
18
+ def resolved?; @resolved == true; end
19
+ def self?; @rel == 'self'; end
20
+
21
+ def valid?
22
+ @errors = []
23
+ if href.nil? || href.empty?
24
+ errors << "href cannot be empty."
25
+ end
26
+ unless VALID_RELATIONSHIPS.include?(rel)
27
+ errors << "#{rel} is not a valid link relationship."
28
+ end
29
+ if (!title || title.empty?) && RELATIONSHIPS_REQUIRING_TITLE.include?(rel)
30
+ errors << "#{rel} #{href} has no title."
31
+ end
32
+ errors.empty?
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,31 @@
1
+ require 'json'
2
+
3
+ module Restfully
4
+
5
+ class ParserNotFound < RestfullyError; end
6
+
7
+ module Parsing
8
+ def parse(object, options = {})
9
+ content_type = options[:content_type]
10
+ content_type ||= object.headers[:content_type] if object.respond_to?(:headers)
11
+ case content_type
12
+ when /^application\/json/i
13
+ JSON.parse(object)
14
+ else
15
+ raise ParserNotFound, [object, content_type]
16
+ end
17
+ end
18
+
19
+ def dump(object, options = {})
20
+ content_type = options[:content_type]
21
+ content_type ||= object.headers[:content_type] if object.respond_to?(:headers)
22
+ case content_type
23
+ when /^application\/json/i
24
+ JSON.dump(object)
25
+ else
26
+ raise ParserNotFound, [object, content_type]
27
+ end
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,97 @@
1
+ require 'delegate'
2
+
3
+ module Restfully
4
+
5
+ # Suppose that the load method has been called on the resource before trying to access its attributes or associations
6
+
7
+ class Resource < DelegateClass(Hash)
8
+ undef :type
9
+ attr_reader :uri, :session, :state, :raw, :uid, :associations
10
+
11
+ def initialize(uri, session, options = {})
12
+ @uri = uri
13
+ @session = session
14
+ @parent = options['parent']
15
+ @raw = options['raw']
16
+ @state = :unloaded
17
+ @attributes = {}
18
+ super(@attributes)
19
+ @associations = {}
20
+ end
21
+
22
+ def loaded?; @state == :loaded; end
23
+
24
+ def method_missing(method, *args)
25
+ if association = @associations[method.to_s]
26
+ session.logger.debug "loading association #{method} with #{args.inspect}"
27
+ association.load(*args)
28
+ else
29
+ super(method, *args)
30
+ end
31
+ end
32
+
33
+ def load(options = {})
34
+ options = options.symbolize_keys
35
+ force_reload = options.delete(:reload) || false
36
+ path = uri
37
+ if options.has_key?(:query)
38
+ path, query_string = uri.split("?")
39
+ query_string ||= ""
40
+ query_string.concat(options.delete(:query).to_params)
41
+ path = "#{path}?#{query_string}"
42
+ force_reload = true
43
+ end
44
+ if loaded? && force_reload == false
45
+ self
46
+ else
47
+ @raw = session.get(path, options) if raw.nil? || force_reload
48
+ @associations.clear
49
+ @attributes.clear
50
+ (raw['links'] || []).each{|link| define_link(Link.new(link))}
51
+ raw.each do |key, value|
52
+ case key
53
+ when 'links' then next
54
+ when 'uid' then @uid = value
55
+ else
56
+ case value
57
+ when Hash
58
+ @attributes.store(key, SpecialHash.new.replace(value)) unless @associations.has_key?(key)
59
+ when Array
60
+ @attributes.store(key, SpecialArray.new(value))
61
+ else
62
+ @attributes.store(key, value)
63
+ end
64
+ end
65
+ end
66
+ @state = :loaded
67
+ self
68
+ end
69
+ end
70
+
71
+ def respond_to?(method, *args)
72
+ @associations.has_key?(method.to_s) || super(method, *args)
73
+ end
74
+
75
+ protected
76
+ def define_link(link)
77
+ if link.valid?
78
+ case link.rel
79
+ when 'collection'
80
+ raw_included = link.resolved? ? raw[link.title] : nil
81
+ @associations[link.title] = Collection.new(link.href, session,
82
+ 'raw' => raw_included,
83
+ 'title' => link.title)
84
+ when 'member'
85
+ raw_included = link.resolved? ? raw[link.title] : nil
86
+ @associations[link.title] = Resource.new(link.href, session,
87
+ 'raw' => raw_included)
88
+ when 'self'
89
+ # uri is supposed to be equal to link['href'] for self relations. so we do nothing
90
+ end
91
+ else
92
+ session.logger.warn link.errors.join("\n")
93
+ end
94
+ end
95
+
96
+ end # class Resource
97
+ end # module Restfully
@@ -0,0 +1,59 @@
1
+ require 'uri'
2
+ require 'logger'
3
+ require 'restclient'
4
+
5
+ module Restfully
6
+ class Error < StandardError; end
7
+ class HTTPError < Error; end
8
+ class NullLogger
9
+ def method_missing(method, *args)
10
+ nil
11
+ end
12
+ end
13
+ class Session
14
+ include Parsing
15
+ attr_reader :base_url, :logger, :connection, :root
16
+
17
+ # TODO: use CacheableResource
18
+ def initialize(base_url, options = {})
19
+ @base_url = base_url
20
+ @root = options['root'] || '/'
21
+ @logger = options['logger'] || NullLogger.new
22
+ @user = options['user']
23
+ @password = options['password']
24
+ # TODO: generalize
25
+ # @connection = Patron::Session.new
26
+ # @connection.timeout = 10
27
+ # @connection.base_url = base_url
28
+ # @connection.headers['User-Agent'] = 'restfully'
29
+ # @connection.insecure = true
30
+ # @connection.username = @user
31
+ # @connection.password = @password
32
+ @connection = RestClient::Resource.new(base_url || '', :user => @user, :password => @password)
33
+ yield Resource.new(root, self).load, self if block_given?
34
+ end
35
+
36
+ # TODO: uniformize headers hash (should accept symbols and strings in any capitalization format)
37
+ # TODO: inspect response headers to determine which methods are available
38
+ def get(path, headers = {})
39
+ headers = headers.symbolize_keys
40
+ headers[:accept] ||= 'application/json'
41
+ logger.info "GET #{path} #{headers.inspect}"
42
+ # TODO: should be able to choose HTTP lib to use, so we must provide abstract interface for HTTP handlers
43
+ api = path.empty? ? connection : connection[path]
44
+ response = api.get(headers)
45
+ parse response#, :content_type => response.headers['Content-Type']
46
+ # response = connection.get(path, headers)
47
+ # logger.info response.headers
48
+ # logger.info response.status
49
+ # if (200..300).include?(response.status)
50
+ # parse response.body, :content_type => response.headers['Content-Type']
51
+ # else
52
+ # TODO: better error management ;-)
53
+ # raise HTTPError.new("Error: #{response.status}")
54
+ # end
55
+ end
56
+
57
+ # TODO: add other HTTP methods
58
+ end
59
+ end
@@ -0,0 +1,5 @@
1
+ module Restfully
2
+ # To be used to provide facilities such as hash-like lookups
3
+ class SpecialArray < Array
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Restfully
2
+ # To be used later
3
+ class SpecialHash < Hash
4
+ end
5
+ end
data/lib/restfully.rb ADDED
@@ -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
+ def symbolize_keys
8
+ inject({}) do |options, (key, value)|
9
+ options[(key.to_sym rescue key) || key] = value
10
+ options
11
+ end
12
+ end
13
+
14
+ def to_params
15
+ params = ''
16
+
17
+ each do |k, v|
18
+ if v.is_a?(Array)
19
+ params << "#{k}=#{v.join(",")}&"
20
+ else
21
+ params << "#{k}=#{v.to_s}&"
22
+ end
23
+ end
24
+
25
+ params.chop!
26
+ params
27
+ end
28
+
29
+ end
30
+
31
+ module Restfully
32
+ class RestfullyError < StandardError; end
33
+ end
34
+
35
+ require File.dirname(__FILE__)+'/restfully/parsing'
36
+ require File.dirname(__FILE__)+'/restfully/session'
37
+ require File.dirname(__FILE__)+'/restfully/special_hash'
38
+ require File.dirname(__FILE__)+'/restfully/special_array'
39
+ require File.dirname(__FILE__)+'/restfully/link'
40
+ require File.dirname(__FILE__)+'/restfully/resource'
41
+ require File.dirname(__FILE__)+'/restfully/collection'
@@ -0,0 +1,37 @@
1
+ require File.dirname(__FILE__)+'/spec_helper'
2
+
3
+ include Restfully
4
+ describe Collection do
5
+
6
+ it "should have all methods of a hash" do
7
+ collection = Collection.new("uri", session=mock('session'))
8
+ collection.size.should == 0
9
+ collection.store("rennes", resource = mock(Resource))
10
+ collection.size.should == 1
11
+ collection.should == {'rennes' => resource}
12
+ collection.should respond_to(:each)
13
+ collection.should respond_to(:store)
14
+ collection.should respond_to(:[])
15
+ collection.should respond_to(:length)
16
+ end
17
+
18
+ it "should not load if already loaded and no force_reload" do
19
+ collection = Collection.new("uri", mock("session"))
20
+ collection.should_receive(:loaded?).and_return(true)
21
+ collection.load(:reload => false).should == collection
22
+ end
23
+ it "should load when force_reload is true [already loaded]" do
24
+ collection = Collection.new("uri", session=mock("session", :logger => Logger.new(STDOUT)))
25
+ collection.should_receive(:loaded?).and_return(true)
26
+ session.should_receive(:get).and_return({})
27
+ collection.load(:reload => true).should == collection
28
+ end
29
+ it "should load when force_reload is true [not loaded]" do
30
+ collection = Collection.new("uri", session=mock("session", :logger => Logger.new(STDOUT)))
31
+ collection.should_receive(:loaded?).and_return(false)
32
+ session.should_receive(:get).and_return({})
33
+ collection.load(:reload => true).should == collection
34
+ end
35
+ it "should force reload when query parameters are given"
36
+
37
+ end
data/spec/link_spec.rb ADDED
@@ -0,0 +1,58 @@
1
+ require File.dirname(__FILE__)+'/spec_helper'
2
+
3
+ include Restfully
4
+ describe Link do
5
+
6
+ it "should have a rel reader" do
7
+ link = Link.new
8
+ link.should_not respond_to(:rel=)
9
+ link.should respond_to(:rel)
10
+ end
11
+ it "should have a href reader" do
12
+ link = Link.new
13
+ link.should_not respond_to(:href=)
14
+ link.should respond_to(:href)
15
+ end
16
+ it "should have a title reader" do
17
+ link = Link.new
18
+ link.should_not respond_to(:title=)
19
+ link.should respond_to(:title)
20
+ end
21
+ it "should have a errors reader" do
22
+ link = Link.new
23
+ link.should_not respond_to(:errors=)
24
+ link.should respond_to(:errors)
25
+ end
26
+ it "should respond to valid?" do
27
+ link = Link.new
28
+ link.should respond_to(:valid?)
29
+ end
30
+ it "should respond to resolved?" do
31
+ link = Link.new
32
+ link.should respond_to(:resolved?)
33
+ end
34
+ it "should respond to resolvable?" do
35
+ link = Link.new
36
+ link.should respond_to(:resolvable?)
37
+ end
38
+ it "by default, should not be resolvable" do
39
+ link = Link.new
40
+ link.should_not be_resolvable
41
+ end
42
+ it "by default, should not be resolved" do
43
+ link = Link.new
44
+ link.should_not be_resolved
45
+ end
46
+ it "should not be valid if there is no href" do
47
+ link = Link.new 'rel' => 'collection', 'title' => 'my collection'
48
+ link.should_not be_valid
49
+ end
50
+ it "should not be valid if there is no rel" do
51
+ link = Link.new 'href' => '/', 'title' => 'my collection'
52
+ link.should_not be_valid
53
+ end
54
+ it "should not be valid if the rel is valid but requires a title that is not given" do
55
+ link = Link.new 'rel' => 'collection', 'href' => '/'
56
+ link.should_not be_valid
57
+ end
58
+ end
@@ -0,0 +1,149 @@
1
+ require File.dirname(__FILE__)+'/spec_helper'
2
+
3
+ include Restfully
4
+
5
+ describe Resource do
6
+ before do
7
+ @logger = Logger.new(STDOUT)
8
+ end
9
+
10
+ it "should have all methods of a hash" do
11
+ resource = Resource.new("uri", session=mock('session'))
12
+ resource.size.should == 0
13
+ resource['whatever'] = 'thing'
14
+ resource.size.should == 1
15
+ resource.should == {'whatever' => 'thing'}
16
+ resource.should respond_to(:each)
17
+ resource.should respond_to(:length)
18
+ end
19
+
20
+ describe "accessors" do
21
+ it "should have a reader on the session" do
22
+ resource = Resource.new("uri", session=mock("session"))
23
+ resource.should_not respond_to(:session=)
24
+ resource.session.should == session
25
+ end
26
+ it "should have a reader on the uri" do
27
+ resource = Resource.new("uri", session=mock("session"))
28
+ resource.should_not respond_to(:uri=)
29
+ resource.uri.should == "uri"
30
+ end
31
+ it "should have a reader on the raw property" do
32
+ resource = Resource.new("uri", session=mock("session"), 'raw' => {})
33
+ resource.should_not respond_to(:raw=)
34
+ resource.raw.should == {}
35
+ end
36
+ it "should have a reader on the state property" do
37
+ resource = Resource.new("uri", session=mock("session"))
38
+ resource.should_not respond_to(:state=)
39
+ resource.state.should == :unloaded
40
+ end
41
+ end
42
+
43
+
44
+ describe "loading" do
45
+
46
+ before do
47
+ @raw = {
48
+ 'links' => [
49
+ {'rel' => 'self', 'href' => '/sites/rennes/versions/123'},
50
+ {'rel' => 'member', 'href' => '/versions/123', 'title' => 'grid'},
51
+ {'rel' => 'invalid_rel', 'href' => '/whatever'},
52
+ {'rel' => 'member', 'href' => '/sites/rennes/statuses/current', 'title' => 'status'},
53
+ {'rel' => 'collection', 'href' => '/sites/rennes/versions', 'resolvable' => false, 'title' => 'versions'},
54
+ {'rel' => 'collection', 'href' => '/sites/rennes/clusters/versions/123', 'resolvable' => true, 'resolved' => true, 'title' => 'clusters'},
55
+ {'rel' => 'collection', 'href' => '/sites/rennes/environments/versions/123', 'resolvable' => true, 'resolved' => false, 'title' => 'environments'},
56
+ {'rel' => 'collection', 'href' => '/has/no/title'}
57
+ ],
58
+ 'uid' => 'rennes',
59
+ 'whatever' => 'whatever',
60
+ 'an_array' => [1, 2, 3],
61
+ 'clusters' => {
62
+ '/sites/rennes/clusters/paradent' => {
63
+ 'guid' => '/sites/rennes/clusters/paradent',
64
+ 'links' => [
65
+ {'rel' => 'self', 'href' => '/sites/rennes/clusters/paradent/versions/123'},
66
+ {'rel' => 'parent', 'href' => '/sites/rennes/versions/123'},
67
+ {'rel' => 'collection', 'href' => '/sites/rennes/clusters/paradent/nodes/versions/123', 'title' => 'nodes', 'resolvable' => true, 'resolved' => false},
68
+ {'rel' => 'collection', 'href' => '/sites/rennes/clusters/paradent/versions', 'resolvable' => false, 'title' => 'versions'},
69
+ {'rel' => 'member', 'href' => '/sites/rennes/clusters/paradent/statuses/current', 'title' => 'status'}
70
+ ],
71
+ 'model' => 'XYZ'
72
+ },
73
+ '/sites/rennes/clusters/paramount' => {
74
+ 'guid' => '/sites/rennes/clusters/paramount',
75
+ 'links' => [
76
+ {'rel' => 'self', 'href' => '/sites/rennes/clusters/paramount/versions/123'},
77
+ {'rel' => 'parent', 'href' => '/sites/rennes/versions/123'},
78
+ {'rel' => 'collection', 'href' => '/sites/rennes/clusters/paramount/nodes/versions/123', 'title' => 'nodes', 'resolvable' => true, 'resolved' => false},
79
+ {'rel' => 'collection', 'href' => '/sites/rennes/clusters/paramount/versions', 'resolvable' => false, 'title' => 'versions'},
80
+ {'rel' => 'member', 'href' => '/sites/rennes/clusters/paramount/statuses/current', 'title' => 'status'}
81
+ ],
82
+ 'model' => 'XYZ1b'
83
+ }
84
+ }
85
+ }
86
+ end
87
+ it "should not be loaded in its initial state" do
88
+ resource = Resource.new("uri", mock('session'))
89
+ resource.should_not be_loaded
90
+ end
91
+ it "should get the raw representation of the resource via the session if it doesn't have it" do
92
+ resource = Resource.new("uri", session = mock("session", :logger => Logger.new(STDOUT)))
93
+ resource.stub!(:define_link) # do not define links
94
+ resource.raw.should be_nil
95
+ session.should_receive(:get).with('uri', {}).and_return(@raw)
96
+ resource.load
97
+ end
98
+ it "should correctly define the functions to access simple values" do
99
+ resource = Resource.new("uri", session = mock("session", :get => @raw, :logger => @logger))
100
+ resource.stub!(:define_link) # do not define links
101
+ resource.load
102
+ resource['whatever'].should == 'whatever'
103
+ resource.uri.should == 'uri'
104
+ resource.uid.should == 'rennes'
105
+ resource['an_array'].should be_a(SpecialArray)
106
+ resource['an_array'].should == [1,2,3]
107
+ lambda{resource.clusters}.should raise_error(NoMethodError)
108
+ end
109
+
110
+ it "should correctly define the functions to access links" do
111
+ resource = Resource.new("uri", session = mock("session", :get => @raw, :logger => @logger))
112
+ @logger.should_receive(:warn).with(/collection \/has\/no\/title has no title/)
113
+ @logger.should_receive(:warn).with(/invalid_rel is not a valid link relationship/)
114
+ Collection.should_receive(:new).
115
+ with('/sites/rennes/versions', session, 'raw' => nil, 'title' => 'versions').
116
+ and_return(versions=mock(Collection))
117
+ Collection.should_receive(:new).
118
+ with('/sites/rennes/clusters/versions/123', session, 'raw' => @raw['clusters'], 'title' => 'clusters').
119
+ and_return(clusters=mock(Collection))
120
+ Collection.should_receive(:new).
121
+ with('/sites/rennes/environments/versions/123', session, 'raw' => nil, 'title' => 'environments').
122
+ and_return(environments=mock(Collection))
123
+ Resource.should_receive(:new).
124
+ with('/versions/123', session, 'raw' => nil).
125
+ and_return(parent=mock(Resource))
126
+ Resource.should_receive(:new).
127
+ with('/sites/rennes/statuses/current', session, 'raw' => nil).
128
+ and_return(status=mock(Resource))
129
+ # associations should not exist before loading
130
+ resource.respond_to?(:clusters).should be_false
131
+ # load the resource so that associations get created
132
+ resource.load
133
+ # check that functions exist
134
+ resource.should respond_to(:clusters)
135
+ clusters.should_receive(:load).and_return(clusters)
136
+ resource.clusters.should == clusters
137
+ resource.should respond_to(:environments)
138
+ environments.should_receive(:load).and_return(environments)
139
+ resource.environments.should == environments
140
+ resource.should respond_to(:versions)
141
+ versions.should_receive(:load).and_return(versions)
142
+ resource.versions.should == versions
143
+ resource.should respond_to(:status)
144
+ status.should_receive(:load).and_return(status)
145
+ resource.status.should == status
146
+ resource.uri.should == 'uri' # self link should not override it
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,5 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Restfully do
4
+
5
+ end
@@ -0,0 +1,21 @@
1
+ require File.dirname(__FILE__)+'/spec_helper'
2
+
3
+ include Restfully
4
+ describe Session do
5
+
6
+ it "should have a logger reader" do
7
+ session = Session.new('https://api.grid5000.fr')
8
+ session.should_not respond_to(:logger=)
9
+ session.should respond_to(:logger)
10
+ end
11
+ it "should have a base_url reader" do
12
+ session = Session.new('https://api.grid5000.fr')
13
+ session.should_not respond_to(:base_url=)
14
+ session.base_url.should == 'https://api.grid5000.fr'
15
+ end
16
+
17
+ it "should log to NullLogger by default" do
18
+ NullLogger.should_receive(:new)
19
+ session = Session.new('https://api.grid5000.fr')
20
+ end
21
+ end
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+ require 'restfully'
5
+ require 'spec'
6
+ require 'spec/autorun'
7
+
8
+
9
+ Spec::Runner.configure do |config|
10
+
11
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: crohr-restfully
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Cyril Rohr
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-08-31 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rest-client
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description: Experimental code for auto-generation of wrappers on top of RESTful APIs that follow HATEOAS principles and provide OPTIONS methods and/or Allow headers.
26
+ email: cyril.rohr@gmail.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - LICENSE
33
+ - README.rdoc
34
+ files:
35
+ - .document
36
+ - .gitignore
37
+ - LICENSE
38
+ - README.rdoc
39
+ - Rakefile
40
+ - VERSION
41
+ - examples/grid5000.rb
42
+ - lib/restfully.rb
43
+ - lib/restfully/collection.rb
44
+ - lib/restfully/link.rb
45
+ - lib/restfully/parsing.rb
46
+ - lib/restfully/resource.rb
47
+ - lib/restfully/session.rb
48
+ - lib/restfully/special_array.rb
49
+ - lib/restfully/special_hash.rb
50
+ - spec/collection_spec.rb
51
+ - spec/link_spec.rb
52
+ - spec/resource_spec.rb
53
+ - spec/restfully_spec.rb
54
+ - spec/session_spec.rb
55
+ - spec/spec_helper.rb
56
+ has_rdoc: true
57
+ homepage: http://github.com/cryx/restfully
58
+ post_install_message:
59
+ rdoc_options:
60
+ - --charset=UTF-8
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ version:
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: "0"
74
+ version:
75
+ requirements: []
76
+
77
+ rubyforge_project:
78
+ rubygems_version: 1.2.0
79
+ signing_key:
80
+ specification_version: 3
81
+ summary: Experimental code for auto-generation of wrappers on top of RESTful APIs that follow some specific conventions.
82
+ test_files:
83
+ - spec/collection_spec.rb
84
+ - spec/link_spec.rb
85
+ - spec/resource_spec.rb
86
+ - spec/restfully_spec.rb
87
+ - spec/session_spec.rb
88
+ - spec/spec_helper.rb
89
+ - examples/grid5000.rb