makura 2009.05.27
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/CHANGELOG +340 -0
- data/COPYING +18 -0
- data/MANIFEST +42 -0
- data/README.md +41 -0
- data/Rakefile +32 -0
- data/bin/makura +12 -0
- data/example/blog.rb +50 -0
- data/example/couch/map/author_all.js +5 -0
- data/example/couch/map/author_posts.js +5 -0
- data/example/couch/map/post_all.js +5 -0
- data/example/couch/map/post_comments.js +5 -0
- data/example/couch/reduce/sum_length.js +7 -0
- data/lib/makura.rb +66 -0
- data/lib/makura/database.rb +216 -0
- data/lib/makura/design.rb +37 -0
- data/lib/makura/error.rb +14 -0
- data/lib/makura/http_methods.rb +19 -0
- data/lib/makura/layout.rb +64 -0
- data/lib/makura/model.rb +370 -0
- data/lib/makura/plugin/localize.rb +68 -0
- data/lib/makura/plugin/pager.rb +56 -0
- data/lib/makura/server.rb +203 -0
- data/lib/makura/uuid_cache.rb +23 -0
- data/lib/makura/version.rb +3 -0
- data/makura.gemspec +31 -0
- data/tasks/authors.rake +30 -0
- data/tasks/bacon.rake +66 -0
- data/tasks/changelog.rake +18 -0
- data/tasks/copyright.rake +21 -0
- data/tasks/gem.rake +22 -0
- data/tasks/gem_installer.rake +76 -0
- data/tasks/git.rake +46 -0
- data/tasks/grancher.rake +12 -0
- data/tasks/manifest.rake +4 -0
- data/tasks/metric_changes.rake +32 -0
- data/tasks/rcov.rake +23 -0
- data/tasks/release.rake +69 -0
- data/tasks/reversion.rake +8 -0
- data/tasks/todo.rake +27 -0
- data/tasks/traits.rake +21 -0
- data/tasks/yard.rake +4 -0
- data/tasks/ycov.rake +22 -0
- metadata +105 -0
data/bin/makura
ADDED
data/example/blog.rb
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require 'makura'
|
|
2
|
+
|
|
3
|
+
# Setting up everything
|
|
4
|
+
|
|
5
|
+
# Makura::Model.server = 'http://localhost:5984'
|
|
6
|
+
Makura::Model.database = 'mydb'
|
|
7
|
+
|
|
8
|
+
class Post
|
|
9
|
+
include Makura::Model
|
|
10
|
+
|
|
11
|
+
properties :title, :text, :tags
|
|
12
|
+
belongs_to :author
|
|
13
|
+
|
|
14
|
+
layout :all
|
|
15
|
+
|
|
16
|
+
save # submit design docs to CouchDB
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class Author
|
|
20
|
+
include Makura::Model
|
|
21
|
+
|
|
22
|
+
property :name
|
|
23
|
+
|
|
24
|
+
layout :posts, :reduce => :sum_length
|
|
25
|
+
layout :all
|
|
26
|
+
|
|
27
|
+
save
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class Comment
|
|
31
|
+
include Makura::Model
|
|
32
|
+
|
|
33
|
+
property :text
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# And here it goes.
|
|
37
|
+
|
|
38
|
+
author = Author.new('name' => 'Michael Fellinger')
|
|
39
|
+
author.save
|
|
40
|
+
|
|
41
|
+
post = Post.new(
|
|
42
|
+
:title => 'Hello, World!',
|
|
43
|
+
:text => 'This is my first post',
|
|
44
|
+
:author => author)
|
|
45
|
+
post.save
|
|
46
|
+
|
|
47
|
+
Post.view(:all).each do |post|
|
|
48
|
+
p post
|
|
49
|
+
p post.author
|
|
50
|
+
end
|
data/lib/makura.rb
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require 'pp'
|
|
2
|
+
require 'uri'
|
|
3
|
+
|
|
4
|
+
begin
|
|
5
|
+
require 'rubygems'
|
|
6
|
+
rescue LoadError
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require 'rest_client'
|
|
10
|
+
require 'json'
|
|
11
|
+
|
|
12
|
+
module Makura
|
|
13
|
+
ROOT = File.expand_path(File.dirname(__FILE__))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
unless $LOAD_PATH.any?{|lp| File.expand_path(lp) == Makura::ROOT }
|
|
17
|
+
$LOAD_PATH.unshift(Makura::ROOT)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
require 'makura/version'
|
|
21
|
+
require 'makura/error'
|
|
22
|
+
require 'makura/http_methods'
|
|
23
|
+
require 'makura/server'
|
|
24
|
+
require 'makura/database'
|
|
25
|
+
require 'makura/uuid_cache'
|
|
26
|
+
require 'makura/model'
|
|
27
|
+
require 'makura/design'
|
|
28
|
+
require 'makura/layout'
|
|
29
|
+
|
|
30
|
+
module Makura
|
|
31
|
+
CHARS = (48..128).map{|c| c.chr}.grep(/[[:alnum:]]/)
|
|
32
|
+
MOD = CHARS.size
|
|
33
|
+
|
|
34
|
+
module_function
|
|
35
|
+
|
|
36
|
+
# From Rack
|
|
37
|
+
def escape(s)
|
|
38
|
+
s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
|
|
39
|
+
'%'+$1.unpack('H2'*$1.size).join('%').upcase
|
|
40
|
+
}.tr(' ', '+')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def pretty_from_md5(md5)
|
|
44
|
+
id = md5.to_i(16)
|
|
45
|
+
o = []
|
|
46
|
+
while id > 0
|
|
47
|
+
id, r = id.divmod(MOD)
|
|
48
|
+
o.unshift CHARS[r]
|
|
49
|
+
end
|
|
50
|
+
o.join
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def pretty_to_md5(id)
|
|
54
|
+
i = 0
|
|
55
|
+
id.scan(/./) do |c|
|
|
56
|
+
i = i * MOD + CHARS.index(c)
|
|
57
|
+
end
|
|
58
|
+
i.to_s(16)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def constant(name, root = Module)
|
|
62
|
+
name.split('::').inject(root){|s,v| s.const_get(v) }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
Sofa = Makura # be backwards compatible
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
module Makura
|
|
2
|
+
class Database
|
|
3
|
+
include HTTPMethods
|
|
4
|
+
attr_accessor :server, :name
|
|
5
|
+
|
|
6
|
+
# Initialize instance of Database and create if it doesn't exist yet.
|
|
7
|
+
# To prevent automatic creation, pass false as 3rd parameter
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# server = Makura::Server.new
|
|
11
|
+
# # #<URI::HTTP:0xb7788234 URL:http://localhost:5984>
|
|
12
|
+
# database = Makura::Database.new(server, 'foo')
|
|
13
|
+
# # #<Makura::Database 'http://localhost:5984/foo'>
|
|
14
|
+
|
|
15
|
+
def initialize(server, name, auto_create = true)
|
|
16
|
+
@server, @name = server, name
|
|
17
|
+
create if auto_create
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Create the database if it doesn't exist already.
|
|
21
|
+
#
|
|
22
|
+
# Usage:
|
|
23
|
+
# server = Makura::Server.new
|
|
24
|
+
# # #<URI::HTTP:0xb76a4a98 URL:http://localhost:5984>
|
|
25
|
+
#
|
|
26
|
+
# database = Makura::Database.new(server, 'foo', false)
|
|
27
|
+
# # #<Makura::Database 'http://localhost:5984/foo'>
|
|
28
|
+
#
|
|
29
|
+
# database.create
|
|
30
|
+
# # {"update_seq"=>0, "doc_count"=>0, "purge_seq"=>0, "disk_size"=>4096,
|
|
31
|
+
# # "compact_running"=>false, "db_name"=>"foo", "doc_del_count"=>0}
|
|
32
|
+
|
|
33
|
+
def create
|
|
34
|
+
info
|
|
35
|
+
rescue Error::ResourceNotFound
|
|
36
|
+
put("/", :payload => '')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Will delete document in the CouchDB corresponding to given +doc+.
|
|
40
|
+
# Use #destroy to delete the database itself.
|
|
41
|
+
# Use #delete! to automatically rescue exceptions on conflicts.
|
|
42
|
+
#
|
|
43
|
+
# Possible variations (User is a Makura::Model) are:
|
|
44
|
+
#
|
|
45
|
+
# # deleting based on explicit _id and :rev option.
|
|
46
|
+
# database.delete('manveru', :rev => 123134)
|
|
47
|
+
#
|
|
48
|
+
# # deleting based on a Hash
|
|
49
|
+
# database.delete('_id' => 'manveru', '_rev' => 123134)
|
|
50
|
+
#
|
|
51
|
+
# user = User.new(:name => 'manveru')
|
|
52
|
+
# user.save
|
|
53
|
+
# database.delete(user)
|
|
54
|
+
#
|
|
55
|
+
# Usage when deleting document:
|
|
56
|
+
# doc = database.save('name' => 'manveru', 'time' => Time.now)
|
|
57
|
+
# # {"rev"=>"484030692", "id"=>"67e086087d5b7e7196b5c99174b0b66c", "ok"=>true}
|
|
58
|
+
#
|
|
59
|
+
# database[doc['id']]
|
|
60
|
+
# # {"name"=>"manveru", "_rev"=>"484030692",
|
|
61
|
+
# "time"=>"Sat Nov 22 16:37:50 +0900 2008",
|
|
62
|
+
# "_id"=>"67e086087d5b7e7196b5c99174b0b66c"}
|
|
63
|
+
#
|
|
64
|
+
# database.delete(doc['id'], :rev => doc['rev'])
|
|
65
|
+
# # {"rev"=>"2034883605", "id"=>"67e086087d5b7e7196b5c99174b0b66c", "ok"=>true}
|
|
66
|
+
#
|
|
67
|
+
# database[doc['id']]
|
|
68
|
+
# RestClient::ResourceNotFound: RestClient::ResourceNotFound
|
|
69
|
+
#
|
|
70
|
+
# database.delete(doc['id'], :rev => doc['rev'])
|
|
71
|
+
# Makura::RequestFailed: {"reason"=>"Document update conflict.", "error"=>"conflict"}
|
|
72
|
+
|
|
73
|
+
def delete(doc, opts = {})
|
|
74
|
+
case doc
|
|
75
|
+
when Makura::Model
|
|
76
|
+
doc_id, doc_rev = doc._id, doc._rev
|
|
77
|
+
when Hash
|
|
78
|
+
doc_id = doc['_id'] || doc['id'] || doc[:_id] || doc[:id]
|
|
79
|
+
doc_rev = doc['_rev'] || doc['rev'] || doc[:_rev] || doc[:rev]
|
|
80
|
+
else
|
|
81
|
+
doc_id = doc
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
raise(ArgumentError, "document _id wasn't passed") unless doc_id
|
|
85
|
+
|
|
86
|
+
doc_id = Makura.escape(doc_id)
|
|
87
|
+
opts[:rev] ||= doc_rev if doc_rev
|
|
88
|
+
|
|
89
|
+
request(:delete, doc_id.to_s, opts)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def delete!(doc, opts = {})
|
|
93
|
+
delete(doc, opts)
|
|
94
|
+
rescue Error::Conflict, Error::ResourceNotFound
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Delete the database itself.
|
|
98
|
+
#
|
|
99
|
+
# Usage:
|
|
100
|
+
# database.destroy
|
|
101
|
+
# # {"ok"=>true}
|
|
102
|
+
# database.info
|
|
103
|
+
# # RestClient::ResourceNotFound: RestClient::ResourceNotFound
|
|
104
|
+
|
|
105
|
+
def destroy(opts = {})
|
|
106
|
+
request(:delete, '/', opts)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def destroy!(opts = {})
|
|
110
|
+
destroy(opts)
|
|
111
|
+
rescue Error::ResourceNotFound
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def info
|
|
115
|
+
get('/')
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def all_docs(params = {})
|
|
119
|
+
get('_all_docs')
|
|
120
|
+
end
|
|
121
|
+
alias documents all_docs
|
|
122
|
+
|
|
123
|
+
def [](id, rev = nil)
|
|
124
|
+
id = Makura.escape(id)
|
|
125
|
+
if rev
|
|
126
|
+
get(id, :rev => rev)
|
|
127
|
+
else
|
|
128
|
+
get(id)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def []=(id, doc)
|
|
133
|
+
id = Makura.escape(id)
|
|
134
|
+
put(id, :payload => prepare_doc(doc))
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def temp_view(params = {})
|
|
138
|
+
params[:payload] = functions = {}
|
|
139
|
+
functions[:map] = params.delete(:map) if params[:map]
|
|
140
|
+
functions[:reduce] = params.delete(:reduce) if params[:reduce]
|
|
141
|
+
params['Content-Type'] = 'application/json'
|
|
142
|
+
|
|
143
|
+
post('_temp_view', params)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def view(layout, params = {})
|
|
147
|
+
get("_design/#{layout}", params)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def save(doc)
|
|
151
|
+
if id = doc['_id']
|
|
152
|
+
id = Makura.escape(id)
|
|
153
|
+
put(id, :payload => prepare_doc(doc))
|
|
154
|
+
else
|
|
155
|
+
id = doc['_id'] = @server.next_uuid
|
|
156
|
+
id = Makura.escape(id)
|
|
157
|
+
put(id, :payload => prepare_doc(doc))
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# NOTE:
|
|
162
|
+
# * Seems like we don't even need to check _id, CouchDB will assign it.
|
|
163
|
+
# But in order to use our own uuids we still do it.
|
|
164
|
+
def bulk_docs(docs)
|
|
165
|
+
docs.each{|doc| doc['_id'] ||= @server.next_uuid }
|
|
166
|
+
post("_bulk_docs", :payload => {:docs => docs})
|
|
167
|
+
end
|
|
168
|
+
alias bulk_save bulk_docs
|
|
169
|
+
|
|
170
|
+
def get_attachment(doc, file_id)
|
|
171
|
+
doc_id = doc.respond_to?(:_id) ? doc._id : doc.to_str
|
|
172
|
+
file_id, doc_id = Makura.escape(file_id), Makura.escape(doc_id)
|
|
173
|
+
|
|
174
|
+
get("#{doc_id}/#{file_id}", :raw => true)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# PUT an attachment directly to CouchDB
|
|
178
|
+
def put_attachment(doc, file_id, file, options = {})
|
|
179
|
+
doc_id, file_id = Makura.escape(doc._id), Makura.escape(file_id)
|
|
180
|
+
|
|
181
|
+
options[:payload] = file
|
|
182
|
+
options[:raw] = true
|
|
183
|
+
options[:rev] = doc._rev if doc._rev
|
|
184
|
+
|
|
185
|
+
put("#{doc_id}/#{file_id}", options)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def prepare_doc(doc)
|
|
189
|
+
if attachments = doc['_attachments']
|
|
190
|
+
doc['_attachments'] = encode_attachments(attachments)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
return doc
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def request(method, path, params = {})
|
|
197
|
+
@server.send(:request, method, "/#{name}/#{path}", params)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def encode_attachments(attachments)
|
|
201
|
+
attachments.each do |key, value|
|
|
202
|
+
next if value['stub']
|
|
203
|
+
value['data'] = base64(value['data'])
|
|
204
|
+
end
|
|
205
|
+
attachments
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def base64(data)
|
|
209
|
+
[data.to_s].pack('m').delete("\n")
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def inspect
|
|
213
|
+
"#<Makura::Database '#{@server.uri(name || '/')}'>"
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Makura
|
|
2
|
+
class Design
|
|
3
|
+
attr_accessor :database, :name, :language
|
|
4
|
+
attr_reader :layouts
|
|
5
|
+
|
|
6
|
+
def initialize(name, database = nil)
|
|
7
|
+
@name, @database = name, database
|
|
8
|
+
@language = 'javascript'
|
|
9
|
+
@layouts = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def save
|
|
13
|
+
hash = to_hash
|
|
14
|
+
doc = @database[hash['_id']]
|
|
15
|
+
doc['views'] = hash['views']
|
|
16
|
+
@database.save(doc)
|
|
17
|
+
rescue Makura::Error::ResourceNotFound
|
|
18
|
+
@database.save(to_hash)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def [](layout_name)
|
|
22
|
+
@layouts[layout_name.to_s]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def []=(layout_name, layout)
|
|
26
|
+
@layouts[layout_name.to_s] = layout
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_hash
|
|
30
|
+
views = {}
|
|
31
|
+
@layouts.each{|name, layout| views[name] = layout.to_hash }
|
|
32
|
+
views.delete_if{|k,v| !(v[:map] || v['map']) }
|
|
33
|
+
|
|
34
|
+
{'language' => @language, '_id' => "_design/#{@name}", 'views' => views}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/makura/error.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Makura
|
|
2
|
+
# Mother and namespace of all exceptions
|
|
3
|
+
class Error < ::RuntimeError
|
|
4
|
+
class ConnectionRefused < Error; end
|
|
5
|
+
class RequestFailed < Error; end
|
|
6
|
+
class ResourceNotFound < RequestFailed; end
|
|
7
|
+
class Conflict < RequestFailed; end
|
|
8
|
+
class MissingRevision < RequestFailed; end
|
|
9
|
+
class BadRequest < RequestFailed; end
|
|
10
|
+
class Authorization < RequestFailed; end
|
|
11
|
+
class NotFound < RequestFailed; end
|
|
12
|
+
class FileExists < RequestFailed; end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Makura
|
|
2
|
+
module HTTPMethods
|
|
3
|
+
def delete(path, params = {})
|
|
4
|
+
request(:delete, path, params)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def get(path, params = {})
|
|
8
|
+
request(:get, path, params)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def post(path, params = {})
|
|
12
|
+
request(:post, path, params)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def put(path, params = {})
|
|
16
|
+
request(:put, path, params)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|