couchdb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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