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.
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