couch_crumbs 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/Manifest.txt +29 -0
- data/README.rdoc +131 -0
- data/Rakefile +37 -0
- data/lib/core_ext/array.rb +9 -0
- data/lib/couch_crumbs/database.rb +68 -0
- data/lib/couch_crumbs/design.rb +129 -0
- data/lib/couch_crumbs/document.rb +471 -0
- data/lib/couch_crumbs/json/all.json +5 -0
- data/lib/couch_crumbs/json/children.json +5 -0
- data/lib/couch_crumbs/json/design.json +5 -0
- data/lib/couch_crumbs/json/simple.json +5 -0
- data/lib/couch_crumbs/query.rb +95 -0
- data/lib/couch_crumbs/server.rb +45 -0
- data/lib/couch_crumbs/view.rb +73 -0
- data/lib/couch_crumbs.rb +45 -0
- data/script/console +11 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/spec/core_ext/array_spec.rb +21 -0
- data/spec/couch_crumbs/database_spec.rb +69 -0
- data/spec/couch_crumbs/design_spec.rb +103 -0
- data/spec/couch_crumbs/document_spec.rb +473 -0
- data/spec/couch_crumbs/server_spec.rb +63 -0
- data/spec/couch_crumbs/view_spec.rb +41 -0
- data/spec/couch_crumbs_spec.rb +18 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +20 -0
- data/tasks/rspec.rake +21 -0
- metadata +105 -0
data/History.txt
ADDED
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,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
|