couchobject 0.0.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.
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 0.0.1 2007-09-05
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
data/License.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 Johan Sørensen
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/Manifest.txt ADDED
@@ -0,0 +1,41 @@
1
+ History.txt
2
+ License.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ TODO
7
+ config/hoe.rb
8
+ config/requirements.rb
9
+ lib/couch_object.rb
10
+ lib/couch_object/database.rb
11
+ lib/couch_object/document.rb
12
+ lib/couch_object/model.rb
13
+ lib/couch_object/persistable.rb
14
+ lib/couch_object/response.rb
15
+ lib/couch_object/server.rb
16
+ lib/couch_object/utils.rb
17
+ lib/couch_object/version.rb
18
+ lib/couch_object/view.rb
19
+ log/debug.log
20
+ script/console
21
+ script/destroy
22
+ script/generate
23
+ setup.rb
24
+ spec/database_spec.rb
25
+ spec/document_spec.rb
26
+ spec/integration/database_integration_spec.rb
27
+ spec/integration/document_integration_spec.rb
28
+ spec/integration/integration_helper.rb
29
+ spec/model_spec.rb
30
+ spec/persistable_spec.rb
31
+ spec/response_spec.rb
32
+ spec/rspec_autotest.rb
33
+ spec/server_spec.rb
34
+ spec/spec.opts
35
+ spec/spec_helper.rb
36
+ spec/utils_spec.rb
37
+ spec/view_spec.rb
38
+ tasks/deployment.rake
39
+ tasks/environment.rake
40
+ tasks/rspec.rake
41
+ tasks/website.rake
data/README.txt ADDED
@@ -0,0 +1,119 @@
1
+ = CouchObject
2
+
3
+ CouchObject is a set of classes to help you talk to CouchDb (http://couchdbwiki.com/) with Ruby.
4
+
5
+ * Author: Johan Sørensen
6
+ * Contact: johan (at) johansorensen DOT com
7
+ * Home: http://rubyforge.org/projects/couchobject/
8
+ * Source (Git): http://repo.or.cz/w/couchobject.git
9
+
10
+ == Creating, opening and deleting databases
11
+
12
+ CouchObject::Database is the first interaction point to your CouchDb. Creating a CouchDb database:
13
+
14
+ >> CouchObject::Database.create!("http://localhost:8888", "mydb")
15
+ => {"ok"=>true}
16
+ >> CouchObject::Database.all_databases("http://localhost:8888")
17
+ => ["couchobject", "couchobject_test", "foo", "mydb"]
18
+ >> db = CouchObject::Database.open("http://localhost:8888/mydb")
19
+ => #<CouchObject::Database:0x642fa8 @server=#<CouchObject::Server:0x642ef4 @connection=#<Net::HTTP localhost:8888 open=false>, @port=8888, @uri=#<URI::HTTP:0x321612 URL:http://localhost:8888>, @host="localhost">, @uri="http://localhost:8888", @dbname="mydb">
20
+ >> db.name
21
+ => "mydb"
22
+ >> CouchObject::Database.delete!("http://localhost:8888", "mydb")
23
+ => {"ok"=>true}
24
+ >> CouchObject::Database.all_databases("http://localhost:8888").include?("mydb")
25
+ => false
26
+
27
+ === Interacting with the database
28
+
29
+ >> db.get("_all_docs")
30
+ => #<CouchObject::Response:0x14ed364 @response=#<Net::HTTPOK 200 OK readbody=true>, @parsed_body={"rows"=>[], "view"=>"_all_docs"}>
31
+
32
+ Issueing CouchObject::Database#get, CouchObject::Database#post, CouchObject::Database#put and CouchObject::Database#delete requests will return a CouchObject::Response object
33
+
34
+ >> db.get("_all_docs").body
35
+ => "{\"view\":\"_all_docs\", \"rows\":[\n\n]}"
36
+ >> db.get("_all_docs").parsed_body
37
+ => {"rows"=>[], "view"=>"_all_docs"}
38
+ >> db.post("", JSON.unparse({"foo" => "bar"}))
39
+ => #<CouchObject::Response:0x14d7780 @response=#<Net::HTTPCreated 201 Created readbody=true>, @parsed_body={"_rev"=>-992681820, "_id"=>"1206189E4496409DAD3818D241F5478F", "ok"=>true}>
40
+ >> db.get("_all_docs").parsed_body
41
+ => {"rows"=>[{"_rev"=>-992681820, "_id"=>"1206189E4496409DAD3818D241F5478F"}], "view"=>"_all_docs"}
42
+ >> db.get("1206189E4496409DAD3818D241F5478F").parsed_body
43
+ => {"_rev"=>-992681820, "_id"=>"1206189E4496409DAD3818D241F5478F", "foo"=>"bar"}
44
+ >> db.delete("1206189E4496409DAD3818D241F5478F").parsed_body
45
+ => {"_rev"=>548318611, "ok"=>true}
46
+ >> db.get("_all_docs").parsed_body
47
+ => {"rows"=>[], "view"=>"_all_docs"}
48
+
49
+ == The Document object
50
+
51
+ CouchObject::Document wraps a few things in a nice api. In particular you can use it if you don't want to deal with hashes all the time (similar to ActiveRecord and so on):
52
+
53
+ >> doc = CouchObject::Document.new({ "foo" => [1,2], "bar" => true })
54
+ => #<CouchObject::Document:0x14a7224 @id=nil, @attributes={"foo"=>[1, 2], "bar"=>true}, @revision=nil>
55
+ >> doc["foo"]
56
+ => [1, 2]
57
+ >> doc.foo
58
+ => [1, 2]
59
+ >> doc.bar
60
+ => true
61
+ >> doc.bar?
62
+ => true
63
+
64
+ You can also save a document to the database:
65
+
66
+ >> doc.new?
67
+ => true
68
+ >> doc.save(db)
69
+ => #<CouchObject::Response:0x149f358 @response=#<Net::HTTPCreated 201 Created readbody=true>, @parsed_body={"_rev"=>2030456697, "_id"=>"CAEADDC895AC4D506542A3796CCA355D", "ok"=>true}>
70
+ >> doc.id
71
+ => "CAEADDC895AC4D506542A3796CCA355D"
72
+
73
+ Since CouchObject::Database#get returns a CouchObject::Response object we can convert it into a Document instance easily with CouchObject::Database#to_document:
74
+
75
+ >> response = db.get(doc.id)
76
+ => #<CouchObject::Response:0x1498b98 @response=#<Net::HTTPOK 200 OK readbody=true>, @parsed_body={"_rev"=>2030456697, "_id"=>"CAEADDC895AC4D506542A3796CCA355D", "foo"=>[1, 2], "bar"=>true}>
77
+ >> the_doc_we_just_saved = response.to_document
78
+ => #<CouchObject::Document:0x148415c @id="CAEADDC895AC4D506542A3796CCA355D", @attributes={"foo"=>[1, 2], "bar"=>true}, @revision=2030456697>
79
+ >> the_doc_we_just_saved.foo
80
+ => [1, 2]
81
+ >> doc.foo = "quux"
82
+ => "quux"
83
+ >> doc.save(db)
84
+ => #<CouchObject::Response:0x4b0adc @response=#<Net::HTTPCreated 201 Created readbody=true>, @parsed_body={"_rev"=>1670064786, "_id"=>"B4077269D2DF8433D145DC0702B9791C", "ok"=>true}>
85
+
86
+
87
+ == CouchObject::Persistable
88
+
89
+ It all started with this module, it maps ruby objects to CouchDb documents, using two mapping methods. It's highly experimental and may go away n future releases
90
+
91
+ gem "couchobject"
92
+ require "couch_object"
93
+ class Bike
94
+ include CouchObject::Persistable
95
+
96
+ def initialize(wheels)
97
+ @wheels = wheels
98
+ end
99
+ attr_accessor :wheels
100
+
101
+ def to_couch
102
+ {:wheels => @wheels}
103
+ end
104
+
105
+ def self.from_couch(attributes)
106
+ new(attributes["wheels"])
107
+ end
108
+ end
109
+
110
+ By including the CouchObject::Persistable and defining two methods on our class we specify how we should serialize and deserialize our object to and from a CouchDb:
111
+
112
+ >> bike_4wd = Bike.new(4)
113
+ => #<Bike:0x6a0a68 @wheels=4>
114
+ >> bike_4wd.save("http://localhost:8888/couchobject")
115
+ => {"_rev"=>1745167971, "_id"=>"6FA2AFB623A93E0E77DEAAF59BB02565", "ok"=>true}
116
+ >> bike = Bike.get_by_id("http://localhost:8888/couchobject", bike_4wd.id)
117
+ => #<Bike:0x64846c @wheels=4>
118
+
119
+
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require 'config/requirements'
2
+ require 'config/hoe' # setup Hoe + all gem configuration
3
+
4
+ Dir['tasks/**/*.rake'].each { |rake| load rake }
data/TODO ADDED
@@ -0,0 +1,8 @@
1
+ - flat namespace for object attributes? (instead of the "attributes" key)
2
+ - Query/View API
3
+ - better handling of document id, including setting a custom ones
4
+ - Database#filter for filtering documents with Ruby (with some ruby2js or RubyToRuby magic)
5
+ - Expand on View class
6
+ - A Document object
7
+
8
+ - CouchObject::Model for more domain specific/clearer way to model Couch docs in ruby
data/config/hoe.rb ADDED
@@ -0,0 +1,73 @@
1
+ require 'couch_object/version'
2
+
3
+ AUTHOR = 'Johan Sørensen' # can also be an array of Authors
4
+ EMAIL = "johan@johansorensen.com"
5
+ DESCRIPTION = "CouchObject is a library that maps ruby objects to CouchDb documents"
6
+ GEM_NAME = 'couchobject' # what ppl will type to install your gem
7
+ RUBYFORGE_PROJECT = 'couchobject' # The unix name for your project
8
+ HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
9
+ DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
10
+
11
+ @config_file = "~/.rubyforge/user-config.yml"
12
+ @config = nil
13
+ RUBYFORGE_USERNAME = "unknown"
14
+ def rubyforge_username
15
+ unless @config
16
+ begin
17
+ @config = YAML.load(File.read(File.expand_path(@config_file)))
18
+ rescue
19
+ puts <<-EOS
20
+ ERROR: No rubyforge config file found: #{@config_file}"
21
+ Run 'rubyforge setup' to prepare your env for access to Rubyforge
22
+ - See http://newgem.rubyforge.org/rubyforge.html for more details
23
+ EOS
24
+ exit
25
+ end
26
+ end
27
+ RUBYFORGE_USERNAME.replace @config["username"]
28
+ end
29
+
30
+
31
+ REV = nil
32
+ # UNCOMMENT IF REQUIRED:
33
+ # REV = `svn info`.each {|line| if line =~ /^Revision:/ then k,v = line.split(': '); break v.chomp; else next; end} rescue nil
34
+ VERS = CouchObject::VERSION::STRING + (REV ? ".#{REV}" : "")
35
+ RDOC_OPTS = ['--quiet', '--title', 'couch_object documentation',
36
+ "--opname", "index.html",
37
+ "--line-numbers",
38
+ "--main", "README",
39
+ "--inline-source"]
40
+
41
+ class Hoe
42
+ def extra_deps
43
+ @extra_deps.reject! { |x| Array(x).first == 'hoe' }
44
+ @extra_deps
45
+ end
46
+ end
47
+
48
+ # Generate all the Rake tasks
49
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
50
+ hoe = Hoe.new(GEM_NAME, VERS) do |p|
51
+ p.author = AUTHOR
52
+ p.description = DESCRIPTION
53
+ p.email = EMAIL
54
+ p.summary = DESCRIPTION
55
+ p.url = HOMEPATH
56
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
57
+ p.test_globs = ["spec/**/spec_*.rb"]
58
+ p.clean_globs |= ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store'] #An array of file patterns to delete on clean.
59
+
60
+ # == Optional
61
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\\n\\n")
62
+ # An array of rubygem dependencies [name, version], e.g. [ ['active_support', '>= 1.3.1'] ]
63
+ p.extra_deps = [
64
+ ["json", ">= 1.1.1"]
65
+ ]
66
+
67
+ #p.spec_extras = {} # A hash of extra values to set in the gemspec.
68
+
69
+ end
70
+
71
+ CHANGES = hoe.paragraphs_of('History.txt', 0..1).join("\\n\\n")
72
+ PATH = (RUBYFORGE_PROJECT == GEM_NAME) ? RUBYFORGE_PROJECT : "#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
73
+ hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')
@@ -0,0 +1,17 @@
1
+ require 'fileutils'
2
+ include FileUtils
3
+
4
+ require 'rubygems'
5
+ %w[rake hoe newgem rubigen].each do |req_gem|
6
+ begin
7
+ require req_gem
8
+ rescue LoadError
9
+ puts "This Rakefile requires the '#{req_gem}' RubyGem."
10
+ puts "Installation: gem install #{req_gem} -y"
11
+ exit
12
+ end
13
+ end
14
+
15
+ $:.unshift(File.join(File.dirname(__FILE__), %w[.. lib]))
16
+
17
+ require 'couch_object'
@@ -0,0 +1,27 @@
1
+ $KCODE = 'u'
2
+ require 'jcode'
3
+
4
+ require "rubygems"
5
+ gem "json"
6
+ begin
7
+ require "json/ext"
8
+ rescue LoadError
9
+ $stderr.puts "C version of json (fjson) could not be loaded, using pure ruby one"
10
+ require "json/pure"
11
+ end
12
+ require 'json/add/core'
13
+
14
+ $:.unshift File.dirname(__FILE__)
15
+
16
+ require "couch_object/utils"
17
+ require "couch_object/document"
18
+ require "couch_object/response"
19
+ require "couch_object/server"
20
+ require "couch_object/database"
21
+ require "couch_object/view"
22
+ require "couch_object/persistable"
23
+ require "couch_object/model"
24
+
25
+ module CouchObject
26
+
27
+ end
@@ -0,0 +1,115 @@
1
+ module CouchObject
2
+ # A CouchDb database object
3
+ class Database
4
+ # Create a new database at +uri+ with the name if +dbname+
5
+ def self.create!(uri, dbname)
6
+ server = Server.new(uri)
7
+ response = Response.new(server.put("/#{dbname}", "")).parse
8
+ response.parsed_body
9
+ end
10
+
11
+ # Delete the database at +uri+ with the name if +dbname+
12
+ def self.delete!(uri, dbname)
13
+ server = Server.new(uri)
14
+ response = Response.new(server.delete("/#{dbname}")).parse
15
+ response.parsed_body
16
+ end
17
+
18
+ # All databases at +uri+
19
+ def self.all_databases(uri)
20
+ # FIXME: Move to Server ?
21
+ server = Server.new(uri)
22
+ resp = server.get("/_all_dbs")
23
+ response = Response.new(resp).parse
24
+ response.parsed_body
25
+ end
26
+
27
+ # Open a connection to the database at +uri+, where +uri+ is a full uri
28
+ # like: http://localhost:8888/foo
29
+ def self.open(uri)
30
+ uri = URI.parse(uri)
31
+ server_uri = "#{uri.scheme}://#{uri.host}:#{uri.port}"
32
+ new(server_uri, uri.path.tr("/", ""))
33
+ end
34
+
35
+ def initialize(uri, dbname)
36
+ @uri = uri
37
+ @dbname = dbname
38
+ @server = Server.new(@uri)
39
+ end
40
+ attr_accessor :server
41
+
42
+ # The full url of this database, eg http://localhost:8888/foo
43
+ def url
44
+ Utils.join_url(@uri, @dbname).to_s
45
+ end
46
+
47
+ # Name of this database
48
+ def name
49
+ @dbname.dup
50
+ end
51
+
52
+ # Send a GET request to the +path+ which is relative to the database path
53
+ # so calling with with "bar" as the path in the "foo_db" database will call
54
+ # http://host:port/foo_db/bar.
55
+ # Returns a Response object
56
+ def get(path)
57
+ Response.new(@server.get("/#{Utils.join_url(@dbname, path)}")).parse
58
+ end
59
+
60
+ # Send a POST request to the +path+ which is relative to the database path
61
+ # so calling with with "bar" as the path in the "foo_db" database will call
62
+ # http://host:port/foo_db/bar. The post body is the +payload+
63
+ # Returns a Response object
64
+ def post(path, payload)
65
+ Response.new(@server.post("/#{Utils.join_url(@dbname, path)}", payload)).parse
66
+ end
67
+
68
+ # Send a PUT request to the +path+ which is relative to the database path
69
+ # so calling with with "bar" as the path in the "foo_db" database will call
70
+ # http://host:port/foo_db/bar. The put body is the +payload+
71
+ # Returns a Response object
72
+ def put(path, payload="")
73
+ Response.new(@server.put("/#{Utils.join_url(@dbname, path)}", payload)).parse
74
+ end
75
+
76
+ # Send a DELETE request to the +path+ which is relative to the database path
77
+ # so calling with with "bar" as the path in the "foo_db" database will call
78
+ # http://host:port/foo_db/bar.
79
+ # Returns a Response object
80
+ def delete(path)
81
+ Response.new(@server.delete("/#{Utils.join_url(@dbname, path)}")).parse
82
+ end
83
+
84
+ # Get a document by id
85
+ def [](id)
86
+ get(id.to_s)
87
+ end
88
+
89
+ # Get a document by +id+, optionally a specific +revision+ too
90
+ def document(id, revision=nil)
91
+ if revision
92
+ get("#{id}?rev=#{revision}")
93
+ else
94
+ get(id.to_s)
95
+ end
96
+ end
97
+
98
+ # Returns an Array of all the documents in this db
99
+ def all_documents
100
+ resp = Response.new(get("_all_docs")).parse
101
+ resp.to_document.rows
102
+ end
103
+
104
+ # Queries the database with the block (using a temp. view)
105
+ def filter(&blk)
106
+
107
+ end
108
+
109
+ def views(view_name)
110
+ view = View.new(self, view_name)
111
+ view.query
112
+ end
113
+
114
+ end
115
+ end
@@ -0,0 +1,106 @@
1
+ module CouchObject
2
+ # Represents a CouchDb document
3
+ class Document
4
+ include Enumerable
5
+
6
+ # initializes a new document object with +attributes+ as
7
+ # the document values
8
+ def initialize(attributes={})
9
+ @attributes = attributes.dup
10
+ @id = @attributes.delete("_id")
11
+ @revision = @attributes.delete("_rev")
12
+ end
13
+ attr_accessor :attributes, :id, :revision
14
+
15
+ # Sets the id to +new_id+
16
+ # (Only for internal use really, but public nevertheless)
17
+ def id=(new_id)
18
+ if new?
19
+ attributes["_id"] = @id = new_id
20
+ else
21
+ nil
22
+ end
23
+ end
24
+
25
+ # is this a new document?
26
+ def new?
27
+ @id.nil? && @revision.nil?
28
+ end
29
+
30
+ # yields each document attribute
31
+ def each(&blk)
32
+ @attributes.each(&blk)
33
+ end
34
+
35
+ # Saves this document to the +database+
36
+ def save(database)
37
+ new? ? create(database) : update(database)
38
+ end
39
+
40
+ # Look up an attribute by +key+
41
+ def [](key)
42
+ attributes[key]
43
+ end
44
+
45
+ # Set an attributes by +key+ to +value+
46
+ def []=(key, value)
47
+ attributes[key] = value
48
+ end
49
+
50
+ # is the attribute +key+ in this document?
51
+ def has_key?(key)
52
+ attributes.has_key?(key)
53
+ end
54
+
55
+ # is the attribute +key+ in this document?
56
+ def respond_to?(meth)
57
+ method_name = meth.to_s
58
+
59
+ if has_key?(method_name)
60
+ return true
61
+ elsif %w[ ? = ].include?(method_name[-1..-1]) && has_key?(method_name[0..-2])
62
+ return true
63
+ end
64
+
65
+ super
66
+ end
67
+
68
+ # Converts the Document to a JSON representation of its attributes
69
+ def to_json(extra={})
70
+ if id.nil?
71
+ opts = {}.merge(extra)
72
+ else
73
+ opts = {"_id" => id}.merge(extra)
74
+ end
75
+ attributes.merge(opts).to_json
76
+ end
77
+
78
+ protected
79
+ def create(database)
80
+ response = database.post("", self.to_json)
81
+ # TODO error handling
82
+ @id = response.to_document.id
83
+ @revision = response.to_document.revision
84
+ response
85
+ end
86
+
87
+ def update(database)
88
+ response = database.put(id, self.to_json("_rev" => revision))
89
+ # TODO error handling
90
+ end
91
+
92
+ private
93
+ def method_missing(method_symbol, *arguments)
94
+ method_name = method_symbol.to_s
95
+
96
+ case method_name[-1..-1]
97
+ when "="
98
+ self[method_name[0..-2]] = arguments.first
99
+ when "?"
100
+ self[method_name[0..-2]] == true
101
+ else
102
+ has_key?(method_name) ? self[method_name] : super
103
+ end
104
+ end
105
+ end
106
+ end