demetriusnunes-clouder 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ == 0.5.1 2009-03-09
2
+
3
+ * (timshadel) Added a simple fix to make Note.get(”/notes/“) work.
4
+
5
+ == 0.5.0 2009-01-29
6
+
7
+ * Initial release
@@ -0,0 +1,13 @@
1
+ History.txt
2
+ Manifest.txt
3
+ PostInstall.txt
4
+ README.rdoc
5
+ Rakefile
6
+ lib/clouder.rb
7
+ lib/clouder/entity.rb
8
+ lib/clouder/rest.rb
9
+ spec/clouder_spec.rb
10
+ spec/entity_spec.rb
11
+ spec/spec_helper.rb
12
+ spec/spec.opts
13
+ spec/config.ru
File without changes
@@ -0,0 +1,136 @@
1
+ = clouder
2
+
3
+ * http://github.com/demetriusnunes/clouder
4
+
5
+ == DESCRIPTION:
6
+
7
+ A Ruby client library for accessing CloudKit (http://getcloudkit.com)
8
+ RESTful repositories using simple Ruby objects.
9
+
10
+ == FEATURES/PROBLEMS:
11
+
12
+ At this time, *clouder* implements all methods in CloudKit API (as seen
13
+ in the checklist at the requirements section) and is good to be used in
14
+ most basic cases.
15
+
16
+ Clouder::Entity is a subclass of OStruct, but that might change in
17
+ the future.
18
+
19
+ == SYNOPSIS:
20
+
21
+ Here is some sample code of how to use *clouder*, assuming that you
22
+ have a CloudKit server running at <tt>http://localhost:9292/</tt> and
23
+ exposing "*notes*".
24
+
25
+ === Entity declaration
26
+
27
+ class Note < Clouder::Entity
28
+ uri "http://localhost:9292/notes"
29
+ end
30
+
31
+ === Creating new documents
32
+
33
+ n = Note.new
34
+ n.new? # true
35
+ n.text = "My first note"
36
+ n.author = "John Doe"
37
+ n.save
38
+ n.new? # false
39
+
40
+ # Short alternative
41
+ n = Note.create(:text => "My first note", :author => "John Doe")
42
+
43
+ === Retrieving a single document
44
+
45
+ n = Note.get("d35bfa70-cca0-012b-cd41-0016cb91f13d") # some valid id
46
+ puts n.text, n.author
47
+
48
+ === Deleting documents
49
+
50
+ n = Note.get("d35bfa70-cca0-012b-cd41-0016cb91f13d") # some valid id
51
+ n.delete
52
+ n.deleted? # true
53
+
54
+ === Retrieving collections
55
+
56
+ # This retrieves only the URIs
57
+ uris = Note.all
58
+ uris.each { |uri|
59
+ n = Note.get(uri)
60
+ puts n.text, n.author
61
+ }
62
+
63
+ # Using offset and limit
64
+ uris = Note.all(:offset => 5, :limit => 10)
65
+
66
+ # To retrieve full documents, use the :resolved option
67
+ notes = Note.all(:resolved => true)
68
+
69
+ # You can combine all the options
70
+ notes = Note.all(:resolved => true, :offset => 3, :limit => 50)
71
+
72
+ === Retrieving older versions of a document
73
+
74
+ n = Note.get("d35bfa70-cca0-012b-cd41-0016cb91f13d") # some valid id
75
+
76
+ # This retrieves only the URIs
77
+ # - you can also use offset and limit options
78
+ older_uris = n.versions(:limit => 5)
79
+
80
+ # To retrieve full documents, use the :resolved option
81
+ older_notes = n.versions(:resolved)
82
+
83
+ === Inspecting some metadata for a repository
84
+
85
+ # Which collections are available for this server?
86
+ uris = Clouder.collections("http://localhost:9292/")
87
+ uris # [ "notes" ]
88
+
89
+ # Retrieving only the headers for a valid URI
90
+ headers = Clouder.head("http://localhost:9292/notes")
91
+
92
+ == REQUIREMENTS:
93
+
94
+ CloudKit API (http://getcloudkit.com/rest-api.html) Checklist:
95
+
96
+ * GET /cloudkit-meta - OK!
97
+ * OPTIONS /%uri% - OK!
98
+ * GET /%collection% - OK!
99
+ * GET /%collection%/_resolved - OK!
100
+ * GET /%collection%/%uuid% - OK!
101
+ * GET /%collection%/%uuid%/versions - OK!
102
+ * GET /%collection%/%uuid%/versions/_resolved - OK!
103
+ * GET /%collection%/%uuid%/versions/%etag% - OK!
104
+ * POST /%collection% - OK!
105
+ * PUT /%collection%/%uuid% - OK!
106
+ * DELETE /%collection%/%uuid% - OK!
107
+ * HEAD /%uri% - OK!
108
+
109
+ == INSTALL:
110
+
111
+ * sudo gem install clouder
112
+
113
+ == LICENSE:
114
+
115
+ (The MIT License)
116
+
117
+ Copyright (c) 2009 Demetrius Nunes
118
+
119
+ Permission is hereby granted, free of charge, to any person obtaining
120
+ a copy of this software and associated documentation files (the
121
+ 'Software'), to deal in the Software without restriction, including
122
+ without limitation the rights to use, copy, modify, merge, publish,
123
+ distribute, sublicense, and/or sell copies of the Software, and to
124
+ permit persons to whom the Software is furnished to do so, subject to
125
+ the following conditions:
126
+
127
+ The above copyright notice and this permission notice shall be
128
+ included in all copies or substantial portions of the Software.
129
+
130
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
131
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
132
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
133
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
134
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
135
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
136
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,28 @@
1
+ %w[rubygems rake rake/clean fileutils newgem rubigen].each { |f| require f }
2
+ require File.dirname(__FILE__) + '/lib/clouder'
3
+
4
+ # Generate all the Rake tasks
5
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
6
+ $hoe = Hoe.new('clouder', Clouder::VERSION) do |p|
7
+ p.developer('Demetrius Nunes', 'demetriusnunes@gmail.com')
8
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
9
+ # p.post_install_message = 'PostInstall.txt' # TODO remove if post-install message not required
10
+ p.rubyforge_name = p.name # TODO this is default value
11
+ # p.extra_deps = [
12
+ # ['activesupport','>= 2.0.2'],
13
+ # ]
14
+ p.extra_dev_deps = [
15
+ ['newgem', ">= #{::Newgem::VERSION}"]
16
+ ]
17
+
18
+ p.clean_globs |= %w[**/.DS_Store tmp *.log]
19
+ path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
20
+ p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
21
+ p.rsync_args = '-av --delete --ignore-errors'
22
+ end
23
+
24
+ require 'newgem/tasks' # load /tasks/*.rake
25
+ Dir['tasks/**/*.rake'].each { |t| load t }
26
+
27
+ # TODO - want other tests/tasks run by default? Add them to the list
28
+ # task :default => [:spec, :features]
@@ -0,0 +1,31 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require "rubygems"
5
+ require 'json'
6
+ require 'restclient'
7
+ require 'clouder/entity'
8
+ require 'clouder/rest'
9
+
10
+ # The Clouder module holds global server-wide functions.
11
+ module Clouder
12
+ VERSION = '0.5.0'
13
+
14
+ # Returns an array of URIs of the resources exposed by
15
+ # the CloudKit server at the +uri+.
16
+ #
17
+ # Clouder.collection("http://localhost:9292")
18
+ # => [ "notes", "comments" ]
19
+ def Clouder.collections(uri)
20
+ uris = Rest.get(File.join(uri, "cloudkit-meta"))["uris"]
21
+ uris.map { |uri| uri.split("/").last }
22
+ end
23
+
24
+ # Makes a HEAD request to the +uri+ and returns a hash
25
+ # of headers contained in the response.
26
+ def Clouder.head(uri)
27
+ Rest.head(uri)
28
+ end
29
+
30
+ end
31
+
@@ -0,0 +1,255 @@
1
+ require 'ostruct'
2
+ require 'time'
3
+
4
+ module Clouder
5
+ # This is the base class to be used when accessing resources
6
+ # in a CloudKit server. It contains all the basic persistence
7
+ # methods and attributes. See the entity_spec.rb file for sample usages.
8
+ #
9
+ # A Note class should be declared as follows:
10
+ # class Note < Clouder::Entity
11
+ # uri "http://localhost:9292/notes"
12
+ # end
13
+ class Entity < OpenStruct
14
+
15
+ class << self
16
+
17
+ # If +address+ is passed, sets the URI for the target class.
18
+ # If nothing is passed, returns the current URI for the target class.
19
+ #
20
+ # Note.uri "http://localhost:8989/notes" # changes the old URI
21
+ # Note.uri # => "http://localhost:8989/notes"
22
+ def uri(address = nil)
23
+ address ? @uri = address : @uri
24
+ end
25
+
26
+ # If +options+ is nil, returns an array containing all URIs
27
+ # of existing objects for this class. Sort order is from the
28
+ # most recent to the oldest.
29
+ #
30
+ # For other results, +options+ can be:
31
+ # [:resolved] If +true+, returns full objects instead of URIs.
32
+ # [:offset] A positive integer, starting at 0, offsetting the result.
33
+ # [:limit] A positive integer, limiting the results.
34
+ #
35
+ # All options can be combined.
36
+ #
37
+ # Note.all
38
+ # Note.all(:resolved => true)
39
+ # Note.all(:offset => 20, :limit => 10)
40
+ # Note.all(:resolved => true, :limit => 20, :offset => 10)
41
+ def all(options = {})
42
+ uri = options[:resolved] ? File.join(@uri, "_resolved") : @uri
43
+ result = Rest.get(Rest.paramify_url(uri, options))
44
+ options[:resolved] ? result["documents"].map { |d| new(d) } : result["uris"]
45
+ end
46
+
47
+ # Creates and saves an object with the attributes and values
48
+ # passed as a hash. Returns the newly created object.
49
+ #
50
+ # note = Note.create(:text => "My note", :author => "John Doe")
51
+ def create(hsh = {})
52
+ obj = self.new(hsh || {})
53
+ obj.save
54
+ obj
55
+ end
56
+
57
+ # Retrieves an existing object with the given id or uri.
58
+ # Returns nil if the object is not found.
59
+ #
60
+ # note = Note.get("ce655c90-cf09-012b-cd41-0016cb91f13d")
61
+ def get(id_or_uri)
62
+ uri = uri_from_id(id_or_uri)
63
+ document = Rest.get(uri)
64
+ new({'uri' => uri, 'etag' => document.headers[:etag],
65
+ 'last_modified' => document.headers[:last_modified],
66
+ 'document' => document })
67
+ rescue RestClient::ResourceNotFound
68
+ nil
69
+ end
70
+
71
+ # Returns an array of allowed HTTP methods to be requested at
72
+ # +uri+. If +uri+ is nil, the class URI is queried.
73
+ #
74
+ # Note.options # => [ "GET", "HEAD", "POST", "OPTIONS" ]
75
+ def options(uri = self.uri)
76
+ doc = Rest.custom(:options, uri)
77
+ doc["allow"].to_s.split(",").map { |s| s.strip }
78
+ end
79
+
80
+ # Extracts object ids from absolute or partial URIs.
81
+ #
82
+ # Note.id_from_uri("http://localhost:9292/notes/ce655c90-cf09-012b-cd41-0016cb91f13d")
83
+ # => "ce655c90-cf09-012b-cd41-0016cb91f13d"
84
+ def id_from_uri(uri)
85
+ id = URI.parse(uri)
86
+ # /notes/abc
87
+ if id.path[0,1] == "/"
88
+ id.path.split("/")[2]
89
+ else
90
+ id.to_s
91
+ end
92
+ end
93
+
94
+ # Composes a full URI from an object id or partial URI.
95
+ #
96
+ # Note.uri_from_id("/notes/ce655c90-cf09-012b-cd41-0016cb91f13d")
97
+ # => "http://localhost:9292/notes/ce655c90-cf09-012b-cd41-0016cb91f13d"
98
+ #
99
+ # Note.uri_from_id("ce655c90-cf09-012b-cd41-0016cb91f13d")
100
+ # => "http://localhost:9292/notes/ce655c90-cf09-012b-cd41-0016cb91f13d"
101
+ def uri_from_id(id)
102
+ url = URI.parse(id)
103
+ if url.absolute?
104
+ url.to_s
105
+ else
106
+ # /notes/1234
107
+ if url.path[0,1] == "/"
108
+ (URI.parse(self.uri) + url).to_s
109
+ # 1234
110
+ else
111
+ File.join("#{self.uri}", id)
112
+ end
113
+ end
114
+ end
115
+
116
+ end
117
+
118
+ # Unique object id - not an URI, just a UUID
119
+ attr_reader :id
120
+
121
+ # ETag, a UUID
122
+ attr_reader :etag
123
+
124
+ # Last modified timestamp
125
+ attr_reader :last_modified
126
+
127
+ # Full URI for the object
128
+ # => "http://localhost:9292/notes/ce655c90-cf09-012b-cd41-0016cb91f13d"
129
+ def uri
130
+ @uri ||= self.class.uri_from_id(id) if id
131
+ end
132
+
133
+ # Partial URI for the object (without the protocol, hostname)
134
+ # => "/notes/ce655c90-cf09-012b-cd41-0016cb91f13d"
135
+ def path
136
+ URI.parse(uri).path
137
+ end
138
+
139
+ # Constructs a new, unsaved object. If +attributes+ are passed in a hash
140
+ # the new object is initialized with the corresponding attributes and values.
141
+ #
142
+ # note = Note.new # => new, empty
143
+ # note = Note.new(:text => "Ready note", :author => "Myself") # => new, with attributes set
144
+ def initialize(attributes = {})
145
+ @id, @etag, @last_modified, @deleted = nil
146
+ build(attributes)
147
+ end
148
+
149
+ # Saves a new or existing object. If the object already exists,
150
+ # then its etag should match the etag in the database, otherwise
151
+ # the operation fails.
152
+ #
153
+ # Returns +true+ if save was successful, +false+ otherwise.
154
+ # note = Note.new(:text => "Ready note", :author => "Myself")
155
+ # note.save # => true
156
+ def save
157
+ result = new? ? Rest.post(collection_uri, @table) : Rest.put(uri, @table, "If-Match" => etag)
158
+ @id, @etag, @last_modified = result.values_at("uri", "etag", "last_modified")
159
+ @id = self.class.id_from_uri(@id)
160
+ @last_modified = Time.parse(@last_modified)
161
+ true
162
+ rescue RestClient::RequestFailed
163
+ false
164
+ end
165
+
166
+ # Deletes an existing object. Its etag should match the etag in
167
+ # the database, otherwise the operation fails.
168
+ #
169
+ # Returns +true+ if save was successful, +false+ otherwise.
170
+ # note = Note.new("ce655c90-cf09-012b-cd41-0016cb91f13d")
171
+ # note.delete # => true
172
+ def delete
173
+ Rest.delete uri, "If-Match" => etag
174
+ @deleted = true
175
+ freeze
176
+ true
177
+ rescue RestClient::RequestFailed
178
+ false
179
+ end
180
+
181
+ # +true+ if object was not saved yet, +false+ otherwise.
182
+ def new?
183
+ @uri == nil and @etag == nil and @last_modified == nil
184
+ end
185
+
186
+ # +true+ if object was deleted, +false+ otherwise.
187
+ def deleted?; @deleted end
188
+
189
+ # Retrieves older versions of the object. Sort order is from the
190
+ # current version to the oldest one.
191
+ # The +options+ parameter works as in +all+.
192
+ #
193
+ # note = Note.new("ce655c90-cf09-012b-cd41-0016cb91f13d")
194
+ # older_versions = note.versions(:resolved, :limit => 3)
195
+ def versions(options = {})
196
+ if uri
197
+ url = File.join(uri, "versions")
198
+ if options[:etag]
199
+ url = File.join(url, options[:etag])
200
+ self.class.get(url)
201
+ else
202
+ url = options[:resolved] ? File.join(url, "_resolved") : url
203
+ result = Rest.get(Rest.paramify_url(url, options))
204
+ if options[:resolved]
205
+ result["documents"].map { |d| self.class.new(d) }
206
+ else
207
+ result["uris"]
208
+ end
209
+ end
210
+ else
211
+ []
212
+ end
213
+ end
214
+
215
+ # Returns an array of allowed HTTP methods to be requested for
216
+ # this object.
217
+ #
218
+ # note = Note.new("ce655c90-cf09-012b-cd41-0016cb91f13d") # => existing object
219
+ # note.options # => [ "DELETE", "GET", "HEAD", "PUT", "OPTIONS" ]
220
+ def options
221
+ self.class.options(uri)
222
+ end
223
+
224
+ def inspect
225
+ "#<#{self.class.name} uri=#{uri}, id=#{id}, etag=#{@etag}, last_modified=#{@last_modified}, #{@table.inspect}>"
226
+ end
227
+
228
+ private
229
+
230
+ def collection_uri
231
+ self.class.uri
232
+ end
233
+
234
+ def build(doc)
235
+ if doc['uri']
236
+ set_internal_attributes(doc['uri'], doc['etag'],
237
+ doc['last_modified'], doc['document'])
238
+ else
239
+ @table = doc
240
+ end
241
+ symbolize_table_keys!
242
+ end
243
+
244
+ def set_internal_attributes(id, etag, last_modified, table)
245
+ @id = self.class.id_from_uri(id)
246
+ @etag = etag.to_s.gsub('"', '')
247
+ @last_modified = Time.parse(last_modified)
248
+ @table = table.is_a?(String) ? JSON.parse(table) : table
249
+ end
250
+
251
+ def symbolize_table_keys!
252
+ @table.keys.each { |k| v = @table.delete(k); @table[k.to_sym] = v }
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,80 @@
1
+ require 'cgi'
2
+
3
+ class Rest
4
+ class << self
5
+
6
+ # set proxy for RestClient to use
7
+ def proxy url
8
+ RestClient.proxy = url
9
+ end
10
+
11
+ def put uri, doc = nil, headers = {}
12
+ payload = doc.to_json if doc
13
+ parse(RestClient.put(uri, payload, headers))
14
+ end
15
+
16
+ def get uri
17
+ parse(RestClient.get(uri), :max_nesting => false)
18
+ end
19
+
20
+ def post uri, doc = nil, headers = {}
21
+ payload = doc.to_json if doc
22
+ parse(RestClient.post(uri, payload, headers))
23
+ end
24
+
25
+ def delete uri, headers = {}
26
+ parse(RestClient.delete(uri, headers))
27
+ end
28
+
29
+ def copy uri, destination
30
+ parse(RestClient.copy(uri, {'Destination' => destination}))
31
+ end
32
+
33
+ def move uri, destination
34
+ parse(RestClient.move(uri, {'Destination' => destination}))
35
+ end
36
+
37
+ def head uri, headers = {}
38
+ custom(:head, uri, headers)
39
+ end
40
+
41
+ def custom method, uri, headers = {}
42
+ response = nil
43
+ url = URI.parse(uri)
44
+ Net::HTTP.start(url.host, url.port) { |http|
45
+ response = http.send(method, url.path, headers)
46
+ }
47
+ response.to_hash
48
+ end
49
+
50
+ def parse(response, opts = {})
51
+ if response
52
+ json = JSON.parse(response, opts)
53
+ json.extend(ResponseHeaders)
54
+ json.headers = response.headers
55
+ json
56
+ end
57
+ end
58
+
59
+ def paramify_url url, params = {}
60
+ if params && !params.empty?
61
+ query = params.collect do |k,v|
62
+ v = v.to_json if %w{key startkey endkey}.include?(k.to_s)
63
+ "#{k}=#{CGI.escape(v.to_s)}"
64
+ end.join("&")
65
+ url = "#{url}?#{query}"
66
+ end
67
+ url
68
+ end
69
+ end # class << self
70
+ end
71
+
72
+ module ResponseHeaders
73
+ def headers
74
+ @headers
75
+ end
76
+
77
+ def headers=(h)
78
+ @headers = h
79
+ end
80
+ end
@@ -0,0 +1,17 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ describe "Clouder" do
4
+
5
+ it "should get available collections" do
6
+ collections = Clouder.collections "http://localhost:9292/"
7
+ collections.size.should == 1
8
+ collections.first.should == "notes"
9
+ end
10
+
11
+ it "should allow to query any uri and just return the headers" do
12
+ headers = Clouder.head("http://localhost:9292/notes")
13
+ headers["content-type"].should include("application/json")
14
+ end
15
+
16
+
17
+ end
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ require 'cloudkit'
3
+
4
+ expose :notes
@@ -0,0 +1,303 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ describe "Entity" do
4
+
5
+ before :all do
6
+ class Note < Clouder::Entity
7
+ uri "http://localhost:9292/notes"
8
+ end
9
+ Note.all(:resolved => true).each { |n| n.delete }
10
+ end
11
+
12
+ it "should let each class be associated with a different uri" do
13
+ class Comment < Clouder::Entity
14
+ uri "http://localhost:9292/comments"
15
+ end
16
+
17
+ Comment.uri.should == "http://localhost:9292/comments"
18
+ Note.uri.should == "http://localhost:9292/notes"
19
+ end
20
+
21
+ it "should let each subclass be associated with a different uri" do
22
+ class Reminder < Note
23
+ uri "http://localhost:9292/reminders"
24
+ end
25
+
26
+ Reminder.uri.should == "http://localhost:9292/reminders"
27
+ Note.uri.should == "http://localhost:9292/notes"
28
+ end
29
+
30
+ it "should let you know the available methods for the class" do
31
+ options = Note.options
32
+ options.should == %w(GET HEAD POST OPTIONS)
33
+ end
34
+
35
+ it "should let you know the available methods for the object" do
36
+ n = Note.new
37
+ n.text = "My note"
38
+ n.save
39
+ n.options.should == %w(GET HEAD PUT DELETE OPTIONS)
40
+ end
41
+
42
+ it "should let you create entity classes" do
43
+ n = Note.new
44
+ n.new?.should == true
45
+ end
46
+
47
+ it "should let you create entity classes with attributes" do
48
+ n = Note.new(:text => "My first note", :author => "Myself")
49
+ n.new?.should == true
50
+ n.text.should == "My first note"
51
+ n.author.should == "Myself"
52
+ end
53
+
54
+ it "should let you inspect its uri" do
55
+ Note.uri.should == "http://localhost:9292/notes"
56
+ end
57
+
58
+ it "should retrieve all uris" do
59
+ size = Note.all.size
60
+ Note.create(:text => "note 1").should be_an_instance_of(Note)
61
+ Note.create(:text => "note 2").should be_an_instance_of(Note)
62
+ Note.all.size.should == size + 2
63
+ end
64
+
65
+ it "should retrieve all instances" do
66
+ size = Note.all.size
67
+ Note.create(:text => "note 1").should be_an_instance_of(Note)
68
+ Note.create(:text => "note 2").should be_an_instance_of(Note)
69
+
70
+ notes = Note.all(:resolved => true)
71
+ notes.each { |n|
72
+ n.should be_an_instance_of(Note)
73
+ n.etag.should_not == nil
74
+ n.id.should_not == nil
75
+ n.last_modified.should_not == nil
76
+ }
77
+ Note.all.size.should == size + 2
78
+ end
79
+
80
+ it "should retrieve all uris with limit and offset" do
81
+ size = Note.all.size
82
+
83
+ notes = []
84
+ (1..4).each { |i| notes << Note.create(:text => "$note #{i}") }
85
+
86
+ last_notes = Note.all(:limit => 4)
87
+ last_notes.size.should == 4
88
+ last_notes.should == notes.map { |n| n.path }.reverse
89
+
90
+ last_notes = Note.all(:offset => 2, :limit => 2)
91
+ last_notes.size.should == 2
92
+ last_notes.should == notes[0..1].map { |n| n.path }.reverse
93
+
94
+ last_notes = Note.all(:offset => 2)
95
+ last_notes.size.should == size + 2
96
+ end
97
+
98
+ it "should let you access attributes" do
99
+ n = Note.new
100
+ n.text = "My Note"
101
+ n.author = "John Doe"
102
+
103
+ n.text.should == "My Note"
104
+ n.author.should == "John Doe"
105
+ end
106
+
107
+ it "should let you save new objects" do
108
+ n = Note.new
109
+ n.new?.should == true
110
+ n.uri.should == nil
111
+ n.last_modified.should == nil
112
+ n.etag.should == nil
113
+
114
+ n.text = "My Note"
115
+ n.author = "John Doe"
116
+
117
+ n.save
118
+
119
+ n.new?.should == false
120
+ n.uri.should_not == nil
121
+ n.last_modified.should be_close(Time.now, 10)
122
+ n.etag.should_not == nil
123
+ end
124
+
125
+ it "should let you retrieve saved objects" do
126
+ n = Note.new
127
+ n.text = "My Note"
128
+ n.author = "John Doe"
129
+ n.save
130
+
131
+ id, etag, last_modified = n.id, n.etag, n.last_modified
132
+
133
+ n = Note.get(id)
134
+ n.text.should == "My Note"
135
+ n.author.should == "John Doe"
136
+ n.id.should == id
137
+ n.etag.should == etag
138
+ n.last_modified.should == last_modified
139
+ end
140
+
141
+ it "should let you retrieve saved objects by URI" do
142
+ n = Note.new
143
+ n.text = "My Note"
144
+ n.author = "John Doe"
145
+ n.save
146
+
147
+ id, etag, last_modified = n.id, n.etag, n.last_modified
148
+
149
+ n = Note.get Note.all.first
150
+ n.text.should == "My Note"
151
+ n.author.should == "John Doe"
152
+ n.id.should == id
153
+ n.etag.should == etag
154
+ n.last_modified.should == last_modified
155
+ end
156
+
157
+ it "should return nil when trying to retrieve a non-existing object" do
158
+ Note.get("abcdef").should == nil
159
+ end
160
+
161
+ it "should let you update saved objects" do
162
+ n = Note.new
163
+ n.text = "My Note"
164
+ n.author = "John Doe"
165
+ n.save
166
+
167
+ n = Note.get(n.id)
168
+ n.versions.size.should == 1
169
+ n.text = "My modified note"
170
+ n.author = "John Doe II"
171
+ n.save.should == true
172
+
173
+ n = Note.get(n.id)
174
+ n.versions.size.should == 2
175
+ n.text.should == "My modified note"
176
+ n.author.should == "John Doe II"
177
+ end
178
+
179
+ it "should let you delete existing objects" do
180
+ size = Note.all.size
181
+ n = Note.new
182
+ n.text = "My Note"
183
+ n.author = "John Doe"
184
+ n.save
185
+ Note.all.size.should == size + 1
186
+ n.delete
187
+ n.frozen?.should == true
188
+ n.deleted?.should == true
189
+ Note.all.size.should == size
190
+ end
191
+
192
+ it "should let you query versions about the object" do
193
+ n = Note.new
194
+ n.versions.should == []
195
+ n.text = "First version"
196
+ n.save
197
+
198
+ etag = n.etag
199
+ n.versions.size.should == 1
200
+ n.text = "Second version"
201
+ n.save
202
+
203
+ n.versions.size.should == 2
204
+ n.versions[1].should =~ Regexp.new(etag)
205
+ n.text = "Third version"
206
+ etag = n.etag
207
+ n.save
208
+ n.versions.size.should == 3
209
+ n.versions[1].should =~ Regexp.new(etag)
210
+ end
211
+
212
+ it "should let you query versions about the object using offset and limit" do
213
+ etags = []
214
+ n = Note.new
215
+ n.text = "First version"
216
+ n.save
217
+ etags << n.etag
218
+
219
+ n.text = "Second version"
220
+ n.save
221
+ etags << n.etag
222
+
223
+ n.text = "Third version"
224
+ etag = n.etag
225
+ n.save
226
+ etags << n.etag
227
+
228
+ versions = n.versions(:limit => 2)
229
+ versions.size.should == 2
230
+ versions.first.should include(n.path)
231
+ versions.last.should =~ Regexp.new(etags[1])
232
+
233
+ versions = n.versions(:offset => 1, :limit => 2)
234
+ versions.size.should == 2
235
+ versions.first.should =~ Regexp.new(etags[1])
236
+ versions.last.should =~ Regexp.new(etags[0])
237
+
238
+ versions = n.versions(:offset => 2, :limit => 2)
239
+ versions.size.should == 1
240
+ versions.first.should =~ Regexp.new(etags[0])
241
+ end
242
+
243
+ it "should let you retrieve full objects from versions" do
244
+ texts = %w{text1 text2 text3}
245
+ etags = []
246
+ last_modifieds = []
247
+ n = Note.new
248
+ texts.each { |text|
249
+ n.text = text
250
+ n.save
251
+ etags << n.etag
252
+ last_modifieds << n.last_modified
253
+ }
254
+ versions = n.versions(:resolved => true)
255
+ versions.size.should == 3
256
+ versions.map { |v| v.etag }.should == etags.reverse
257
+ versions.map { |v| v.last_modified }.should == last_modifieds.reverse
258
+ versions.map { |v| v.text }.should == texts.reverse
259
+ end
260
+
261
+ it "should let you retrieve older versions by etag" do
262
+ texts = %w{text1 text2 text3}
263
+ etags = []
264
+ last_modifieds = []
265
+ n = Note.new
266
+ texts.each { |text|
267
+ n.text = text
268
+ n.save
269
+ etags << n.etag
270
+ last_modifieds << n.last_modified
271
+ }
272
+
273
+ etags[0..1].each_with_index { |etag, i|
274
+ n = n.versions(:etag => etag)
275
+ n.etag.should == etag
276
+ n.text.should == texts[i]
277
+ n.last_modified.should == last_modifieds[i]
278
+ }
279
+ end
280
+
281
+ it "should fail when trying to save an outdated object" do
282
+ n = Note.create(:text => "Original")
283
+
284
+ n2 = Note.get(n.id)
285
+ n2.text = "Modified"
286
+ n2.save.should == true
287
+
288
+ n.text = "Modified but outdated"
289
+ n.save.should == false
290
+ end
291
+
292
+ it "should fail when trying to delete an outdated object" do
293
+ n = Note.create(:text => "Original")
294
+
295
+ n2 = Note.get(n.id)
296
+ n2.text = "Modified"
297
+ n2.save.should == true
298
+
299
+ n.text = "Modified but outdated"
300
+ n.delete.should == false
301
+ end
302
+
303
+ end
@@ -0,0 +1 @@
1
+ --colour
@@ -0,0 +1,10 @@
1
+ begin
2
+ require 'spec'
3
+ rescue LoadError
4
+ require 'rubygems'
5
+ gem 'rspec'
6
+ require 'spec'
7
+ end
8
+
9
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
10
+ require 'clouder'
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: demetriusnunes-clouder
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.1
5
+ platform: ruby
6
+ authors:
7
+ - Demetrius Nunes
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-29 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: newgem
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.2.3
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: hoe
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.8.0
34
+ version:
35
+ description: A Ruby client library for accessing CloudKit using Ruby objects.
36
+ email:
37
+ - demetriusnunes@gmail.com
38
+ executables: []
39
+
40
+ extensions: []
41
+
42
+ extra_rdoc_files:
43
+ - History.txt
44
+ - Manifest.txt
45
+ - PostInstall.txt
46
+ - README.rdoc
47
+ files:
48
+ - History.txt
49
+ - Manifest.txt
50
+ - PostInstall.txt
51
+ - README.rdoc
52
+ - Rakefile
53
+ - lib/clouder.rb
54
+ - lib/clouder/entity.rb
55
+ - lib/clouder/rest.rb
56
+ - spec/clouder_spec.rb
57
+ - spec/entity_spec.rb
58
+ - spec/spec_helper.rb
59
+ - spec/spec.opts
60
+ - spec/config.ru
61
+ has_rdoc: true
62
+ homepage: http://github.com/demetriusnunes/clouder/tree/master
63
+ post_install_message: PostInstall.txt
64
+ rdoc_options:
65
+ - --main
66
+ - README.rdoc
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: "0"
74
+ version:
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: "0"
80
+ version:
81
+ requirements: []
82
+
83
+ rubyforge_project: clouder
84
+ rubygems_version: 1.2.0
85
+ signing_key:
86
+ specification_version: 2
87
+ summary: A Ruby client library for accessing CloudKit using Ruby objects.
88
+ test_files: []
89
+