restfully 0.6.3 → 0.7.0.pre
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/README.md +166 -0
- data/Rakefile +35 -35
- data/bin/restfully +68 -10
- data/lib/restfully.rb +8 -14
- data/lib/restfully/collection.rb +70 -90
- data/lib/restfully/error.rb +2 -0
- data/lib/restfully/http.rb +3 -3
- data/lib/restfully/http/error.rb +1 -20
- data/lib/restfully/http/helper.rb +49 -0
- data/lib/restfully/http/request.rb +60 -24
- data/lib/restfully/http/response.rb +55 -24
- data/lib/restfully/link.rb +32 -24
- data/lib/restfully/media_type.rb +70 -0
- data/lib/restfully/media_type/abstract_media_type.rb +162 -0
- data/lib/restfully/media_type/application_json.rb +21 -0
- data/lib/restfully/media_type/application_vnd_bonfire_xml.rb +177 -0
- data/lib/restfully/media_type/application_x_www_form_urlencoded.rb +33 -0
- data/lib/restfully/media_type/grid5000.rb +67 -0
- data/lib/restfully/media_type/wildcard.rb +27 -0
- data/lib/restfully/rack.rb +1 -0
- data/lib/restfully/rack/basic_auth.rb +26 -0
- data/lib/restfully/resource.rb +134 -197
- data/lib/restfully/session.rb +127 -70
- data/lib/restfully/version.rb +3 -0
- data/spec/fixtures/bonfire-collection-with-fragments.xml +6 -0
- data/spec/fixtures/bonfire-compute-existing.xml +43 -0
- data/spec/fixtures/bonfire-empty-collection.xml +4 -0
- data/spec/fixtures/bonfire-experiment-collection.xml +51 -0
- data/spec/fixtures/bonfire-network-collection.xml +35 -0
- data/spec/fixtures/bonfire-network-existing.xml +6 -0
- data/spec/fixtures/bonfire-root.xml +5 -0
- data/spec/fixtures/grid5000-rennes-jobs.json +988 -146
- data/spec/fixtures/grid5000-rennes.json +63 -0
- data/spec/restfully/collection_spec.rb +87 -0
- data/spec/restfully/http/helper_spec.rb +18 -0
- data/spec/restfully/http/request_spec.rb +97 -0
- data/spec/restfully/http/response_spec.rb +53 -0
- data/spec/restfully/link_spec.rb +80 -0
- data/spec/restfully/media_type/application_vnd_bonfire_xml_spec.rb +153 -0
- data/spec/restfully/media_type_spec.rb +117 -0
- data/spec/restfully/resource_spec.rb +109 -0
- data/spec/restfully/session_spec.rb +229 -0
- data/spec/spec_helper.rb +10 -9
- metadata +162 -83
- data/.document +0 -5
- data/CHANGELOG +0 -62
- data/README.rdoc +0 -146
- data/TODO.rdoc +0 -3
- data/VERSION +0 -1
- data/examples/grid5000.rb +0 -33
- data/examples/scratch.rb +0 -37
- data/lib/restfully/extensions.rb +0 -34
- data/lib/restfully/http/adapters/abstract_adapter.rb +0 -29
- data/lib/restfully/http/adapters/patron_adapter.rb +0 -16
- data/lib/restfully/http/adapters/rest_client_adapter.rb +0 -75
- data/lib/restfully/http/headers.rb +0 -20
- data/lib/restfully/parsing.rb +0 -66
- data/lib/restfully/special_array.rb +0 -5
- data/lib/restfully/special_hash.rb +0 -5
- data/restfully.gemspec +0 -114
- data/spec/collection_spec.rb +0 -120
- data/spec/fixtures/configuration_file.yml +0 -4
- data/spec/fixtures/grid5000-sites.json +0 -540
- data/spec/http/error_spec.rb +0 -18
- data/spec/http/headers_spec.rb +0 -17
- data/spec/http/request_spec.rb +0 -49
- data/spec/http/response_spec.rb +0 -19
- data/spec/http/rest_client_adapter_spec.rb +0 -35
- data/spec/link_spec.rb +0 -61
- data/spec/parsing_spec.rb +0 -40
- data/spec/resource_spec.rb +0 -320
- data/spec/restfully_spec.rb +0 -16
- data/spec/session_spec.rb +0 -171
data/README.md
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
# restfully
|
2
|
+
|
3
|
+
An attempt at dynamically providing wrappers on top of RESTful APIs that follow the principle of Hyperlinks As The Engine Of Application State (HATEOAS).
|
4
|
+
It does not require to use specific (and often complex) server-side libraries, but a few constraints and conventions must be followed:
|
5
|
+
|
6
|
+
1. Return sensible HTTP status codes;
|
7
|
+
2. Make use of GET, POST, PUT, DELETE HTTP methods;
|
8
|
+
3. Return a Location HTTP header in 201 or 202 responses;
|
9
|
+
4. Return a <tt>links</tt> property in all responses to a GET request, that contains a list of link objects:
|
10
|
+
|
11
|
+
{
|
12
|
+
"property": "value",
|
13
|
+
"links": [
|
14
|
+
{"rel": "self", "href": "uri/to/resource", "type": "application/vnd.whatever+json;level=1,application/json"},
|
15
|
+
{"rel": "parent", "href": "uri/to/parent/resource", "type": "application/json"}
|
16
|
+
{"rel": "collection", "href": "uri/to/collection", "title": "my_collection", "type": "application/json"},
|
17
|
+
{"rel": "member", "href": "uri/to/member", "title": "member_title", "type": "application/json"}
|
18
|
+
]
|
19
|
+
}
|
20
|
+
|
21
|
+
* Adding a <tt>parent</tt> link automatically creates a <tt>#parent</tt> method on the current resource.
|
22
|
+
* Adding a <tt>collection</tt> link automatically creates a <tt>#my_collection</tt> method that will fetch the Collection when called.
|
23
|
+
* Adding a <tt>member</tt> link automatically creates a <tt>#member_title</tt> method that will fetch the Resource when called.
|
24
|
+
|
25
|
+
5. Advertise allowed HTTP methods in the response to GET requests by returning a <tt>Allow</tt> HTTP header containing a comma-separated list of the HTTP methods that can be used on the resource. This will allow the automatic generation of methods to interact with the resource. e.g.: advertising a <tt>POST</tt> method (<tt>Allow: GET, POST</tt>) will result in the creation of a <tt>submit</tt> method on the resource.
|
26
|
+
|
27
|
+
## Installation
|
28
|
+
|
29
|
+
$ gem install restfully
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
### Command line
|
34
|
+
|
35
|
+
$ export RUBYOPT="-rubygems"
|
36
|
+
$ restfully base_uri [-u username] [-p password]
|
37
|
+
|
38
|
+
e.g., for the Grid5000 API:
|
39
|
+
|
40
|
+
$ restfully https://api.grid5000.fr/sid/grid5000 -u username -p password
|
41
|
+
|
42
|
+
If the connection was successful, you should get a prompt. You may enter:
|
43
|
+
|
44
|
+
irb(main):001:0> pp root
|
45
|
+
|
46
|
+
to get back a pretty-printed output of the root resource:
|
47
|
+
|
48
|
+
#<Restfully::Resource:0x91f08c
|
49
|
+
@uri=#<URI::HTTP:0x123e30c URL:http://api.local/sid/grid5000>
|
50
|
+
LINKS
|
51
|
+
@environments=#<Restfully::Collection:0x917666>,
|
52
|
+
@sites=#<Restfully::Collection:0x9170d0>,
|
53
|
+
@version=#<Restfully::Resource:0x91852a>,
|
54
|
+
@versions=#<Restfully::Collection:0x917e68>
|
55
|
+
PROPERTIES
|
56
|
+
"uid"=>"grid5000",
|
57
|
+
"type"=>"grid",
|
58
|
+
"version"=>"4fe96b25d2cbfee16abe5a4fb999c82dbafc2ee8">
|
59
|
+
|
60
|
+
You can see the `LINKS` and `PROPERTIES` headers that respectively indicate what links you can follow from there (by calling `root.link_name`) and what properties are available (by calling `root[property_name]`).
|
61
|
+
|
62
|
+
Let's say you want to access the collection of `sites`, you would enter:
|
63
|
+
|
64
|
+
irb(main):002:0> pp root.sites
|
65
|
+
|
66
|
+
and get back:
|
67
|
+
|
68
|
+
#<Restfully::Collection:0x9170d0
|
69
|
+
@uri=#<URI::HTTP:0x122e128 URL:http://api.local/sid/grid5000/sites>
|
70
|
+
LINKS
|
71
|
+
@version=#<Restfully::Resource:0x8f553e>,
|
72
|
+
@versions=#<Restfully::Collection:0x8f52be>
|
73
|
+
PROPERTIES
|
74
|
+
"total"=>9,
|
75
|
+
"version"=>"4fe96b25d2cbfee16abe5a4fb999c82dbafc2ee8",
|
76
|
+
"offset"=>0
|
77
|
+
ITEMS (0..9)/9
|
78
|
+
#<Restfully::Resource:0x9058bc uid="bordeaux">,
|
79
|
+
#<Restfully::Resource:0x903d0a uid="grenoble">,
|
80
|
+
#<Restfully::Resource:0x901cc6 uid="lille">,
|
81
|
+
#<Restfully::Resource:0x8fff0c uid="lyon">,
|
82
|
+
#<Restfully::Resource:0x8fe288 uid="nancy">,
|
83
|
+
#<Restfully::Resource:0x8fc4a6 uid="orsay">,
|
84
|
+
#<Restfully::Resource:0x8fa782 uid="rennes">,
|
85
|
+
#<Restfully::Resource:0x8f8bb2 uid="sophia">,
|
86
|
+
#<Restfully::Resource:0x8f6c9a uid="toulouse">>
|
87
|
+
|
88
|
+
A Restfully::Collection is a special kind of Resource, which includes the Enumerable module, which means you can call all of its methods on the `Restfully::Collection` object.
|
89
|
+
For example:
|
90
|
+
|
91
|
+
irb(main):003:0> pp root.sites.find{|s| s['uid'] == 'rennes'}
|
92
|
+
#<Restfully::Resource:0x8fa782
|
93
|
+
@uri=#<URI::HTTP:0x11f4e64 URL:http://api.local/sid/grid5000/sites/rennes>
|
94
|
+
LINKS
|
95
|
+
@environments=#<Restfully::Collection:0x8f9ab2>,
|
96
|
+
@parent=#<Restfully::Resource:0x8f981e>,
|
97
|
+
@deployments=#<Restfully::Collection:0x8f935a>,
|
98
|
+
@clusters=#<Restfully::Collection:0x8f9d46>,
|
99
|
+
@version=#<Restfully::Resource:0x8fa354>,
|
100
|
+
@versions=#<Restfully::Collection:0x8fa0b6>,
|
101
|
+
@status=#<Restfully::Collection:0x8f95ee>
|
102
|
+
PROPERTIES
|
103
|
+
"name"=>"Rennes",
|
104
|
+
"latitude"=>48.1,
|
105
|
+
"location"=>"Rennes, France",
|
106
|
+
"security_contact"=>"rennes-staff@lists.grid5000.fr",
|
107
|
+
"uid"=>"rennes",
|
108
|
+
"type"=>"site",
|
109
|
+
"user_support_contact"=>"rennes-staff@lists.grid5000.fr",
|
110
|
+
"version"=>"4fe96b25d2cbfee16abe5a4fb999c82dbafc2ee8",
|
111
|
+
"description"=>"",
|
112
|
+
"longitude"=>-1.6667,
|
113
|
+
"compilation_server"=>false,
|
114
|
+
"email_contact"=>"rennes-staff@lists.grid5000.fr",
|
115
|
+
"web"=>"http://www.irisa.fr",
|
116
|
+
"sys_admin_contact"=>"rennes-staff@lists.grid5000.fr">
|
117
|
+
|
118
|
+
or:
|
119
|
+
|
120
|
+
irb(main):006:0> root.sites.map{|s| s['uid']}.grep(/re/)
|
121
|
+
=> ["grenoble", "rennes"]
|
122
|
+
|
123
|
+
A shortcut is available to find a specific entry in a collection, by entering the searched `uid` as a Symbol:
|
124
|
+
|
125
|
+
irb(main):007:0> root.sites[:rennes]
|
126
|
+
# will find the item whose uid is "rennes"
|
127
|
+
|
128
|
+
For ease of use and better security, you may prefer to use a configuration file to avoid re-entering the options every time you use the client:
|
129
|
+
|
130
|
+
$ echo '
|
131
|
+
base_uri: https://api.grid5000.fr/sid/grid5000
|
132
|
+
username: MYLOGIN
|
133
|
+
password: MYPASSWORD
|
134
|
+
' > ~/.restfully/api.grid5000.fr.yml && chmod 600 ~/.restfully/api.grid5000.fr.yml
|
135
|
+
|
136
|
+
And then:
|
137
|
+
|
138
|
+
$ restfully -c ~/.restfully/api.grid5000.fr.yml
|
139
|
+
|
140
|
+
### As a library
|
141
|
+
See the `examples` directory for examples.
|
142
|
+
|
143
|
+
## Discovering the API capabilities
|
144
|
+
A `Restfully::Resource` (and by extension its child `Restfully::Collection`) has the following methods available for introspection:
|
145
|
+
|
146
|
+
* `links` will return a hash whose keys are the name of the methods that can be called to navigate between resources;
|
147
|
+
* `http_methods` will return an array containing the list of the HTTP methods that are allowed on the resource;
|
148
|
+
|
149
|
+
## Development
|
150
|
+
|
151
|
+
### Testing
|
152
|
+
|
153
|
+
* `rake spec`; or
|
154
|
+
* run `autotest` in the project directory.
|
155
|
+
|
156
|
+
### Note on Patches/Pull Requests
|
157
|
+
|
158
|
+
* Fork the project.
|
159
|
+
* Make your feature addition or bug fix.
|
160
|
+
* Add tests for it. This is important so I don't break it in a future version unintentionally.
|
161
|
+
* 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).
|
162
|
+
* Send me a pull request.
|
163
|
+
|
164
|
+
## Copyright
|
165
|
+
|
166
|
+
Copyright (c) 2009 Cyril Rohr, INRIA Rennes - Bretagne Atlantique. See LICENSE for details.
|
data/Rakefile
CHANGED
@@ -1,47 +1,47 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'rake'
|
3
|
+
require 'rspec/core/rake_task'
|
3
4
|
|
4
|
-
|
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.4'
|
14
|
-
gem.add_dependency "json", '>= 1.2.0'
|
15
|
-
gem.add_dependency "backports"
|
16
|
-
gem.add_development_dependency "webmock"
|
17
|
-
gem.add_development_dependency "rspec"
|
18
|
-
gem.add_development_dependency "json"
|
19
|
-
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
20
|
-
end
|
5
|
+
ROOT_DIR = File.dirname(__FILE__)
|
21
6
|
|
22
|
-
|
23
|
-
|
7
|
+
require 'rake/rdoctask'
|
8
|
+
Rake::RDocTask.new do |rdoc|
|
9
|
+
rdoc.rdoc_dir = 'rdoc'
|
10
|
+
rdoc.title = 'restfully'
|
11
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
12
|
+
rdoc.rdoc_files.include('README*')
|
13
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
24
14
|
end
|
25
15
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
16
|
+
desc "Run Tests"
|
17
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
18
|
+
t.rcov = false
|
19
|
+
t.pattern = 'spec/**/*_spec.rb'
|
30
20
|
end
|
31
21
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
22
|
+
desc "Run Test coverage"
|
23
|
+
RSpec::Core::RakeTask.new(:rcov) do |t|
|
24
|
+
t.rcov = true
|
25
|
+
t.pattern = 'spec/**/*_spec.rb'
|
26
|
+
t.rcov_opts = ['-Ispec', '--exclude', 'spec']
|
36
27
|
end
|
37
28
|
|
38
29
|
task :default => :spec
|
39
30
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
31
|
+
task :clean do
|
32
|
+
Dir.chdir(ROOT_DIR) do
|
33
|
+
rm_f "*.gem"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
task :build => :clean do
|
38
|
+
Dir.chdir(ROOT_DIR) do
|
39
|
+
sh "gem build restfully.gemspec"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
task :install => :build do
|
44
|
+
Dir.chdir(ROOT_DIR) do
|
45
|
+
sh "gem install #{Dir["*.gem"].last}"
|
46
|
+
end
|
47
|
+
end
|
data/bin/restfully
CHANGED
@@ -10,35 +10,39 @@ require 'logger'
|
|
10
10
|
require 'pp'
|
11
11
|
|
12
12
|
|
13
|
-
logger = Logger.new(
|
13
|
+
logger = Logger.new(STDERR)
|
14
14
|
logger.level = Logger::WARN
|
15
|
-
@options = {
|
15
|
+
@options = {"logger" => logger, "require" => []}
|
16
|
+
|
16
17
|
option_parser = OptionParser.new do |opts|
|
17
18
|
opts.banner = <<BANNER
|
18
19
|
* Description
|
19
20
|
Restfully #{Restfully::VERSION} - Access REST APIs effortlessly
|
20
21
|
* Usage
|
21
|
-
restfully [
|
22
|
+
restfully [uri] [options]
|
22
23
|
* Options
|
23
24
|
BANNER
|
24
25
|
|
25
26
|
opts.on("-u=", "--username=", "Sets the username") do |u|
|
26
|
-
@options[
|
27
|
+
@options["username"] = u
|
27
28
|
end
|
28
29
|
opts.on("-p=", "--password=", "Sets the user password") do |p|
|
29
|
-
@options[
|
30
|
+
@options["password"] = p
|
30
31
|
end
|
31
32
|
opts.on("-c=", "--config=", "Sets the various options based on a custom YAML configuration file") do |v|
|
32
|
-
@options[
|
33
|
+
@options["configuration_file"] = v
|
34
|
+
end
|
35
|
+
opts.on("-r=", "--require=", "Require an additional media-type") do |v|
|
36
|
+
@options["require"].push(v)
|
33
37
|
end
|
34
38
|
opts.on("--log=", "Outputs log messages to the given file. Defaults to stdout") do |v|
|
35
39
|
original_logger_level = logger.level
|
36
40
|
logger = Logger.new(File.expand_path(v))
|
37
41
|
logger.level = original_logger_level
|
38
|
-
@options[
|
42
|
+
@options["logger"] = logger
|
39
43
|
end
|
40
44
|
opts.on("-v", "--verbose", "Run verbosely") do |v|
|
41
|
-
@options[
|
45
|
+
@options["logger"].level = Logger::DEBUG
|
42
46
|
end
|
43
47
|
opts.on_tail("-h", "--help", "Show this message") do
|
44
48
|
puts opts
|
@@ -49,10 +53,29 @@ end
|
|
49
53
|
|
50
54
|
option_parser.parse!
|
51
55
|
|
52
|
-
@options[
|
56
|
+
if @options["configuration_file"]
|
57
|
+
@options.merge!(YAML.load_file(
|
58
|
+
File.expand_path(@options["configuration_file"])
|
59
|
+
))
|
60
|
+
end
|
61
|
+
|
62
|
+
@options["require"].each do |r|
|
63
|
+
logger.info "Requiring #{r} media-type..."
|
64
|
+
require "restfully/media_type/#{r.underscore}"
|
65
|
+
end
|
66
|
+
|
67
|
+
if given_uri = ARGV.shift
|
68
|
+
@options["uri"] = given_uri
|
69
|
+
end
|
70
|
+
# p @options
|
71
|
+
# Compatibility with restfully < 0.6
|
72
|
+
@options["uri"] ||= @options.delete("base_uri")
|
73
|
+
|
74
|
+
@session = Restfully::Session.new(@options)
|
75
|
+
|
53
76
|
|
54
77
|
def session
|
55
|
-
@session
|
78
|
+
@session
|
56
79
|
end
|
57
80
|
|
58
81
|
def root
|
@@ -63,6 +86,41 @@ puts "Restfully/#{Restfully::VERSION} - The root resource is available in the 'r
|
|
63
86
|
|
64
87
|
require 'irb'
|
65
88
|
require 'irb/completion'
|
89
|
+
require 'irb/ext/save-history'
|
90
|
+
|
91
|
+
HOME = ENV['HOME'] || ENV['HOMEPATH']
|
92
|
+
# Keep history of your last commands.
|
93
|
+
# Taken from <http://blog.nicksieger.com/articles/2006/04/23/tweaking-irb>
|
94
|
+
IRB.conf[:SAVE_HISTORY] = 100
|
95
|
+
IRB.conf[:HISTORY_FILE] = "#{HOME}/.irb-save-history"
|
96
|
+
|
97
|
+
module Readline
|
98
|
+
module History
|
99
|
+
LOG = "#{HOME}/.irb-history"
|
100
|
+
|
101
|
+
def self.write_log(line)
|
102
|
+
File.open(LOG, 'ab') {|f|
|
103
|
+
f << "#{line}\n"
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.start_session_log
|
108
|
+
write_log("\n")
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
alias :old_readline :readline
|
113
|
+
def readline(*args)
|
114
|
+
ln = old_readline(*args)
|
115
|
+
begin
|
116
|
+
History.write_log(ln)
|
117
|
+
rescue
|
118
|
+
end
|
119
|
+
ln
|
120
|
+
end
|
121
|
+
end
|
122
|
+
Readline::History.start_session_log
|
66
123
|
ARGV.clear
|
124
|
+
ARGV.concat [ "--readline", "--prompt-mode", "simple" ]
|
67
125
|
IRB.start
|
68
126
|
exit!
|
data/lib/restfully.rb
CHANGED
@@ -1,24 +1,18 @@
|
|
1
1
|
require 'backports'
|
2
2
|
require 'yaml'
|
3
|
-
|
3
|
+
|
4
|
+
require 'restfully/version'
|
4
5
|
require 'restfully/error'
|
5
|
-
require 'restfully/parsing'
|
6
6
|
require 'restfully/http'
|
7
|
-
require 'restfully/http/adapters/rest_client_adapter'
|
8
|
-
require 'restfully/session'
|
9
|
-
require 'restfully/special_hash'
|
10
|
-
require 'restfully/special_array'
|
11
7
|
require 'restfully/link'
|
12
8
|
require 'restfully/resource'
|
13
9
|
require 'restfully/collection'
|
10
|
+
require 'restfully/rack'
|
11
|
+
require 'restfully/session'
|
12
|
+
require 'restfully/media_type'
|
14
13
|
|
15
14
|
module Restfully
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
class << self
|
20
|
-
attr_accessor :adapter
|
21
|
-
end
|
22
|
-
|
23
|
-
self.adapter = Restfully::HTTP::Adapters::RestClientAdapter
|
15
|
+
MediaType.register MediaType::Grid5000
|
16
|
+
MediaType.register MediaType::ApplicationJson
|
17
|
+
MediaType.register MediaType::Wildcard
|
24
18
|
end
|
data/lib/restfully/collection.rb
CHANGED
@@ -1,111 +1,91 @@
|
|
1
|
-
|
2
1
|
module Restfully
|
3
|
-
|
4
|
-
# methods that you could expect from an <tt>Array</tt>.
|
5
|
-
# Remember that this class inherits from a <tt>Restfully::Resource</tt> and
|
6
|
-
# as such, the <tt>#[]</tt> method gives access to Resource properties, and
|
7
|
-
# not to an item in the collection.
|
8
|
-
# If you want to operate on the array of items, you MUST call <tt>#to_a</tt>
|
9
|
-
# first (or <tt>#items</tt>) on the Restfully::Collection.
|
10
|
-
class Collection < Resource
|
2
|
+
module Collection
|
11
3
|
include Enumerable
|
12
|
-
attr_reader :items
|
13
|
-
|
14
|
-
# See Resource#new
|
15
|
-
def initialize(uri, session, options = {})
|
16
|
-
super(uri, session, options)
|
17
|
-
end
|
18
4
|
|
19
|
-
#
|
20
|
-
|
21
|
-
super
|
22
|
-
@items = Array.new
|
23
|
-
@indexes = Hash.new
|
24
|
-
self
|
25
|
-
end
|
26
|
-
|
27
|
-
# Iterates over the collection of items
|
28
|
-
def each(*args, &block)
|
29
|
-
@items.each_index{ |i|
|
30
|
-
block.call(@items[i])
|
31
|
-
if i == @items.length-1 && @items.length+self["offset"] < self["total"]
|
32
|
-
# load next page
|
33
|
-
query_options = executed_requests['GET']['options'][:query] || {}
|
34
|
-
query_options[:offset] = self["offset"]+@items.length
|
35
|
-
query_options[:limit] ||= 200
|
36
|
-
next_page = Collection.new(uri, session).load(:query => query_options) rescue nil
|
37
|
-
if next_page && next_page['offset'] != self["offset"]
|
38
|
-
@items.push(*next_page.to_a)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
}
|
42
|
-
end
|
43
|
-
|
44
|
-
# if property is a Symbol, it tries to find the corresponding item whose uid.to_sym is matching the property
|
45
|
-
# else, returns the result of calling <tt>[]</tt> on its superclass.
|
5
|
+
# If property is a Symbol, it tries to find the corresponding item in the collection.
|
6
|
+
# Else, returns the result of calling <tt>[]</tt> on its superclass.
|
46
7
|
def [](property)
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
8
|
+
case property
|
9
|
+
when Symbol
|
10
|
+
find_by_uid(property)
|
11
|
+
when Integer
|
12
|
+
find_by_index(property)
|
51
13
|
else
|
52
14
|
super(property)
|
53
15
|
end
|
54
16
|
end
|
55
17
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
18
|
+
def find_by_uid(symbol)
|
19
|
+
find{ |i| reload_if_empty(i).media_type.represents?(symbol) }
|
20
|
+
end
|
21
|
+
|
22
|
+
def find_by_index(index)
|
23
|
+
index = index+length if index < 0
|
24
|
+
each_with_index{|item, i|
|
25
|
+
return reload_if_empty(item) if i == index
|
26
|
+
}
|
27
|
+
nil
|
60
28
|
end
|
61
29
|
|
62
|
-
def
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
case value
|
78
|
-
when Hash
|
79
|
-
@properties.store(key, SpecialHash.new.replace(value)) unless @links.has_key?(key)
|
80
|
-
when Array
|
81
|
-
@properties.store(key, SpecialArray.new(value))
|
82
|
-
else
|
83
|
-
@properties.store(key, value)
|
30
|
+
def each(*args, &block)
|
31
|
+
@items ||= {}
|
32
|
+
media_type.each(*args) do |item_media_type|
|
33
|
+
hash = item_media_type.hash
|
34
|
+
unless @items.has_key?(hash)
|
35
|
+
self_link = item_media_type.links.find{|l| l.self?}
|
36
|
+
|
37
|
+
req = HTTP::Request.new(session, :get, self_link.href, :head => {
|
38
|
+
'Accept' => self_link.types[0]
|
39
|
+
})
|
40
|
+
|
41
|
+
res = HTTP::Response.new(session, 200, {
|
42
|
+
'Content-Type' => self_link.types[0]
|
43
|
+
}, item_media_type.io)
|
44
|
+
@items[hash] = Resource.new(session, res, req).load
|
84
45
|
end
|
46
|
+
block.call @items[hash]
|
85
47
|
end
|
86
|
-
end
|
48
|
+
end
|
87
49
|
|
88
|
-
def
|
89
|
-
|
50
|
+
def length
|
51
|
+
self["items"].length
|
90
52
|
end
|
91
53
|
|
54
|
+
def total
|
55
|
+
self["total"].to_i
|
56
|
+
end
|
57
|
+
|
58
|
+
def offset
|
59
|
+
(self["offset"] || 0).to_i
|
60
|
+
end
|
61
|
+
|
62
|
+
def last
|
63
|
+
self[-1]
|
64
|
+
end
|
65
|
+
|
66
|
+
def empty?
|
67
|
+
total == 0
|
68
|
+
end
|
92
69
|
|
93
|
-
def
|
94
|
-
|
95
|
-
if @items.length > 0
|
96
|
-
inner_pp.breakable
|
97
|
-
inner_pp.text "ITEMS (#{self["offset"]}..#{self["offset"]+@items.length})/#{self["total"]}"
|
98
|
-
inner_pp.nest 2 do
|
99
|
-
@items.each_with_index do |item, i|
|
100
|
-
inner_pp.breakable
|
101
|
-
inner_pp.text "#<#{item.class}:0x#{item.object_id.to_s(16)} uid=#{item['uid'].inspect}>"
|
102
|
-
inner_pp.text "," if i < @items.length-1
|
103
|
-
end
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
70
|
+
def inspect
|
71
|
+
map{|item| item}.inspect
|
107
72
|
end
|
73
|
+
# def (key)
|
74
|
+
# p Addressable::URI.parse("./"+key.to_s)
|
75
|
+
# p self.uri
|
76
|
+
# uri_to_find = Addressable::URI.join(self.uri, "./"+key.to_s)
|
77
|
+
# p uri_to_find
|
78
|
+
# find{|resource|
|
79
|
+
# resource.uri == uri_to_find
|
80
|
+
# }
|
81
|
+
# end
|
108
82
|
|
109
|
-
|
83
|
+
protected
|
84
|
+
def reload_if_empty(resource)
|
85
|
+
resource.reload if resource && !resource.media_type.complete?
|
86
|
+
resource
|
87
|
+
end
|
88
|
+
|
110
89
|
end
|
90
|
+
|
111
91
|
end
|