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