couchobject 0.0.1

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