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