couch_crumbs 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 2009-08-04
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
data/Manifest.txt ADDED
@@ -0,0 +1,29 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.rdoc
4
+ Rakefile
5
+ lib/couch_crumbs.rb
6
+ lib/core_ext/array.rb
7
+ lib/couch_crumbs/server.rb
8
+ lib/couch_crumbs/database.rb
9
+ lib/couch_crumbs/query.rb
10
+ lib/couch_crumbs/design.rb
11
+ lib/couch_crumbs/document.rb
12
+ lib/couch_crumbs/view.rb
13
+ lib/couch_crumbs/json/all.json
14
+ lib/couch_crumbs/json/children.json
15
+ lib/couch_crumbs/json/design.json
16
+ lib/couch_crumbs/json/simple.json
17
+ script/console
18
+ script/destroy
19
+ script/generate
20
+ spec/couch_crumbs_spec.rb
21
+ spec/core_ext/array_spec.rb
22
+ spec/couch_crumbs/database_spec.rb
23
+ spec/couch_crumbs/design_spec.rb
24
+ spec/couch_crumbs/document_spec.rb
25
+ spec/couch_crumbs/server_spec.rb
26
+ spec/couch_crumbs/view_spec.rb
27
+ spec/spec.opts
28
+ spec/spec_helper.rb
29
+ tasks/rspec.rake
data/README.rdoc ADDED
@@ -0,0 +1,131 @@
1
+ = couch_crumbs
2
+
3
+ * http://github.com/lmlo/couch_crumbs
4
+
5
+ == DESCRIPTION:
6
+
7
+ A small library for basic CouchDB document-based apps.
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ - objects are JSON derived hashes
12
+ (meaning properties support arrays, other hashes, dates, etc.)
13
+ - named properties
14
+ - timestamps (created_at/updated_at)
15
+ - life-cycle callbacks
16
+ - per-class database settings
17
+ - simple "finder" views
18
+ - advanced JavaScript views (in .json files)
19
+ - document relationships
20
+
21
+ == SYNOPSIS:
22
+
23
+ # Connect to a CouchDB server and set the default database
24
+ # :default_server is optional and defaults to http://couchdb.local/
25
+ # :default_database is also optional, and is used for classes
26
+ # that don't otherwise specify a database
27
+ CouchCrumbs::connect(:default_database => "example_database")
28
+
29
+ @server = CouchCrumbs.default_server
30
+
31
+ # Make sure we're running
32
+ @server.status
33
+
34
+ # Create or retrieve a database
35
+ @database = Database.new(:name => "example_database")
36
+
37
+
38
+ # Create a class
39
+ class Person
40
+
41
+ # Mixin document goodness
42
+ include CouchCrumbs::Document
43
+
44
+ # Declare some properties
45
+ property :name
46
+ property :title
47
+ property :status
48
+
49
+ # Add :created_at and :updated_at properties
50
+ timestamps!
51
+
52
+ child_document :address
53
+
54
+ end
55
+
56
+ class Address
57
+
58
+ include CouchCrumbs::Document
59
+
60
+ property :location
61
+
62
+ timestamps!
63
+
64
+ parent_document :person
65
+
66
+ end
67
+
68
+ # Instantiate a new Person document with properties
69
+ @person = Person.new(:name => "Steve")
70
+ # Manually set a property
71
+ @person.title = "CEO"
72
+ # Save the document (with #before_save and #after_save callbacks)
73
+ @person.save!
74
+
75
+
76
+ # Get a single document from the database
77
+ @steve = Person.get!(@person.id)
78
+
79
+
80
+ # Instantiate and save a new document in a single call (w/ callbacks)
81
+ @bill = Person.create!(:name => "Bill", :status => "Retired")
82
+
83
+ # Destroy a document
84
+ @bill.destroy!
85
+
86
+
87
+ # Add an address to a person
88
+ @address = Address.create!(:location => "123 Main, Anytown USA")
89
+
90
+ @steve.address = @address
91
+
92
+
93
+ == LIMITATIONS:
94
+
95
+ - lacks support for attachments
96
+ - lacks support for show functions
97
+ - lacks support for many other aspects of CouchDB
98
+ - both sides of an association must be saved before being set
99
+
100
+ == REQUIREMENTS:
101
+
102
+ sudo gem install rest_client, json, english
103
+
104
+ == INSTALL:
105
+
106
+ sudo gem install couch_crumbs
107
+
108
+ == LICENSE:
109
+
110
+ (The MIT License)
111
+
112
+ Copyright (c) 2009 CouchCrumbs project.
113
+
114
+ Permission is hereby granted, free of charge, to any person obtaining
115
+ a copy of this software and associated documentation files (the
116
+ 'Software'), to deal in the Software without restriction, including
117
+ without limitation the rights to use, copy, modify, merge, publish,
118
+ distribute, sublicense, and/or sell copies of the Software, and to
119
+ permit persons to whom the Software is furnished to do so, subject to
120
+ the following conditions:
121
+
122
+ The above copyright notice and this permission notice shall be
123
+ included in all copies or substantial portions of the Software.
124
+
125
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
126
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
127
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
128
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
129
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
130
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
131
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,37 @@
1
+ require 'rubygems'
2
+ gem 'hoe', '>= 2.1.0'
3
+ require 'hoe'
4
+ require 'fileutils'
5
+
6
+ Hoe.plugin :newgem
7
+ # Hoe.plugin :website
8
+ # Hoe.plugin :cucumberfeatures
9
+
10
+ # Generate all the Rake tasks
11
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
12
+ $hoe = Hoe.spec 'couch_crumbs' do
13
+ self.developer 'Often Void, Inc.', 'admin@oftenvoid.com'
14
+ self.post_install_message = 'PostInstall.txt' # TODO remove if post-install message not required
15
+ self.rubyforge_name = self.name # TODO this is default value
16
+ self.extra_deps = [['english']]
17
+ self.version = "0.0.1"
18
+ end
19
+
20
+ #require 'newgem/tasks'
21
+ Dir['tasks/**/*.rake'].each { |t| load t }
22
+
23
+ # TODO - want other tests/tasks run by default? Add them to the list
24
+ # remove_task :default
25
+ # task :default => [:spec, :features]
26
+
27
+ # Re-add the standard install task
28
+ task :install => [:install_gem]
29
+
30
+ # See: http://rspec.info/documentation/tools/rcov.html
31
+ desc "Run all examples with RCov"
32
+ Spec::Rake::SpecTask.new('coverage') do |t|
33
+ t.spec_opts = ['--format', 'specdoc', '-c']
34
+ t.spec_files = FileList['spec/**/*_spec.rb']
35
+ t.rcov = true
36
+ t.rcov_opts = ['--exclude', '/gems/,spec/']
37
+ end
@@ -0,0 +1,9 @@
1
+ class Array
2
+
3
+ # Add a destroy all method to arrays
4
+ #
5
+ def destroy!
6
+ each { |o| o.destroy! }
7
+ end
8
+
9
+ end
@@ -0,0 +1,68 @@
1
+ module CouchCrumbs
2
+
3
+ # Direct representation of a CouchDB database (contains documents).
4
+ #
5
+ class Database
6
+
7
+ include CouchCrumbs::Query
8
+
9
+ DEFAULT_NAME = :couch_crumbs_database.freeze
10
+
11
+ attr_accessor :server, :uri, :name, :status
12
+
13
+ # Get or create a database
14
+ #
15
+ def initialize(opts = {})
16
+ self.server = opts[:server] || CouchCrumbs::default_server
17
+ self.name = (opts[:name] || DEFAULT_NAME).to_s
18
+ self.uri = File.join(server.uri, self.name)
19
+
20
+ begin
21
+ self.status = RestClient.get(uri)
22
+ rescue RestClient::ResourceNotFound
23
+ RestClient.put(uri, "{}")
24
+ retry
25
+ end
26
+ end
27
+
28
+ # Return an array of all documents
29
+ #
30
+ def documents(opts = {})
31
+ # Query the special built-in _all_docs view
32
+ query_docs(File.join(uri, "_all_docs"), opts).collect do |doc|
33
+ # Regular documents
34
+ if doc["crumb_type"]
35
+ # Eval the class (with basic filtering, i.e. trusting your database)
36
+ eval(doc["crumb_type"].gsub(/\W/i, '').capitalize!).get!(doc["_id"])
37
+
38
+ elsif doc["_id"] =~ /^\_design\//
39
+ # Design docs
40
+ Design.get!(self, :id => doc["_id"])
41
+ else
42
+ # Ignore any other docs
43
+ warn "skipping unknown document: #{ doc }"
44
+
45
+ nil
46
+ end
47
+ end
48
+ end
49
+
50
+ # Return an array of only design documents
51
+ #
52
+ def design_documents
53
+ documents(:startkey => "_design/")
54
+ end
55
+
56
+ # Delete database from the server
57
+ #
58
+ def destroy!
59
+ freeze
60
+
61
+ result = JSON.parse(RestClient.delete(uri))
62
+
63
+ result["ok"]
64
+ end
65
+
66
+ end
67
+
68
+ end
@@ -0,0 +1,129 @@
1
+ module CouchCrumbs
2
+
3
+ # Represents "special" design documents
4
+ #
5
+ class Design
6
+
7
+ attr_accessor :raw, :uri
8
+
9
+ # Return a single design doc (from a specific database)
10
+ #
11
+ # ==== Parameters
12
+ # database<String>:: database instance
13
+ # opts => name<String>:: design doc name (i.e. "person")
14
+ # opts => id<String:: id of an existing design doc (i.e. "_design/person")
15
+ #
16
+ def self.get!(database, opts = {})
17
+ raise "opts must contain an :id or :name" unless (opts.has_key?(:id) || opts.has_key?(:name))
18
+
19
+ # Munge the URI from an :id or :name
20
+ uri = File.join(database.uri, (opts[:id] || "_design/#{ opts[:name] }"))
21
+
22
+ begin
23
+ # Try for an existing doc
24
+ result = RestClient.get(uri)
25
+
26
+ design = Design.new(database, :json => result)
27
+ rescue RestClient::ResourceNotFound
28
+ # Or create a new one
29
+ design = Design.new(database, :name => File.basename(uri))
30
+
31
+ design.save!
32
+ end
33
+
34
+ design
35
+ end
36
+
37
+ # Instantiate a new design document
38
+ #
39
+ # ==== Parameters
40
+ # database<String>:: database instance
41
+ # opts => json<String>:: raw json to init from
42
+ # opts => name<String>:: design doc name (i.e. "person")
43
+ #
44
+ def initialize(database, opts = {})
45
+ if opts.has_key?(:json)
46
+ self.raw = JSON.parse(opts[:json])
47
+ elsif opts.has_key?(:name)
48
+ # Read the design doc template
49
+ template = File.read(File.join(File.dirname(__FILE__), "json", "design.json"))
50
+
51
+ # Make our substitutions
52
+ template.gsub!(/\#design_id/, "_design/#{ opts[:name] }")
53
+
54
+ # Init the raw hash
55
+ self.raw = JSON.parse(template)
56
+ else
57
+ raise "#new must have :json or a :name supplied"
58
+ end
59
+
60
+ # Set out unique URI
61
+ self.uri = File.join(database.uri, id)
62
+ end
63
+
64
+ # Return design doc id (typically "_design/resource")
65
+ #
66
+ def id
67
+ raw["_id"]
68
+ end
69
+
70
+ # Return the revision
71
+ #
72
+ def rev
73
+ raw["_rev"]
74
+ end
75
+
76
+ # Return the design name (id - prefix, i.e. "resource")
77
+ #
78
+ def name
79
+ raw["_id"].split("/").last
80
+ end
81
+
82
+ # Save the design to the database
83
+ #
84
+ def save!
85
+ result = JSON.parse(RestClient.put(uri, raw.to_json))
86
+
87
+ # update our stats
88
+ raw["_id"] = result["id"]
89
+ raw["_rev"] = result["rev"]
90
+
91
+ result["ok"]
92
+ end
93
+
94
+ # Remove a design from the database
95
+ #
96
+ def destroy!
97
+ freeze
98
+
99
+ result = JSON.parse(RestClient.delete(File.join(uri, "?rev=#{ rev }")))
100
+
101
+ result["ok"]
102
+ end
103
+
104
+ # Return all views of this design doc
105
+ # opts => supply a name to select a specific view
106
+ #
107
+ def views(opts = {})
108
+ if opts.has_key?(:name)
109
+ View.new(self, opts[:name].to_s, {:name => raw["views"][opts[:name].to_s]}.to_json)
110
+ else
111
+ raw["views"].collect do |key, value|
112
+ View.new(self, key.to_s, {key => value}.to_json)
113
+ end
114
+ end
115
+ end
116
+
117
+ # Append a view to the view list
118
+ #
119
+ def add_view(view)
120
+ raise ArgumentError.new("view must be of kind View: #{ view }") unless view.kind_of?(View)
121
+
122
+ raw["views"].merge!(view.raw)
123
+
124
+ save!
125
+ end
126
+
127
+ end
128
+
129
+ end