restfully 0.6.3 → 0.7.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/README.md +166 -0
  2. data/Rakefile +35 -35
  3. data/bin/restfully +68 -10
  4. data/lib/restfully.rb +8 -14
  5. data/lib/restfully/collection.rb +70 -90
  6. data/lib/restfully/error.rb +2 -0
  7. data/lib/restfully/http.rb +3 -3
  8. data/lib/restfully/http/error.rb +1 -20
  9. data/lib/restfully/http/helper.rb +49 -0
  10. data/lib/restfully/http/request.rb +60 -24
  11. data/lib/restfully/http/response.rb +55 -24
  12. data/lib/restfully/link.rb +32 -24
  13. data/lib/restfully/media_type.rb +70 -0
  14. data/lib/restfully/media_type/abstract_media_type.rb +162 -0
  15. data/lib/restfully/media_type/application_json.rb +21 -0
  16. data/lib/restfully/media_type/application_vnd_bonfire_xml.rb +177 -0
  17. data/lib/restfully/media_type/application_x_www_form_urlencoded.rb +33 -0
  18. data/lib/restfully/media_type/grid5000.rb +67 -0
  19. data/lib/restfully/media_type/wildcard.rb +27 -0
  20. data/lib/restfully/rack.rb +1 -0
  21. data/lib/restfully/rack/basic_auth.rb +26 -0
  22. data/lib/restfully/resource.rb +134 -197
  23. data/lib/restfully/session.rb +127 -70
  24. data/lib/restfully/version.rb +3 -0
  25. data/spec/fixtures/bonfire-collection-with-fragments.xml +6 -0
  26. data/spec/fixtures/bonfire-compute-existing.xml +43 -0
  27. data/spec/fixtures/bonfire-empty-collection.xml +4 -0
  28. data/spec/fixtures/bonfire-experiment-collection.xml +51 -0
  29. data/spec/fixtures/bonfire-network-collection.xml +35 -0
  30. data/spec/fixtures/bonfire-network-existing.xml +6 -0
  31. data/spec/fixtures/bonfire-root.xml +5 -0
  32. data/spec/fixtures/grid5000-rennes-jobs.json +988 -146
  33. data/spec/fixtures/grid5000-rennes.json +63 -0
  34. data/spec/restfully/collection_spec.rb +87 -0
  35. data/spec/restfully/http/helper_spec.rb +18 -0
  36. data/spec/restfully/http/request_spec.rb +97 -0
  37. data/spec/restfully/http/response_spec.rb +53 -0
  38. data/spec/restfully/link_spec.rb +80 -0
  39. data/spec/restfully/media_type/application_vnd_bonfire_xml_spec.rb +153 -0
  40. data/spec/restfully/media_type_spec.rb +117 -0
  41. data/spec/restfully/resource_spec.rb +109 -0
  42. data/spec/restfully/session_spec.rb +229 -0
  43. data/spec/spec_helper.rb +10 -9
  44. metadata +162 -83
  45. data/.document +0 -5
  46. data/CHANGELOG +0 -62
  47. data/README.rdoc +0 -146
  48. data/TODO.rdoc +0 -3
  49. data/VERSION +0 -1
  50. data/examples/grid5000.rb +0 -33
  51. data/examples/scratch.rb +0 -37
  52. data/lib/restfully/extensions.rb +0 -34
  53. data/lib/restfully/http/adapters/abstract_adapter.rb +0 -29
  54. data/lib/restfully/http/adapters/patron_adapter.rb +0 -16
  55. data/lib/restfully/http/adapters/rest_client_adapter.rb +0 -75
  56. data/lib/restfully/http/headers.rb +0 -20
  57. data/lib/restfully/parsing.rb +0 -66
  58. data/lib/restfully/special_array.rb +0 -5
  59. data/lib/restfully/special_hash.rb +0 -5
  60. data/restfully.gemspec +0 -114
  61. data/spec/collection_spec.rb +0 -120
  62. data/spec/fixtures/configuration_file.yml +0 -4
  63. data/spec/fixtures/grid5000-sites.json +0 -540
  64. data/spec/http/error_spec.rb +0 -18
  65. data/spec/http/headers_spec.rb +0 -17
  66. data/spec/http/request_spec.rb +0 -49
  67. data/spec/http/response_spec.rb +0 -19
  68. data/spec/http/rest_client_adapter_spec.rb +0 -35
  69. data/spec/link_spec.rb +0 -61
  70. data/spec/parsing_spec.rb +0 -40
  71. data/spec/resource_spec.rb +0 -320
  72. data/spec/restfully_spec.rb +0 -16
  73. 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
- 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.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
- rescue LoadError
23
- puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
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
- require 'spec/rake/spectask'
27
- Spec::Rake::SpecTask.new(:spec) do |spec|
28
- spec.libs << 'lib' << 'spec'
29
- spec.spec_files = FileList['spec/**/*_spec.rb']
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
- Spec::Rake::SpecTask.new(:rcov) do |spec|
33
- spec.libs << 'lib' << 'spec'
34
- spec.pattern = 'spec/**/*_spec.rb'
35
- spec.rcov = true
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
- require 'rake/rdoctask'
41
- Rake::RDocTask.new do |rdoc|
42
- rdoc.rdoc_dir = 'rdoc'
43
- rdoc.title = 'restfully'
44
- rdoc.options << '--line-numbers' << '--inline-source'
45
- rdoc.rdoc_files.include('README*')
46
- rdoc.rdoc_files.include('lib/**/*.rb')
47
- end
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(STDOUT)
13
+ logger = Logger.new(STDERR)
14
14
  logger.level = Logger::WARN
15
- @options = {:logger => logger}
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 [base_uri] [options]
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[:username] = u
27
+ @options["username"] = u
27
28
  end
28
29
  opts.on("-p=", "--password=", "Sets the user password") do |p|
29
- @options[:password] = p
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[:configuration_file] = v
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[:logger] = logger
42
+ @options["logger"] = logger
39
43
  end
40
44
  opts.on("-v", "--verbose", "Run verbosely") do |v|
41
- @options[:logger].level = Logger::DEBUG
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[:base_uri] = ARGV.shift
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 ||= Restfully::Session.new(@options)
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
- require 'restfully/extensions'
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
- # To be changed on version bump
17
- VERSION = "0.6.3"
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
@@ -1,111 +1,91 @@
1
-
2
1
  module Restfully
3
- # This includes the <tt>Enumerable</tt> module, but does not have all the
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
- # See Resource#reset
20
- def reset
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
- if property.kind_of?(Symbol)
48
- item = find{|i| i['uid'] == property.to_s} ||
49
- find{|i| i['uid'] == property.to_s.to_i} ||
50
- Resource.new([uri, property].join("/"), session).load rescue nil
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
- # Returns the current number of items (not the total number)
57
- # in the collection.
58
- def length
59
- @items.length
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 populate_object(key, value)
63
- case key
64
- when "links"
65
- value.each{|link| define_link(Link.new(link))}
66
- when "items"
67
- value.each do |item|
68
- self_link = (item['links'] || []).
69
- map{|link| Link.new(link)}.detect{|link| link.self?}
70
- if self_link && self_link.valid?
71
- @items.push Resource.new(uri_for(self_link.href), session).load(:body => item)
72
- else
73
- session.logger.warn "Resource #{key} does not have a 'self' link. skipped."
74
- end
75
- end
76
- else
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 inspect
89
- @items.inspect
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 pretty_print(pp)
94
- super(pp) do |inner_pp|
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
- alias_method :size, :length
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