throne 0.0.1 → 0.0.2
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/.gitignore +4 -1
- data/{README.md → README.rdoc} +25 -8
- data/TODO +21 -0
- data/VERSION +1 -1
- data/lib/throne/database.rb +28 -7
- data/lib/throne/tasks.rb +233 -30
- metadata +6 -5
- data/FEATURES +0 -5
data/.gitignore
CHANGED
data/{README.md → README.rdoc}
RENAMED
@@ -1,10 +1,20 @@
|
|
1
|
-
|
1
|
+
= ♚ Throne
|
2
2
|
|
3
3
|
The king is here - on his couch, covered in rubies.
|
4
4
|
|
5
5
|
Simple library for working with CouchDB
|
6
6
|
|
7
|
-
|
7
|
+
== Caution!
|
8
|
+
|
9
|
+
This code is in _heavy_ development, in conjunction with a few development projects.
|
10
|
+
This means the API can and probably will change substantially over the next few
|
11
|
+
releases as we work out what fits best. You have been warned.
|
12
|
+
|
13
|
+
== API documentation
|
14
|
+
|
15
|
+
can be found at http://rdoc.info/projects/lstoll/throne
|
16
|
+
|
17
|
+
== Basic Usage
|
8
18
|
|
9
19
|
Create a database object to work with. Will create the DB if it doesn't exist
|
10
20
|
|
@@ -33,21 +43,28 @@ Delete a document
|
|
33
43
|
|
34
44
|
@db.delete(document_id_or_document_object)
|
35
45
|
|
36
|
-
Run a design document
|
46
|
+
Run a design document/function
|
37
47
|
|
38
|
-
@db.
|
48
|
+
res = @db.function('_design/DesignDoc/_view/viewname')
|
49
|
+
res
|
39
50
|
=> An array of documents
|
51
|
+
res.offset
|
52
|
+
=> couchdb offset data
|
40
53
|
|
41
54
|
# with parameters
|
42
|
-
@db.
|
55
|
+
@db.function('_design/DD/_list/listname/viewname', :key => ab..fg, :xyz => 7)
|
43
56
|
=> An array of documents
|
44
57
|
|
45
58
|
# Iterator Method
|
46
|
-
@db.
|
59
|
+
@db.function('_design/DesignDoc/_view/viewname') do |doc|
|
47
60
|
#invoked for each document
|
48
61
|
p doc
|
49
62
|
end
|
50
63
|
|
64
|
+
# All documents
|
65
|
+
@db.function('_all_docs')
|
66
|
+
=> All docs in the database
|
67
|
+
|
51
68
|
Delete the database
|
52
69
|
|
53
70
|
@db.delete_database
|
@@ -56,7 +73,7 @@ Create the database
|
|
56
73
|
|
57
74
|
@db.create_database
|
58
75
|
|
59
|
-
|
76
|
+
== Note on Patches/Pull Requests
|
60
77
|
|
61
78
|
* Fork the project.
|
62
79
|
* Make your feature addition or bug fix.
|
@@ -66,6 +83,6 @@ Create the database
|
|
66
83
|
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
67
84
|
* Send me a pull request. Bonus points for topic branches.
|
68
85
|
|
69
|
-
|
86
|
+
== Copyright
|
70
87
|
|
71
88
|
Copyright (c) 2009 Lincoln Stoll, Ben Schwarz. See LICENSE for details.
|
data/TODO
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
* Update the get method to take an existing doc, and reload it. Use ETags
|
2
|
+
* At the top level retrieved, look for a RubyType field, and cast
|
3
|
+
* vague AR methods (Create, save, hooks)
|
4
|
+
|
5
|
+
* Model Thoughts:
|
6
|
+
* Everything extends Dash.
|
7
|
+
* When a new Document is instantiated, it's RubyClass property is set to its class
|
8
|
+
* It's converted to JSON just by calling to_json on the parent 'document' class
|
9
|
+
* When it's parsed in, the overridden/delegated get method checks the type of the
|
10
|
+
parent object, and in instantiates it, passing the JSON hash in as a constuctor.
|
11
|
+
* The super constructor then loops the sub items, instantiating when it see's a
|
12
|
+
RubyClass object, otherwise just setting the value.
|
13
|
+
* There should be a different parent class for Master documents and the sub items.
|
14
|
+
sub item's don't have save/get methods - only useful when loading from a view.
|
15
|
+
|
16
|
+
* Bulk fetch, and maybe bulk update.
|
17
|
+
* Bulk fetch, get method checks for array param. If it's an array, POST with keys
|
18
|
+
* bulk put, save method checks for array param, and bundles into docs array
|
19
|
+
|
20
|
+
* Attachments
|
21
|
+
* Just a put to url/attchment with data attached. Data in as a #read object?
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.2
|
data/lib/throne/database.rb
CHANGED
@@ -33,9 +33,17 @@ class Throne::Database
|
|
33
33
|
end
|
34
34
|
|
35
35
|
# gets a document by it's ID
|
36
|
+
#
|
37
|
+
# @param [String] docid the ID of the document to retrieve
|
38
|
+
# @param [String] rev (optional) the revision of the document to retrieve
|
39
|
+
# @return [Hash, nil] the document mapped to a hash, or nil if not found.
|
36
40
|
def get(docid, rev=nil)
|
37
|
-
|
38
|
-
|
41
|
+
begin
|
42
|
+
revurl = rev ? "?rev=#{rev}" : ""
|
43
|
+
JSON.parse(c.get(@url + '/' + docid + revurl))
|
44
|
+
rescue RestClient::ResourceNotFound
|
45
|
+
nil
|
46
|
+
end
|
39
47
|
end
|
40
48
|
|
41
49
|
# creates/updates a document from a hash/array structure
|
@@ -62,10 +70,11 @@ class Throne::Database
|
|
62
70
|
c.delete(@url + '/' + doc + '?rev=' + rev)
|
63
71
|
end
|
64
72
|
|
65
|
-
# runs a
|
66
|
-
def
|
67
|
-
url = @url + '/
|
73
|
+
# runs a function by path, with optional params passed in
|
74
|
+
def function(path, params = {}, &block)
|
75
|
+
url = @url + '/' + path
|
68
76
|
res = JSON.parse(c.get(paramify_url(url, params)))
|
77
|
+
res = Throne::ArrayWithFunctionMeta.new(res['rows'], res['offset'])
|
69
78
|
if block_given?
|
70
79
|
# TODO - stream properly
|
71
80
|
res.each do |i|
|
@@ -77,8 +86,6 @@ class Throne::Database
|
|
77
86
|
end
|
78
87
|
end
|
79
88
|
|
80
|
-
|
81
|
-
|
82
89
|
private
|
83
90
|
|
84
91
|
def paramify_url url, params = {}
|
@@ -96,7 +103,9 @@ class Throne::Database
|
|
96
103
|
def c; RestClient; end
|
97
104
|
end
|
98
105
|
|
106
|
+
# Extended string, to store the couch revision
|
99
107
|
class Throne::StringWithRevision < String
|
108
|
+
# Couch revision ID
|
100
109
|
attr_reader :revision
|
101
110
|
|
102
111
|
def initialize(id, rev)
|
@@ -104,3 +113,15 @@ class Throne::StringWithRevision < String
|
|
104
113
|
super(id)
|
105
114
|
end
|
106
115
|
end
|
116
|
+
|
117
|
+
# Extended array, to store the couch extra data
|
118
|
+
class Throne::ArrayWithFunctionMeta < DelegateClass(Array)
|
119
|
+
# Offset field as returned by couch
|
120
|
+
attr_reader :offset
|
121
|
+
|
122
|
+
def initialize(array, offset)
|
123
|
+
@offset = offset
|
124
|
+
super(array)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
data/lib/throne/tasks.rb
CHANGED
@@ -1,52 +1,254 @@
|
|
1
1
|
# Defines rake tasks for managing databases and views.
|
2
|
+
#
|
3
|
+
# Requires either a SERVER_URL or DB_URL defined as an environment variable.
|
4
|
+
# If SERVER_URL is set the database path is inferred from the directory the files
|
5
|
+
# are stored in, if DB_URL is set that database is explicitly used,
|
6
|
+
# overriding the name of the folder/task.
|
2
7
|
class Throne::Tasks
|
3
|
-
|
8
|
+
# Injects rake tasks for loading and dumping data from databases.
|
9
|
+
# @param [String] base_path the path where design docs are stored
|
10
|
+
def self.inject_data_tasks(base_path)
|
4
11
|
namespace :throne do
|
5
|
-
namespace :
|
12
|
+
namespace :load_data do
|
6
13
|
dirs_in_path(base_path).each do |dir, fulldir|
|
7
|
-
desc "Loads
|
14
|
+
desc "Loads data for database #{dir}"
|
8
15
|
task dir.to_sym do
|
9
|
-
|
10
|
-
items = []
|
11
|
-
# grab and parse all YML files
|
12
|
-
Dir.glob(fulldir + '/**/*.yml').each do |yml|
|
13
|
-
items << YAML::load(File.open(yml))
|
14
|
-
end
|
15
|
-
# and json
|
16
|
-
Dir.glob(fulldir + '/**/*.json').each do |json|
|
17
|
-
items << JSON.parse(File.open(json).read)
|
18
|
-
end
|
19
|
-
# load em up
|
20
|
-
items.each do |item|
|
21
|
-
if item.kind_of? Array
|
22
|
-
item.each do |doc|
|
23
|
-
@db.save(doc)
|
24
|
-
end
|
25
|
-
elsif item.kind_of? Hash
|
26
|
-
@db.save(item)
|
27
|
-
else
|
28
|
-
puts "There is something funky with the data for #{dir}"
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
p items[0]
|
16
|
+
load_data_for_database(url(dir), fulldir)
|
33
17
|
end
|
34
18
|
end
|
35
19
|
|
36
20
|
desc "load all dbs"
|
37
21
|
task :all => dirs_in_path(base_path)
|
38
22
|
end
|
23
|
+
namespace :dump_data do
|
24
|
+
desc "Dump data for a database into a YAML file"
|
25
|
+
task :yml, :db do |t, args|
|
26
|
+
dump_docs_from_db(args.db, base_path, :yml)
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "Dump data for a database into a JSON file"
|
30
|
+
task :json, :db do |t, args|
|
31
|
+
dump_docs_from_db(args.db, base_path, :json)
|
32
|
+
end
|
33
|
+
end
|
39
34
|
end
|
40
35
|
end
|
41
36
|
|
37
|
+
# This will inject Rake tasks for loading design docs into the DB
|
38
|
+
# The docs should be layed out in the following format:
|
39
|
+
# base_path/
|
40
|
+
# |-- <db name>
|
41
|
+
# `-- <design doc name>
|
42
|
+
# |-- lists
|
43
|
+
# | `-- statuses
|
44
|
+
# | `-- list.js
|
45
|
+
# `-- views
|
46
|
+
# `-- statuses
|
47
|
+
# |-- map.js
|
48
|
+
# `-- reduce.js
|
49
|
+
#
|
50
|
+
# @param [String] base_path the path where design docs are stored
|
42
51
|
def self.inject_design_doc_tasks(base_path)
|
43
52
|
namespace :throne do
|
44
|
-
|
45
|
-
|
53
|
+
namespace :push_design do
|
54
|
+
dirs_in_path(base_path).each do |dir, fulldir|
|
55
|
+
desc "pushes designs for database #{dir}"
|
56
|
+
task dir.to_sym do
|
57
|
+
load_design_documents(url(dir), fulldir)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
desc "push all designs"
|
62
|
+
task :all => dirs_in_path(base_path)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Injects tasks to create and delete databases
|
68
|
+
def self.inject_database_tasks(base_path)
|
69
|
+
namespace :throne do
|
70
|
+
desc "Creates a database if it doesn't exist"
|
71
|
+
task :createdb, :db do |t, args|
|
72
|
+
get_db(args).create_database
|
73
|
+
puts "Database at #{url(args.db)}"
|
74
|
+
end
|
75
|
+
|
76
|
+
desc "Deletes a database"
|
77
|
+
task :deletedb, :db do |t, args|
|
78
|
+
get_db(args).delete_database
|
79
|
+
puts "Database at #{url(args.db)}"
|
80
|
+
end
|
81
|
+
|
82
|
+
desc "Deletes, creates and re-loads a database"
|
83
|
+
task :rebuilddb, :db do |t, args|
|
84
|
+
db = get_db(args)
|
85
|
+
raise "you must specify the database name (task[db])" unless args.db
|
86
|
+
db.delete_database
|
87
|
+
# re-getting the object will create the DB
|
88
|
+
db = get_db(args)
|
89
|
+
# load design docs
|
90
|
+
load_design_documents(db.url, File.join(base_path, 'design', args.db))
|
91
|
+
# load data
|
92
|
+
load_data_for_database(db.url, File.join(base_path, 'data', args.db))
|
93
|
+
puts "Done."
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
def self.get_db(args)
|
98
|
+
args ? db = args.db : db = nil
|
99
|
+
unless (ENV['SERVER_URL'] && args.db) || ENV['DB_URL']
|
100
|
+
raise "You must specify DB_URL or task[db_name] and SERVER_URL"
|
101
|
+
end
|
102
|
+
Throne::Database.new(url(db))
|
46
103
|
end
|
47
104
|
end
|
48
105
|
end
|
49
106
|
|
107
|
+
# Injects all tasks. Related data files will be stores in base_path/data, and
|
108
|
+
# design docs in base_path/design
|
109
|
+
#
|
110
|
+
# @param [String] base_path path for db related files.
|
111
|
+
def self.inject_all_tasks(base_path)
|
112
|
+
inject_data_tasks(File.join(base_path, 'data'))
|
113
|
+
inject_design_doc_tasks(File.join(base_path, 'design'))
|
114
|
+
inject_database_tasks(base_path)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Loads design documents into the database url, extracted from the source path
|
118
|
+
# The docs should be layed out in the following format:
|
119
|
+
# base_path/
|
120
|
+
# |-- <db name>
|
121
|
+
# `-- <design doc name>
|
122
|
+
# |-- lists
|
123
|
+
# | `-- statuses
|
124
|
+
# | `-- list.js
|
125
|
+
# `-- views
|
126
|
+
# `-- statuses
|
127
|
+
# |-- map.js
|
128
|
+
# `-- reduce.js
|
129
|
+
# @param [String] db_url the url of the database to load the data in to
|
130
|
+
# @param [String] source_path the path to search for .yml and .json files
|
131
|
+
def self.load_design_documents(db_url, source_path)
|
132
|
+
# for each folder in base path, create a new design doc key
|
133
|
+
# create a lists key
|
134
|
+
# for each path in lists, add a key with the folder name
|
135
|
+
# inside this, there is a key called list, with the contents of the list function
|
136
|
+
# views is the same, except with a map and reduce function
|
137
|
+
Dir.glob(File.join(source_path, '*')).each do |doc_path|
|
138
|
+
doc_name = File.basename(doc_path)
|
139
|
+
doc = {'lists' => {}, 'views' => {}}
|
140
|
+
Dir.glob(File.join(doc_path, 'lists', '*')) do |list_path|
|
141
|
+
list_name = File.basename(list_path)
|
142
|
+
doc['lists'][list_name] = {}
|
143
|
+
listfn = File.join(list_path, 'list.js')
|
144
|
+
doc['lists'][list_name] =
|
145
|
+
File.read(listfn) if File.exists?(listfn)
|
146
|
+
end
|
147
|
+
Dir.glob(File.join(doc_path, 'views', '*')) do |view_path|
|
148
|
+
view_name = File.basename(view_path)
|
149
|
+
doc['views'][view_name] = {}
|
150
|
+
mapfn = File.join(view_path, 'map.js')
|
151
|
+
reducefn = File.join(view_path, 'reduce.js')
|
152
|
+
doc['views'][view_name]['map'] =
|
153
|
+
File.read(mapfn) if File.exists?(mapfn)
|
154
|
+
doc['views'][view_name]['reduce'] =
|
155
|
+
File.read(reducefn) if File.exists?(reducefn)
|
156
|
+
end
|
157
|
+
# try to get the existing doc
|
158
|
+
doc_id = "_design/#{doc_name}"
|
159
|
+
db = Throne::Database.new(db_url)
|
160
|
+
if svr_doc = db.get(doc_id)
|
161
|
+
# merge
|
162
|
+
doc = svr_doc.merge(doc)
|
163
|
+
else
|
164
|
+
doc['_id'] = doc_id
|
165
|
+
end
|
166
|
+
doc['language'] = 'javascript'
|
167
|
+
db.save(doc)
|
168
|
+
puts "Design documents from #{source_path} loaded"
|
169
|
+
end
|
170
|
+
|
171
|
+
# try and get a document with the design name
|
172
|
+
# if it's there, replace the lists and views keys with above data
|
173
|
+
# otherwise, create a new document, set language to javascript
|
174
|
+
# put document.
|
175
|
+
# WIN
|
176
|
+
end
|
177
|
+
|
178
|
+
# Loads data into the database url from the source path. Picks up .yml and .json
|
179
|
+
# @param [String] db_url the url of the database to load the data in to
|
180
|
+
# @param [String] source_path the path to search for .yml and .json files
|
181
|
+
def self.load_data_for_database(db_url, source_path)
|
182
|
+
@db = Throne::Database.new(db_url)
|
183
|
+
items = []
|
184
|
+
doccount = 0
|
185
|
+
# grab and parse all YML files
|
186
|
+
Dir.glob(source_path + '/**/*.yml').each do |yml|
|
187
|
+
items << YAML::load(File.open(yml))
|
188
|
+
end
|
189
|
+
# and json
|
190
|
+
Dir.glob(source_path + '/**/*.json').each do |json|
|
191
|
+
items << JSON.parse(File.open(json).read)
|
192
|
+
end
|
193
|
+
# load em up
|
194
|
+
items.each do |item|
|
195
|
+
if item.kind_of? Array
|
196
|
+
item.each do |doc|
|
197
|
+
begin
|
198
|
+
@db.save(doc)
|
199
|
+
rescue RestClient::RequestFailed => e
|
200
|
+
if e.message =~ /409$/
|
201
|
+
puts "Duplicate document - this data has probaby already been loaded"
|
202
|
+
doccount -= 1
|
203
|
+
else
|
204
|
+
raise e
|
205
|
+
end
|
206
|
+
end
|
207
|
+
doccount += 1
|
208
|
+
end
|
209
|
+
elsif item.kind_of? Hash
|
210
|
+
begin
|
211
|
+
@db.save(item)
|
212
|
+
rescue RestClient::RequestFailed => e
|
213
|
+
if e.message =~ /409$/
|
214
|
+
puts "Duplicate document - this data has probaby already been loaded"
|
215
|
+
doccount -= 1
|
216
|
+
else
|
217
|
+
raise e
|
218
|
+
end
|
219
|
+
end
|
220
|
+
doccount += 1
|
221
|
+
else
|
222
|
+
puts "There is something funky with the data for #{source_path}"
|
223
|
+
end
|
224
|
+
puts "#{doccount} document(s) loaded into database at #{@db.url}"
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def self.dump_docs_from_db(db, base_path, format)
|
229
|
+
raise "You must specify a DB name to dump task[dbname]" unless db
|
230
|
+
outdir = File.join(base_path, db)
|
231
|
+
Dir.mkdir(outdir) unless File.exists?(outdir)
|
232
|
+
@db = Throne::Database.new(url(db))
|
233
|
+
docs = []
|
234
|
+
|
235
|
+
@db.function('_all_docs') do |res|
|
236
|
+
docs << @db.get(res['key']) unless res['key'].match(/^_/)
|
237
|
+
end
|
238
|
+
|
239
|
+
outfn = format == :json ? 'dump.json' : 'dump.yml'
|
240
|
+
File.open(File.join(outdir, outfn), 'w') do |f|
|
241
|
+
if format == :json
|
242
|
+
f.puts docs.to_json
|
243
|
+
elsif format == :yml
|
244
|
+
f.puts docs.to_yaml
|
245
|
+
else
|
246
|
+
raise("Internal Error - invalid dump format specified")
|
247
|
+
end
|
248
|
+
end
|
249
|
+
puts "Data dumped to #{base_path + '/' + db + '/' + outfn}"
|
250
|
+
end
|
251
|
+
|
50
252
|
private
|
51
253
|
|
52
254
|
def self.dirs_in_path(path)
|
@@ -63,7 +265,8 @@ class Throne::Tasks
|
|
63
265
|
elsif svrurl = ENV['SERVER_URL']
|
64
266
|
return svrurl + '/' + dbname
|
65
267
|
else
|
66
|
-
raise("You must provide a SERVER_URL or
|
268
|
+
raise("You must provide a SERVER_URL or DB_URL")
|
67
269
|
end
|
68
270
|
end
|
271
|
+
|
69
272
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: throne
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lincoln Stoll
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-12-
|
12
|
+
date: 2009-12-14 00:00:00 +11:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -60,14 +60,15 @@ extensions: []
|
|
60
60
|
|
61
61
|
extra_rdoc_files:
|
62
62
|
- LICENSE
|
63
|
-
- README.
|
63
|
+
- README.rdoc
|
64
|
+
- TODO
|
64
65
|
files:
|
65
66
|
- .document
|
66
67
|
- .gitignore
|
67
|
-
- FEATURES
|
68
68
|
- LICENSE
|
69
|
-
- README.
|
69
|
+
- README.rdoc
|
70
70
|
- Rakefile
|
71
|
+
- TODO
|
71
72
|
- VERSION
|
72
73
|
- lib/throne.rb
|
73
74
|
- lib/throne/database.rb
|