demetriusnunes-clouder 0.5.1

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