couchdb 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +47 -0
- data/Rakefile +48 -0
- data/lib/couchdb.rb +12 -0
- data/lib/couchdb/collection.rb +121 -0
- data/lib/couchdb/database.rb +57 -0
- data/lib/couchdb/design.rb +57 -0
- data/lib/couchdb/design/view.rb +37 -0
- data/lib/couchdb/document.rb +127 -0
- data/lib/couchdb/row.rb +22 -0
- data/lib/couchdb/server.rb +42 -0
- data/spec/acceptance/database_spec.rb +56 -0
- data/spec/acceptance/document_spec.rb +78 -0
- data/spec/acceptance/server_spec.rb +43 -0
- data/spec/acceptance/views_spec.rb +49 -0
- data/spec/lib/couchdb/collection_spec.rb +121 -0
- data/spec/lib/couchdb/database_spec.rb +138 -0
- data/spec/lib/couchdb/design/view_spec.rb +89 -0
- data/spec/lib/couchdb/design_spec.rb +99 -0
- data/spec/lib/couchdb/document_spec.rb +322 -0
- data/spec/lib/couchdb/row_spec.rb +56 -0
- data/spec/lib/couchdb/server_spec.rb +87 -0
- data/spec/spec_helper.rb +5 -0
- metadata +137 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Philipp Brüll
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
|
2
|
+
= A straight-forward client for CouchDB REST API
|
3
|
+
|
4
|
+
The intend of this project is to provide a plain, straight-forward abstraction of the CouchDB REST API. The resources
|
5
|
+
exposed by the API are simply wrapped into classes.
|
6
|
+
|
7
|
+
== An example
|
8
|
+
|
9
|
+
server = CouchDB::Server.new "localhost", 5984
|
10
|
+
database = CouchDB::Database.new server, "test"
|
11
|
+
database.delete_if_exists!
|
12
|
+
database.create_if_missing!
|
13
|
+
|
14
|
+
document_one = CouchDB::Document.new database, "_id" => "test_document_1", "category" => "one"
|
15
|
+
document_one.save
|
16
|
+
|
17
|
+
document_two = CouchDB::Document.new database, "_id" => "test_document_2", "category" => "two"
|
18
|
+
document_two.save
|
19
|
+
|
20
|
+
design = CouchDB::Design.new database, "design_1"
|
21
|
+
view = CouchDB::Design::View.new design, "view_1",
|
22
|
+
"function(document) { emit([ document['category'], document['_id'] ]); }"
|
23
|
+
design.save
|
24
|
+
|
25
|
+
collection = view.collection :startkey => [ "one", nil ], :endkey => [ "one", { } ]
|
26
|
+
collection.total_count # => 2
|
27
|
+
collection.size # => 1
|
28
|
+
collection[0].id # => "test_document_1"
|
29
|
+
collection[0].key # => [ "one", "test_document_1" ]
|
30
|
+
collection[0].value # => nil
|
31
|
+
|
32
|
+
collection.documents.include? document_one # => true
|
33
|
+
collection.documents.include? document_two # => false
|
34
|
+
|
35
|
+
This example creates a database on the local CouchDB server (if it's missing) and stores two documents in it. It also
|
36
|
+
creates a design document with a view, that makes it possible to select the results by the document's category.
|
37
|
+
|
38
|
+
The <tt>collection</tt> call on that view, returns a subset of the results, by defining a start- and an endkey. The
|
39
|
+
collection object itself acts as a proxy to the results. This first request of it's content will actually fetch the
|
40
|
+
data from the server.
|
41
|
+
|
42
|
+
If the results are accessed by the <tt>documents</tt> proxy, the <tt>include_docs</tt> parameter will be passed to the
|
43
|
+
server and the delivered document hashes will be returned as an array of document (<tt>CouchDB::Document</tt>) objects.
|
44
|
+
|
45
|
+
== Development
|
46
|
+
|
47
|
+
This project is still under development. Any bug report and contribution is welcome!
|
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'rspec'
|
3
|
+
gem 'reek'
|
4
|
+
require 'rspec'
|
5
|
+
require 'rake/rdoctask'
|
6
|
+
require 'rspec/core/rake_task'
|
7
|
+
require 'reek/rake/task'
|
8
|
+
|
9
|
+
task :default => :spec
|
10
|
+
|
11
|
+
namespace :gem do
|
12
|
+
|
13
|
+
desc "Builds the gem"
|
14
|
+
task :build do
|
15
|
+
system "gem build *.gemspec && mkdir -p pkg/ && mv *.gem pkg/"
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Builds and installs the gem"
|
19
|
+
task :install => :build do
|
20
|
+
system "gem install pkg/"
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
Reek::Rake::Task.new do |task|
|
26
|
+
task.fail_on_error = true
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "Generate the rdoc"
|
30
|
+
Rake::RDocTask.new do |rdoc|
|
31
|
+
rdoc.rdoc_files.add [ "README.rdoc", "lib/**/*.rb" ]
|
32
|
+
rdoc.main = "README.rdoc"
|
33
|
+
rdoc.title = ""
|
34
|
+
end
|
35
|
+
|
36
|
+
desc "Run all specs in spec directory"
|
37
|
+
RSpec::Core::RakeTask.new do |task|
|
38
|
+
task.pattern = "spec/gom/**/*_spec.rb"
|
39
|
+
end
|
40
|
+
|
41
|
+
namespace :spec do
|
42
|
+
|
43
|
+
desc "Run all integration specs in spec/acceptance directory"
|
44
|
+
RSpec::Core::RakeTask.new(:acceptance) do |task|
|
45
|
+
task.pattern = "spec/acceptance/**/*_spec.rb"
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
data/lib/couchdb.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'transport'
|
2
|
+
|
3
|
+
module CouchDB
|
4
|
+
|
5
|
+
autoload :Server, File.join(File.dirname(__FILE__), "couchdb", "server")
|
6
|
+
autoload :Database, File.join(File.dirname(__FILE__), "couchdb", "database")
|
7
|
+
autoload :Document, File.join(File.dirname(__FILE__), "couchdb", "document")
|
8
|
+
autoload :Design, File.join(File.dirname(__FILE__), "couchdb", "design")
|
9
|
+
autoload :Collection, File.join(File.dirname(__FILE__), "couchdb", "collection")
|
10
|
+
autoload :Row, File.join(File.dirname(__FILE__), "couchdb", "row")
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
|
2
|
+
module CouchDB
|
3
|
+
|
4
|
+
# Collection is a proxy class for the result-set of a CouchDB view. It provides
|
5
|
+
# all read-only methods of an array. The loading of content is lazy and
|
6
|
+
# will be triggered on the first request.
|
7
|
+
class Collection
|
8
|
+
|
9
|
+
REQUEST_PARAMETER_KEYS = [
|
10
|
+
:key, :startkey, :startkey_docid, :endkey, :endkey_docid,
|
11
|
+
:limit, :stale, :descending, :skip, :group, :group_level,
|
12
|
+
:reduce, :inclusive_end, :include_docs
|
13
|
+
].freeze unless defined?(REQUEST_PARAMETER_KEYS)
|
14
|
+
|
15
|
+
ARRAY_METHOD_NAMES = [
|
16
|
+
:[], :at, :collect, :compact, :count, :cycle, :each, :each_index,
|
17
|
+
:empty?, :fetch, :index, :first, :flatten, :include?, :join, :last,
|
18
|
+
:length, :map, :pack, :reject, :reverse, :reverse_each, :rindex,
|
19
|
+
:sample, :shuffle, :size, :slice, :sort, :take, :to_a, :to_ary,
|
20
|
+
:values_at, :zip
|
21
|
+
].freeze unless defined?(ARRAY_METHOD_NAMES)
|
22
|
+
|
23
|
+
attr_reader :database
|
24
|
+
attr_reader :url
|
25
|
+
attr_reader :options
|
26
|
+
attr_reader :documents
|
27
|
+
|
28
|
+
def initialize(database, url, options = { })
|
29
|
+
@database, @url, @options = database, url, options
|
30
|
+
@documents = DocumentsProxy.new self
|
31
|
+
end
|
32
|
+
|
33
|
+
def total_count
|
34
|
+
fetch_meta unless @total_count
|
35
|
+
@total_count
|
36
|
+
end
|
37
|
+
|
38
|
+
def respond_to?(method_name)
|
39
|
+
ARRAY_METHOD_NAMES.include?(method_name) || super
|
40
|
+
end
|
41
|
+
|
42
|
+
def method_missing(method_name, *arguments, &block)
|
43
|
+
if ARRAY_METHOD_NAMES.include?(method_name)
|
44
|
+
fetch
|
45
|
+
@entries.send method_name, *arguments, &block
|
46
|
+
else
|
47
|
+
super
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def fetch
|
54
|
+
fetch_response
|
55
|
+
evaluate_total_count
|
56
|
+
evaluate_entries
|
57
|
+
true
|
58
|
+
end
|
59
|
+
|
60
|
+
def fetch_meta
|
61
|
+
fetch_meta_response
|
62
|
+
evaluate_total_count
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
def fetch_response
|
67
|
+
@response = Transport::JSON.request(
|
68
|
+
:get, url,
|
69
|
+
:parameters => request_parameters,
|
70
|
+
:expected_status_code => 200,
|
71
|
+
:encode_parameters => true
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
def fetch_meta_response
|
76
|
+
@response = Transport::JSON.request(
|
77
|
+
:get, url,
|
78
|
+
:parameters => request_parameters.merge(:limit => 0),
|
79
|
+
:expected_status_code => 200,
|
80
|
+
:encode_parameters => true
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
def request_parameters
|
85
|
+
parameters = { }
|
86
|
+
REQUEST_PARAMETER_KEYS.each do |key|
|
87
|
+
parameters[key] = @options[key] if @options.has_key?(key)
|
88
|
+
end
|
89
|
+
parameters
|
90
|
+
end
|
91
|
+
|
92
|
+
def evaluate_total_count
|
93
|
+
@total_count = @response["total_rows"]
|
94
|
+
end
|
95
|
+
|
96
|
+
def evaluate_entries
|
97
|
+
@entries = (@response["rows"] || [ ]).map{ |row_hash| Row.new @database, row_hash }
|
98
|
+
end
|
99
|
+
|
100
|
+
# A proxy class for the collection's fetched documents.
|
101
|
+
class DocumentsProxy
|
102
|
+
|
103
|
+
def initialize(collection)
|
104
|
+
@collection = collection
|
105
|
+
end
|
106
|
+
|
107
|
+
def method_missing(method_name, *arguments, &block)
|
108
|
+
if ARRAY_METHOD_NAMES.include?(method_name)
|
109
|
+
@collection.options[:include_docs] = true
|
110
|
+
@documents = @collection.map{ |row| row.document }
|
111
|
+
@documents.send method_name, *arguments, &block
|
112
|
+
else
|
113
|
+
super
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
|
2
|
+
module CouchDB
|
3
|
+
|
4
|
+
# The Database class provides methods create, delete and retrieve informations
|
5
|
+
# of a CouchDB database.
|
6
|
+
class Database
|
7
|
+
|
8
|
+
attr_reader :server
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
def initialize(server, name)
|
12
|
+
@server, @name = server, name
|
13
|
+
end
|
14
|
+
|
15
|
+
def ==(other)
|
16
|
+
other.is_a?(self.class) && @name == other.name && @server == other.server
|
17
|
+
end
|
18
|
+
|
19
|
+
def ===(other)
|
20
|
+
object_id == other.object_id
|
21
|
+
end
|
22
|
+
|
23
|
+
def create!
|
24
|
+
Transport::JSON.request :put, url, :expected_status_code => 201
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_if_missing!
|
28
|
+
create! unless exists?
|
29
|
+
end
|
30
|
+
|
31
|
+
def delete!
|
32
|
+
Transport::JSON.request :delete, url, :expected_status_code => 200
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete_if_exists!
|
36
|
+
delete! if exists?
|
37
|
+
end
|
38
|
+
|
39
|
+
def information
|
40
|
+
Transport::JSON.request :get, url, :expected_status_code => 200
|
41
|
+
end
|
42
|
+
|
43
|
+
def exists?
|
44
|
+
@server.database_names.include? @name
|
45
|
+
end
|
46
|
+
|
47
|
+
def url
|
48
|
+
"#{@server.url}/#{@name}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def documents(options = { })
|
52
|
+
Collection.new self, url + "/_all_docs", options
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
|
2
|
+
module CouchDB
|
3
|
+
|
4
|
+
# The Design class acts as a wrapper for CouchDB design documents.
|
5
|
+
class Design < Document
|
6
|
+
|
7
|
+
autoload :View, File.join(File.dirname(__FILE__), "design", "view")
|
8
|
+
|
9
|
+
attr_accessor :language
|
10
|
+
attr_reader :views
|
11
|
+
|
12
|
+
def initialize(database, id, language = "javascript")
|
13
|
+
super database
|
14
|
+
self.id, self.language = id, language
|
15
|
+
@views = ViewsProxy.new self
|
16
|
+
end
|
17
|
+
|
18
|
+
def id
|
19
|
+
super.sub /^_design\//, ""
|
20
|
+
end
|
21
|
+
|
22
|
+
def id=(value)
|
23
|
+
super "_design/#{value}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def language
|
27
|
+
self["language"]
|
28
|
+
end
|
29
|
+
|
30
|
+
def language=(value)
|
31
|
+
self["language"] = value
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# A proxy class for the views property.
|
37
|
+
class ViewsProxy
|
38
|
+
|
39
|
+
def initialize(design)
|
40
|
+
@design = design
|
41
|
+
@design["views"] = { }
|
42
|
+
end
|
43
|
+
|
44
|
+
def <<(view)
|
45
|
+
@design["views"].merge! view.to_hash
|
46
|
+
end
|
47
|
+
|
48
|
+
def [](name)
|
49
|
+
map, reduce = @design["views"][name].values_at("map", "reduce")
|
50
|
+
Design::View.new name, map, reduce
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
|
2
|
+
module CouchDB
|
3
|
+
|
4
|
+
# See CouchDB::Design class for description.
|
5
|
+
class Design < Document
|
6
|
+
|
7
|
+
# The View class acts as a wrapper for the views that are in the CouchDB design document. It also
|
8
|
+
# provides methods to generate simple view javascript functions.
|
9
|
+
class View
|
10
|
+
|
11
|
+
attr_accessor :design
|
12
|
+
attr_accessor :name
|
13
|
+
attr_accessor :map
|
14
|
+
attr_accessor :reduce
|
15
|
+
|
16
|
+
def initialize(design, name, map = nil, reduce = nil)
|
17
|
+
@design, @name, @map, @reduce = design, name, map, reduce
|
18
|
+
@design.views << self
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_hash
|
22
|
+
{ @name => { "map" => @map, "reduce" => @reduce } }
|
23
|
+
end
|
24
|
+
|
25
|
+
def collection(options = { })
|
26
|
+
@design ? Collection.new(@design.database, url, options) : nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def url
|
30
|
+
@design ? "#{@design.url}/_view/#{@name}" : nil
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
|
2
|
+
module CouchDB
|
3
|
+
|
4
|
+
# Base is the main super class of all models that should be stored in CouchDB.
|
5
|
+
# See the README file for more information.
|
6
|
+
class Document
|
7
|
+
|
8
|
+
# The NotFoundError will be raised if an operation is tried on a document that
|
9
|
+
# doesn't exists.
|
10
|
+
class NotFoundError < StandardError; end
|
11
|
+
|
12
|
+
attr_reader :database
|
13
|
+
|
14
|
+
def initialize(database, properties = { })
|
15
|
+
@database = database
|
16
|
+
@properties = properties
|
17
|
+
end
|
18
|
+
|
19
|
+
def [](key)
|
20
|
+
@properties[key.to_s]
|
21
|
+
end
|
22
|
+
|
23
|
+
def []=(key, value)
|
24
|
+
@properties[key.to_s] = value
|
25
|
+
end
|
26
|
+
|
27
|
+
def id
|
28
|
+
self["_id"]
|
29
|
+
end
|
30
|
+
|
31
|
+
def id=(value)
|
32
|
+
self["_id"] = value
|
33
|
+
end
|
34
|
+
|
35
|
+
def rev
|
36
|
+
self["_rev"]
|
37
|
+
end
|
38
|
+
|
39
|
+
def rev=(value)
|
40
|
+
self["_rev"] = value
|
41
|
+
end
|
42
|
+
|
43
|
+
def rev?
|
44
|
+
@properties.has_key? "_rev"
|
45
|
+
end
|
46
|
+
|
47
|
+
def clear_rev
|
48
|
+
@properties.delete "_rev"
|
49
|
+
end
|
50
|
+
|
51
|
+
def ==(other)
|
52
|
+
self.id == other.id
|
53
|
+
end
|
54
|
+
|
55
|
+
def new?
|
56
|
+
!self.rev?
|
57
|
+
end
|
58
|
+
|
59
|
+
def exists?
|
60
|
+
Transport::JSON.request :get, url, :expected_status_code => 200
|
61
|
+
true
|
62
|
+
rescue Transport::UnexpectedStatusCodeError => error
|
63
|
+
raise error unless error.status_code == 404
|
64
|
+
false
|
65
|
+
end
|
66
|
+
|
67
|
+
def load
|
68
|
+
@properties = Transport::JSON.request :get, url, :expected_status_code => 200
|
69
|
+
true
|
70
|
+
rescue Transport::UnexpectedStatusCodeError => error
|
71
|
+
upgrade_unexpected_status_error error
|
72
|
+
end
|
73
|
+
alias reload load
|
74
|
+
|
75
|
+
def save
|
76
|
+
new? ? create : update
|
77
|
+
end
|
78
|
+
|
79
|
+
def destroy
|
80
|
+
return false if new?
|
81
|
+
Transport::JSON.request :delete, url, :headers => { "If-Match" => self.rev }, :expected_status_code => 200
|
82
|
+
self.clear_rev
|
83
|
+
true
|
84
|
+
rescue Transport::UnexpectedStatusCodeError => error
|
85
|
+
upgrade_unexpected_status_error error
|
86
|
+
end
|
87
|
+
|
88
|
+
def url
|
89
|
+
"#{self.database.url}/#{self.id}"
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def create
|
95
|
+
response = Transport::JSON.request :post, @database.url, :body => @properties, :expected_status_code => 201
|
96
|
+
self.id = response["id"]
|
97
|
+
self.rev = response["rev"]
|
98
|
+
true
|
99
|
+
rescue Transport::UnexpectedStatusCodeError
|
100
|
+
false
|
101
|
+
end
|
102
|
+
|
103
|
+
def update
|
104
|
+
response = Transport::JSON.request :put, url, :body => @properties, :expected_status_code => 201
|
105
|
+
self.rev = response["rev"]
|
106
|
+
true
|
107
|
+
rescue Transport::UnexpectedStatusCodeError
|
108
|
+
false
|
109
|
+
end
|
110
|
+
|
111
|
+
def upgrade_unexpected_status_error(error)
|
112
|
+
raise NotFoundError if error.status_code == 404
|
113
|
+
raise error
|
114
|
+
end
|
115
|
+
|
116
|
+
def method_missing(method_name, *arguments, &block)
|
117
|
+
@properties.send method_name, *arguments, &block
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.create(*arguments)
|
121
|
+
model = new *arguments
|
122
|
+
model.save ? model : nil
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|