manveru-makura 2009.02.18
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/COPYING +18 -0
- data/README.md +41 -0
- data/bin/makura +12 -0
- data/example/blog.rb +53 -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/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 +62 -0
- data/lib/makura/model.rb +372 -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.rb +62 -0
- data/makura.gemspec +41 -0
- metadata +91 -0
data/COPYING
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Copyright (c) 2008 Michael Fellinger <m.fellinger@gmail.com>
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to
|
|
5
|
+
deal in the Software without restriction, including without limitation the
|
|
6
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
7
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
|
11
|
+
all copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
16
|
+
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Makura
|
|
2
|
+
|
|
3
|
+
Makura is a Ruby wrapper around the CouchDB REST API.
|
|
4
|
+
|
|
5
|
+
It doesn't provide a lot of bells and whistles, but aims to be as close to the
|
|
6
|
+
original API as possible, while taking advantage of Ruby's expressive power.
|
|
7
|
+
|
|
8
|
+
Most ideas for this have been gathered while trying other libraries such as
|
|
9
|
+
CouchObject, CouchRest, and RelaxDB.
|
|
10
|
+
|
|
11
|
+
It does so with almost no modification of ruby libraries and makes it simple
|
|
12
|
+
to switch the HTTP library used by changing one method.
|
|
13
|
+
Eventually Makura will be using an evented http library to provide better
|
|
14
|
+
performance.
|
|
15
|
+
|
|
16
|
+
We are using the json library, which adds following methods:
|
|
17
|
+
To Kernel: #j, #jj, #JSON
|
|
18
|
+
To Object and most core classes: #to_json
|
|
19
|
+
To String: #to_json_raw_object, #to_json, #to_json_raw
|
|
20
|
+
|
|
21
|
+
## Dependencies
|
|
22
|
+
|
|
23
|
+
* CouchDB - 0.9 trunk (rev 725909 and higher)
|
|
24
|
+
* rest-client
|
|
25
|
+
* rack
|
|
26
|
+
* json
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
* Simple Models, the CouchDB way and without magic.
|
|
31
|
+
* Free choice of inheritance, just include the Makura::Model module.
|
|
32
|
+
* Smart interpretation of returned JSON.
|
|
33
|
+
* Direct mapping of javascript files to map/reduce functions for views.
|
|
34
|
+
* CouchDB specific error reporting, no meaningless HTTP status code.
|
|
35
|
+
* Live update of views during runtime.
|
|
36
|
+
* Easy configuration, possibility to use different servers and databases each
|
|
37
|
+
model.
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
See the /example/blog.rb
|
data/bin/makura
ADDED
data/example/blog.rb
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
validates(:title){ presence and length :within => (3..100) }
|
|
17
|
+
validates(:text){ presence }
|
|
18
|
+
|
|
19
|
+
save # submit design docs to CouchDB
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class Author
|
|
23
|
+
include Makura::Model
|
|
24
|
+
|
|
25
|
+
property :name
|
|
26
|
+
|
|
27
|
+
layout :posts, :reduce => :sum_length
|
|
28
|
+
layout :all
|
|
29
|
+
|
|
30
|
+
save
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class Comment
|
|
34
|
+
include Makura::Model
|
|
35
|
+
|
|
36
|
+
property :text
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# And here it goes.
|
|
40
|
+
|
|
41
|
+
author = Author.new('name' => 'Michael Fellinger')
|
|
42
|
+
author.save
|
|
43
|
+
|
|
44
|
+
post = Post.new(
|
|
45
|
+
:title => 'Hello, World!',
|
|
46
|
+
:text => 'This is my first post',
|
|
47
|
+
:author => author)
|
|
48
|
+
post.save
|
|
49
|
+
|
|
50
|
+
Post.view(:all).each do |post|
|
|
51
|
+
p post
|
|
52
|
+
p post.author
|
|
53
|
+
end
|
|
@@ -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("_view/#{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
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module Makura
|
|
2
|
+
class Layout
|
|
3
|
+
attr_accessor :design, :name, :map, :reduce
|
|
4
|
+
|
|
5
|
+
PATH = [
|
|
6
|
+
'./couch',
|
|
7
|
+
File.join(Makura::ROOT, '../couch')
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
def initialize(name, design = nil)
|
|
11
|
+
@name, @design = name, design
|
|
12
|
+
@design[name] = self
|
|
13
|
+
@map = @reduce = nil
|
|
14
|
+
@options = {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def load_proto_map(file_or_function, replace = {})
|
|
18
|
+
return unless common_load(:proto_map, file_or_function)
|
|
19
|
+
replace.each{|from, to| @proto_map.gsub!(/"\{\{#{from}\}\}"/, to) }
|
|
20
|
+
@map = @proto_map
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def load_proto_reduce(file_or_function, replace = {})
|
|
24
|
+
return unless common_load(:proto_reduce, file_or_function)
|
|
25
|
+
replace.each{|from, to| @proto_reduce.gsub!(/"\{\{#{from}\}\}"/, to) }
|
|
26
|
+
@reduce = @proto_reduce
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def load_map(file_or_function)
|
|
30
|
+
common_load(:map, file_or_function)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def load_reduce(file_or_function)
|
|
34
|
+
common_load(:reduce, file_or_function)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def common_load(root, file_or_function)
|
|
38
|
+
return unless file_or_function
|
|
39
|
+
|
|
40
|
+
if file_or_function =~ /function\(.*\)/
|
|
41
|
+
function = file_or_function
|
|
42
|
+
else
|
|
43
|
+
filename = "#{root}/#{file_or_function}.js"
|
|
44
|
+
|
|
45
|
+
if pathname = PATH.find{|pa| File.file?(File.join(pa, filename)) }
|
|
46
|
+
function = File.read(File.join(pathname, filename))
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
instance_variable_set("@#{root}", function) if function
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def save
|
|
54
|
+
@design[@name] = self.to_hash
|
|
55
|
+
@design.save
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def to_hash
|
|
59
|
+
{:map => @map, :reduce => @reduce, :makura_options => @options}
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/makura/model.rb
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
module Makura
|
|
2
|
+
module Model
|
|
3
|
+
KEY = 'makura_type'
|
|
4
|
+
|
|
5
|
+
class << self
|
|
6
|
+
attr_reader :server, :database
|
|
7
|
+
|
|
8
|
+
def database=(name)
|
|
9
|
+
@database = server.database(name)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def server=(obj)
|
|
13
|
+
case obj
|
|
14
|
+
when Makura::Server
|
|
15
|
+
@server = obj
|
|
16
|
+
when String, URI
|
|
17
|
+
@server = Makura::Server.new(obj)
|
|
18
|
+
else
|
|
19
|
+
raise ArgumentError
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def server
|
|
24
|
+
return @server if @server
|
|
25
|
+
self.server = Makura::Server.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def included(into)
|
|
29
|
+
into.extend(SingletonMethods)
|
|
30
|
+
into.send(:include, InstanceMethods)
|
|
31
|
+
into.makura_relation = {:belongs_to => {}, :has_many => {}}
|
|
32
|
+
into.property_type = {}
|
|
33
|
+
into.defaults = {'type' => into.name}
|
|
34
|
+
into.properties(:_id, :_rev, :type)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
module InstanceMethods
|
|
39
|
+
def initialize(hash = {})
|
|
40
|
+
@_hash = self.class.defaults.dup
|
|
41
|
+
merge!(hash)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def merge!(hash)
|
|
45
|
+
case hash
|
|
46
|
+
when Makura::Model
|
|
47
|
+
merge!(hash.to_hash)
|
|
48
|
+
when Hash
|
|
49
|
+
hash.each{|key, value|
|
|
50
|
+
meth = "#{key}="
|
|
51
|
+
|
|
52
|
+
if respond_to?(meth)
|
|
53
|
+
self.send("#{key}=", value)
|
|
54
|
+
else
|
|
55
|
+
self[key.to_s] = value
|
|
56
|
+
end
|
|
57
|
+
}
|
|
58
|
+
else
|
|
59
|
+
raise ArgumentError, "This is neither relation data nor an Hash"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def [](key)
|
|
64
|
+
@_hash[key.to_s]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def []=(key, value)
|
|
68
|
+
@_hash[key.to_s] = value
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def to_hash
|
|
72
|
+
@_hash.dup
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def to_json
|
|
76
|
+
@_hash.to_json
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def inspect
|
|
80
|
+
"#<#{self.class} #{@_hash.inspect}>"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def pretty_print(o)
|
|
84
|
+
["#<#{self.class} ", @_hash, ">"].each{|e| e.pretty_print(o) }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def saved?
|
|
88
|
+
self['_rev']
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def save
|
|
92
|
+
return if not valid? if respond_to?(:valid)
|
|
93
|
+
return if saved?
|
|
94
|
+
save!
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def save!
|
|
98
|
+
hash = self.to_hash
|
|
99
|
+
|
|
100
|
+
self.class.makura_relation.each do |kind, relation_hash|
|
|
101
|
+
relation_hash.each do |key, value|
|
|
102
|
+
hash[key.to_s] = hash[key.to_s] #._id
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
response = self.class.database.save(hash)
|
|
107
|
+
self._rev = response['rev']
|
|
108
|
+
self._id = response['id']
|
|
109
|
+
|
|
110
|
+
return self
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# path, file, args = {})
|
|
114
|
+
def attach(*args)
|
|
115
|
+
self.class.database.put_attachment(self, *args)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# delete attachment by name.
|
|
119
|
+
# we make sure the parameter is given and a nonempty string to avoid
|
|
120
|
+
# destroying the document itself
|
|
121
|
+
def detach(name)
|
|
122
|
+
name.strip!
|
|
123
|
+
return if name.empty?
|
|
124
|
+
self.class.database.request(:delete, "#{_id}/#{name}", :rev => _rev)
|
|
125
|
+
rescue Makura::Error::Conflict
|
|
126
|
+
self['_rev'] = self.class[self._id]['_rev']
|
|
127
|
+
retry
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def destroy
|
|
131
|
+
self.class.database.delete(_id, :rev => _rev)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def ==(obj)
|
|
135
|
+
self.class == obj.class and self._id == obj._id
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def hash
|
|
139
|
+
@_hash.hash
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def eql?(other)
|
|
143
|
+
other == self && other.hash == self.hash
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def clone
|
|
147
|
+
hash = @_hash.dup
|
|
148
|
+
hash.delete('_id')
|
|
149
|
+
hash.delete('_rev')
|
|
150
|
+
self.class.new(hash)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
module SingletonMethods
|
|
155
|
+
attr_accessor :defaults, :makura_relation, :property_type
|
|
156
|
+
|
|
157
|
+
def plugin(name)
|
|
158
|
+
require "makura/plugin/#{name}".downcase
|
|
159
|
+
|
|
160
|
+
name = name.to_s.capitalize
|
|
161
|
+
mod = Makura::Plugin.const_get(name)
|
|
162
|
+
|
|
163
|
+
include(mod::InstanceMethods) if defined?(mod::InstanceMethods)
|
|
164
|
+
extend(mod::SingletonMethods) if defined?(mod::SingletonMethods)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def database=(name)
|
|
168
|
+
@database = Makura::Model.server.database(name)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def database
|
|
172
|
+
@database || Makura::Model.database
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def properties(*names)
|
|
176
|
+
names.each{|name| property(name) }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def property(name, opts = {})
|
|
180
|
+
name = name.to_s
|
|
181
|
+
defaults[name] = default = opts.delete(:default) if opts[:default]
|
|
182
|
+
property_type[name] = type = opts.delete(:type) if opts[:type]
|
|
183
|
+
|
|
184
|
+
if type == Time
|
|
185
|
+
code = "
|
|
186
|
+
def #{name}()
|
|
187
|
+
Time.at(@_hash[#{name.dump}].to_i)
|
|
188
|
+
end
|
|
189
|
+
def #{name}=(obj)
|
|
190
|
+
@_hash[#{name.dump}] = obj.to_i
|
|
191
|
+
end"
|
|
192
|
+
class_eval(code)
|
|
193
|
+
else
|
|
194
|
+
code = "
|
|
195
|
+
def #{name}() @_hash[#{name.dump}] end
|
|
196
|
+
def #{name}=(obj) @_hash[#{name.dump}] = obj end"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
class_eval(code)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def id(name)
|
|
203
|
+
@id = name
|
|
204
|
+
class_eval("
|
|
205
|
+
alias #{name} _id
|
|
206
|
+
alias #{name}= _id=")
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def belongs_to(name, model = nil)
|
|
210
|
+
name = name.to_s
|
|
211
|
+
klass = (model || name.capitalize).to_s
|
|
212
|
+
@makura_relation[:belongs_to][name] = klass
|
|
213
|
+
|
|
214
|
+
class_eval("
|
|
215
|
+
def #{name}()
|
|
216
|
+
@#{name} ||= #{klass}[self[#{name.dump}]]
|
|
217
|
+
end
|
|
218
|
+
def #{name}=(obj)
|
|
219
|
+
if obj.respond_to?(:_id)
|
|
220
|
+
@_hash[#{name.dump}] = obj._id
|
|
221
|
+
else
|
|
222
|
+
@_hash[#{name.dump}] = obj
|
|
223
|
+
end
|
|
224
|
+
end")
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def has_many(name, model = nil)
|
|
228
|
+
name = name.to_s
|
|
229
|
+
klass = (model || name.capitalize).to_s
|
|
230
|
+
@makura_relation[:has_many][name] = klass
|
|
231
|
+
|
|
232
|
+
class_eval("
|
|
233
|
+
def #{name}()
|
|
234
|
+
@#{name} ||= #{klass}[self[#{name.dump}]]
|
|
235
|
+
end
|
|
236
|
+
def #{name}=(obj)
|
|
237
|
+
return unless obj
|
|
238
|
+
raise RuntimeError, 'You many not assign here'
|
|
239
|
+
end")
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def [](id, rev = nil)
|
|
243
|
+
new(database[id, rev])
|
|
244
|
+
rescue Error::ResourceNotFound
|
|
245
|
+
nil
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def design
|
|
249
|
+
@design ||= Design.new(name.to_s, database)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def layout(name, opts = {})
|
|
253
|
+
design[name] = layout = Layout.new(name, design)
|
|
254
|
+
|
|
255
|
+
map_name = opts[:map] || "#{self.name}_#{name}".downcase
|
|
256
|
+
reduce_name = opts[:reduce] || "#{self.name}_#{name}".downcase
|
|
257
|
+
|
|
258
|
+
layout.load_map(map_name)
|
|
259
|
+
layout.load_reduce(reduce_name)
|
|
260
|
+
|
|
261
|
+
return layout
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def proto_layout(common, name, opts = {})
|
|
265
|
+
design[name] = layout = Layout.new(name, design)
|
|
266
|
+
|
|
267
|
+
map_name = opts.delete(:map) || "#{self.name}_#{common}".downcase
|
|
268
|
+
reduce_name = opts.delete(:reduce) || "#{self.name}_#{common}".downcase
|
|
269
|
+
|
|
270
|
+
layout.load_proto_map(map_name, opts)
|
|
271
|
+
layout.load_proto_reduce(reduce_name, opts)
|
|
272
|
+
|
|
273
|
+
return layout
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def save
|
|
277
|
+
design.save
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# +opts+ must include a :keys or 'keys' key with something that responds
|
|
281
|
+
# to #to_a as value
|
|
282
|
+
#
|
|
283
|
+
# Usage given a map named `Post/by_tags' that does something like:
|
|
284
|
+
#
|
|
285
|
+
# for(t in doc.tags){ emit([doc.tags[t]], null); }
|
|
286
|
+
#
|
|
287
|
+
# You can use this like:
|
|
288
|
+
#
|
|
289
|
+
# keys = ['ruby', 'couchdb']
|
|
290
|
+
# Post.multi_fetch(:by_tags, :keys => keys)
|
|
291
|
+
#
|
|
292
|
+
# And it will return all docs with the tags 'ruby' OR 'couchdb'
|
|
293
|
+
# This can be extended to match even more complex things
|
|
294
|
+
#
|
|
295
|
+
# for(t in doc.tags){ emit([doc.author, doc.tags[t]], null); }
|
|
296
|
+
#
|
|
297
|
+
# Now we do
|
|
298
|
+
#
|
|
299
|
+
# keys = [['manveru', 'ruby'], ['mika', 'couchdb']]
|
|
300
|
+
# Post.multi_fetch(:by_tags, :keys => keys)
|
|
301
|
+
#
|
|
302
|
+
# This will return all docs match following:
|
|
303
|
+
# ((author == 'manveru' && tags.include?('ruby')) ||
|
|
304
|
+
# (author == 'mika' && tags.include?('couchdb')))
|
|
305
|
+
#
|
|
306
|
+
# Of course you can add as many keys as you like:
|
|
307
|
+
#
|
|
308
|
+
# keys = [['manveru', 'ruby'],
|
|
309
|
+
# ['manveru', 'couchdb'],
|
|
310
|
+
# ['mika', 'design']]
|
|
311
|
+
# ['mika', 'couchdb']]
|
|
312
|
+
# Post.multi_fetch(:by_tags, :keys => keys)
|
|
313
|
+
#
|
|
314
|
+
#
|
|
315
|
+
# From http://wiki.apache.org/couchdb/HTTP_view_API
|
|
316
|
+
# A JSON structure of {"keys": ["key1", "key2", ...]} can be posted to
|
|
317
|
+
# any user defined view or _all_docs to retrieve just the view rows
|
|
318
|
+
# matching that set of keys. Rows are returned in the order of the keys
|
|
319
|
+
# specified. Combining this feature with include_docs=true results in
|
|
320
|
+
# the so-called multi-document-fetch feature.
|
|
321
|
+
|
|
322
|
+
def multi_fetch(name, opts = {})
|
|
323
|
+
keys = opts.delete(:keys) || opts.delete('keys')
|
|
324
|
+
opts.merge!(:payload => {'keys' => keys.to_a})
|
|
325
|
+
hash = database.post("_view/#{self}/#{name}", opts)
|
|
326
|
+
convert_raw(hash['rows'])
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def multi_fetch_with_docs(name, opts = {})
|
|
330
|
+
opts.merge!(:include_docs => true, :reduce => false)
|
|
331
|
+
multi_fetch(name, opts)
|
|
332
|
+
end
|
|
333
|
+
alias multi_document_fetch multi_fetch_with_docs
|
|
334
|
+
|
|
335
|
+
# It is generally recommended not to include the doc in the emit of the
|
|
336
|
+
# map function but to use include_docs=true.
|
|
337
|
+
# To make using this approach more convenient use this method.
|
|
338
|
+
|
|
339
|
+
def view_with_docs(name, opts = {})
|
|
340
|
+
opts.merge!(:include_docs => true, :reduce => false)
|
|
341
|
+
view(name, opts)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
alias view_docs view_with_docs
|
|
345
|
+
|
|
346
|
+
def view(name, opts = {})
|
|
347
|
+
flat = opts.delete(:flat)
|
|
348
|
+
hash = database.view("#{self}/#{name}", opts)
|
|
349
|
+
|
|
350
|
+
convert_raw(hash['rows'], flat)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def convert_raw(rows, flat = false)
|
|
354
|
+
rows.map do |row|
|
|
355
|
+
value = row['doc'] || row['value']
|
|
356
|
+
|
|
357
|
+
if value.respond_to?(:to_hash)
|
|
358
|
+
if type = value['type'] and not flat
|
|
359
|
+
const_get(type).new(value)
|
|
360
|
+
else
|
|
361
|
+
row
|
|
362
|
+
end
|
|
363
|
+
elsif not row['key']
|
|
364
|
+
value
|
|
365
|
+
else
|
|
366
|
+
row
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Makura
|
|
2
|
+
module Plugin
|
|
3
|
+
module Pager
|
|
4
|
+
module SingletonMethods
|
|
5
|
+
def pager(page, limit)
|
|
6
|
+
Makura::Plugin::Pager::Pagination.new(self, :pager, page, limit)
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class Pagination
|
|
11
|
+
def initialize(model, view, page, limit)
|
|
12
|
+
@model, @view, @page, @limit = model, view, page, limit
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# /pager/_all_docs?count=10&group=true
|
|
16
|
+
# /pager/_all_docs?startkey=%224f9dca1c66121f9320a69553546db07a%22&startkey_docid=4f9dca1c66121f9320a69553546db07a&skip=1&descending=false&count=10&group=true
|
|
17
|
+
# /pager/_all_docs?startkey=%22_design%2FUser%22&startkey_docid=_design%2FUser&skip=1&descending=false&count=10&group=true
|
|
18
|
+
# /pager/_all_docs?startkey=%22d850f0801686b85035680bb6f38d5c5c%22&startkey_docid=d850f0801686b85035680bb6f38d5c5c&skip=1&descending=false&count=10&group=true
|
|
19
|
+
|
|
20
|
+
# NOTE:
|
|
21
|
+
# * descending should be true if you page backwards
|
|
22
|
+
|
|
23
|
+
include Enumerable
|
|
24
|
+
|
|
25
|
+
def each(start_id = nil, descending = false, &block)
|
|
26
|
+
opts = {
|
|
27
|
+
:count => @limit,
|
|
28
|
+
:group => true,
|
|
29
|
+
:descending => descending,
|
|
30
|
+
# :include_docs => true,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if start_id
|
|
34
|
+
opts[:skip] = 1
|
|
35
|
+
opts[:startkey_docid] = start_id
|
|
36
|
+
opts[:startkey] = start_id
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
@model.view(@view, opts).each(&block)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def count
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def first_page?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def last_page?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def empty?
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
module Makura
|
|
2
|
+
class Server
|
|
3
|
+
include HTTPMethods
|
|
4
|
+
attr_accessor :uri, :cache_ttl, :cache_tries
|
|
5
|
+
|
|
6
|
+
COUCHDB_URI = 'http://localhost:5984'
|
|
7
|
+
CACHE_TTL = 5
|
|
8
|
+
CACHE_TRIES = 2
|
|
9
|
+
|
|
10
|
+
# Usage:
|
|
11
|
+
# server = Makura::Server.new
|
|
12
|
+
# #<URI::HTTP:0xb778ce38 URL:http://localhost:5984>
|
|
13
|
+
# server.info
|
|
14
|
+
# {"couchdb"=>"Welcome", "version"=>"0.9.0a718650-incubating"}
|
|
15
|
+
|
|
16
|
+
def initialize(uri = COUCHDB_URI, cache_ttl = CACHE_TTL, cache_tries = CACHE_TRIES)
|
|
17
|
+
@uri = URI(uri.to_s)
|
|
18
|
+
@cache_ttl = cache_ttl
|
|
19
|
+
@cache_tries = cache_tries
|
|
20
|
+
@uuids = UUIDCache.new(self)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def inspect
|
|
24
|
+
@uri.inspect
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# General queries
|
|
28
|
+
|
|
29
|
+
# Answers with general couchdb info, looks like:
|
|
30
|
+
#
|
|
31
|
+
# Usage:
|
|
32
|
+
# server.info
|
|
33
|
+
# # {'couchdb' => 'Welcome', 'version' => '0.9.0a718650-incubating'}
|
|
34
|
+
def info
|
|
35
|
+
get('/')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Answers with configuration info.
|
|
39
|
+
#
|
|
40
|
+
# Usage:
|
|
41
|
+
# server.config
|
|
42
|
+
#
|
|
43
|
+
def config
|
|
44
|
+
get('/_config')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Issue restart of the CouchDB daemon.
|
|
48
|
+
#
|
|
49
|
+
# Usage:
|
|
50
|
+
# server.restart
|
|
51
|
+
# # {'ok' => true}
|
|
52
|
+
def restart
|
|
53
|
+
post('/_restart')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Array of names of databases on the server
|
|
57
|
+
#
|
|
58
|
+
# Usage:
|
|
59
|
+
# server.databases
|
|
60
|
+
# # ["another", "blog", "makura-spec"]
|
|
61
|
+
def databases
|
|
62
|
+
get('/_all_dbs')
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Return new database instance using this server instance.
|
|
66
|
+
#
|
|
67
|
+
# Usage:
|
|
68
|
+
# foo = server.database('foo')
|
|
69
|
+
# # #<Makura::Database 'http://localhost:5984/foo'>
|
|
70
|
+
# server.databases
|
|
71
|
+
# # ["another", "blog", "foo", "makura-spec"]
|
|
72
|
+
|
|
73
|
+
def database(name)
|
|
74
|
+
Database.new(self, name)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Answers with an uuid from the UUIDCache.
|
|
78
|
+
#
|
|
79
|
+
# Usage:
|
|
80
|
+
# server.next_uuid
|
|
81
|
+
# # "55fdca746fa5a5b56f5270875477a2cc"
|
|
82
|
+
|
|
83
|
+
def next_uuid
|
|
84
|
+
@uuids.next
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def start_cache(namespace = 'makura', *servers)
|
|
88
|
+
servers << 'localhost:11211' if servers.empty?
|
|
89
|
+
@cache = MemCache.new(servers, :namespace => namespace, :multithread => true)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def stop_cache
|
|
93
|
+
@cache = nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def cached(request, ttl = cache_ttl, tries = cache_tries)
|
|
97
|
+
key = request[:url]
|
|
98
|
+
|
|
99
|
+
unless response = @cache.get(key)
|
|
100
|
+
response = execute(request)
|
|
101
|
+
@cache.add(key, response, ttl)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
return response
|
|
105
|
+
rescue MemCache::MemCacheError => error
|
|
106
|
+
servers = @cache.servers.map{|s| "#{s.host}:#{s.port}"}
|
|
107
|
+
start_cache(@cache.namespace, *servers)
|
|
108
|
+
tries -= 1
|
|
109
|
+
retry if tries > 0
|
|
110
|
+
warn "[makura caching disabled] #{error.message}"
|
|
111
|
+
@cache = nil
|
|
112
|
+
execute(request)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Helpers
|
|
116
|
+
|
|
117
|
+
def request(method, path, params = {})
|
|
118
|
+
keep_raw = params.delete(:raw)
|
|
119
|
+
payload = params.delete(:payload)
|
|
120
|
+
payload = payload.to_json if payload and not keep_raw
|
|
121
|
+
headers = {}
|
|
122
|
+
|
|
123
|
+
if content_type = params.delete('Content-Type')
|
|
124
|
+
headers['Content-Type'] = content_type
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
params.delete_if{|k,v| v.nil? }
|
|
128
|
+
uri = uri(path, params).to_s
|
|
129
|
+
|
|
130
|
+
request = {
|
|
131
|
+
:method => method,
|
|
132
|
+
:url => uri,
|
|
133
|
+
:payload => payload,
|
|
134
|
+
:headers => headers}
|
|
135
|
+
|
|
136
|
+
if @cache and request[:method] == :get
|
|
137
|
+
raw = cached(request)
|
|
138
|
+
else
|
|
139
|
+
raw = execute(request)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
return raw if keep_raw
|
|
143
|
+
json = JSON.parse(raw)
|
|
144
|
+
rescue JSON::ParserError
|
|
145
|
+
return raw
|
|
146
|
+
rescue RestClient::RequestFailed => ex
|
|
147
|
+
raise appropriate_error(ex)
|
|
148
|
+
rescue RestClient::ResourceNotFound => ex
|
|
149
|
+
raise Error::ResourceNotFound, request[:url], ex.backtrace
|
|
150
|
+
rescue Errno::ECONNREFUSED
|
|
151
|
+
raise Error::ConnectionRefused, "Is CouchDB running at #{@uri}?"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def execute(request)
|
|
155
|
+
RestClient::Request.execute(request)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def appropriate_error(exception)
|
|
159
|
+
body = exception.response.body if exception.respond_to?(:response)
|
|
160
|
+
backtrace = exception.backtrace
|
|
161
|
+
|
|
162
|
+
raise(Error::RequestFailed, exception.message, backtrace) unless body
|
|
163
|
+
raise(Error::RequestFailed, exception.message, backtrace) if body.empty?
|
|
164
|
+
|
|
165
|
+
json = JSON.parse(body)
|
|
166
|
+
error, reason = json['error'], json['reason']
|
|
167
|
+
|
|
168
|
+
case error
|
|
169
|
+
when 'bad_request'
|
|
170
|
+
raise(Error::BadRequest, reason, backtrace)
|
|
171
|
+
when 'authorization'
|
|
172
|
+
raise(Error::Authorization, reason, backtrace)
|
|
173
|
+
when 'not_found'
|
|
174
|
+
raise(Error::NotFound, reason, backtrace)
|
|
175
|
+
when 'file_exists'
|
|
176
|
+
raise(Error::FileExists, reason, backtrace)
|
|
177
|
+
when 'missing_rev'
|
|
178
|
+
raise(Error::MissingRevision, reason, backtrace)
|
|
179
|
+
when 'conflict'
|
|
180
|
+
raise(Error::Conflict, reason, backtrace)
|
|
181
|
+
else
|
|
182
|
+
raise(Error::RequestFailed, json.inspect, backtrace)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
JSON_PARAMS = %w[key startkey endkey]
|
|
187
|
+
|
|
188
|
+
def paramify(hash)
|
|
189
|
+
hash.map{|k,v|
|
|
190
|
+
k = k.to_s
|
|
191
|
+
v = v.to_json if JSON_PARAMS.include?(k)
|
|
192
|
+
"#{Makura.escape(k)}=#{Makura.escape(v)}"
|
|
193
|
+
}.join('&')
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def uri(path = '/', params = {})
|
|
197
|
+
uri = @uri.dup
|
|
198
|
+
uri.path = (path[0,1] == '/' ? path : "/#{path}").squeeze('/')
|
|
199
|
+
uri.query = paramify(params) unless params.empty?
|
|
200
|
+
uri
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Makura
|
|
2
|
+
class UUIDCache
|
|
3
|
+
attr_accessor :min, :max, :server, :pretty
|
|
4
|
+
|
|
5
|
+
def initialize(server, min = 500, max = 1500, pretty = true)
|
|
6
|
+
@server, @min, @max, @pretty = server, min, max, pretty
|
|
7
|
+
@uuids = []
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def next
|
|
11
|
+
fetch if @uuids.size < min
|
|
12
|
+
@uuids.shift
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def fetch(count = 0)
|
|
16
|
+
todo = max - @uuids.size
|
|
17
|
+
count = [min, todo, max].sort[1]
|
|
18
|
+
uuids = @server.get('/_uuids', :count => count)['uuids']
|
|
19
|
+
uuids.map!{|u| Makura.pretty_from_md5(u) } if pretty
|
|
20
|
+
@uuids.concat(uuids)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/makura.rb
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
VERSION = '2008.01.15'
|
|
14
|
+
ROOT = File.expand_path(File.dirname(__FILE__))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
unless $LOAD_PATH.any?{|lp| File.expand_path(lp) == Makura::ROOT }
|
|
18
|
+
$LOAD_PATH.unshift(Makura::ROOT)
|
|
19
|
+
end
|
|
20
|
+
|
|
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
|
+
end
|
|
61
|
+
|
|
62
|
+
Sofa = Makura # be backwards compatible
|
data/makura.gemspec
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Gem::Specification.new do |s|
|
|
2
|
+
s.name = "makura"
|
|
3
|
+
s.version = "2009.02.18"
|
|
4
|
+
|
|
5
|
+
s.summary = "Ruby wrapper around the CouchDB REST API."
|
|
6
|
+
s.description = "Ruby wrapper around the CouchDB REST API."
|
|
7
|
+
s.platform = "ruby"
|
|
8
|
+
s.has_rdoc = true
|
|
9
|
+
s.author = "Michael 'manveru' Fellinger"
|
|
10
|
+
s.email = "m.fellinger@gmail.com"
|
|
11
|
+
s.homepage = "http://github.com/manveru/makura"
|
|
12
|
+
s.executables = ['makura']
|
|
13
|
+
s.bindir = "bin"
|
|
14
|
+
s.require_path = "lib"
|
|
15
|
+
|
|
16
|
+
s.add_dependency('rest-client', '>= 0.8.1')
|
|
17
|
+
s.add_dependency('json', '>= 1.1.3')
|
|
18
|
+
|
|
19
|
+
s.files = [
|
|
20
|
+
"COPYING",
|
|
21
|
+
"README.md",
|
|
22
|
+
"bin/makura",
|
|
23
|
+
"example/blog.rb",
|
|
24
|
+
"example/couch/map/author_all.js",
|
|
25
|
+
"example/couch/map/author_posts.js",
|
|
26
|
+
"example/couch/map/post_all.js",
|
|
27
|
+
"example/couch/map/post_comments.js",
|
|
28
|
+
"example/couch/reduce/sum_length.js",
|
|
29
|
+
"lib/makura/database.rb",
|
|
30
|
+
"lib/makura/design.rb",
|
|
31
|
+
"lib/makura/error.rb",
|
|
32
|
+
"lib/makura/http_methods.rb",
|
|
33
|
+
"lib/makura/layout.rb",
|
|
34
|
+
"lib/makura/model.rb",
|
|
35
|
+
"lib/makura/plugin/pager.rb",
|
|
36
|
+
"lib/makura/server.rb",
|
|
37
|
+
"lib/makura/uuid_cache.rb",
|
|
38
|
+
"lib/makura.rb",
|
|
39
|
+
"makura.gemspec"
|
|
40
|
+
]
|
|
41
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: manveru-makura
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 2009.02.18
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Michael 'manveru' Fellinger
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
|
|
12
|
+
date: 2009-02-13 00:00:00 -08:00
|
|
13
|
+
default_executable:
|
|
14
|
+
dependencies:
|
|
15
|
+
- !ruby/object:Gem::Dependency
|
|
16
|
+
name: rest-client
|
|
17
|
+
type: :runtime
|
|
18
|
+
version_requirement:
|
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
20
|
+
requirements:
|
|
21
|
+
- - ">="
|
|
22
|
+
- !ruby/object:Gem::Version
|
|
23
|
+
version: 0.8.1
|
|
24
|
+
version:
|
|
25
|
+
- !ruby/object:Gem::Dependency
|
|
26
|
+
name: json
|
|
27
|
+
type: :runtime
|
|
28
|
+
version_requirement:
|
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 1.1.3
|
|
34
|
+
version:
|
|
35
|
+
description: Ruby wrapper around the CouchDB REST API.
|
|
36
|
+
email: m.fellinger@gmail.com
|
|
37
|
+
executables:
|
|
38
|
+
- makura
|
|
39
|
+
extensions: []
|
|
40
|
+
|
|
41
|
+
extra_rdoc_files: []
|
|
42
|
+
|
|
43
|
+
files:
|
|
44
|
+
- COPYING
|
|
45
|
+
- README.md
|
|
46
|
+
- bin/makura
|
|
47
|
+
- example/blog.rb
|
|
48
|
+
- example/couch/map/author_all.js
|
|
49
|
+
- example/couch/map/author_posts.js
|
|
50
|
+
- example/couch/map/post_all.js
|
|
51
|
+
- example/couch/map/post_comments.js
|
|
52
|
+
- example/couch/reduce/sum_length.js
|
|
53
|
+
- lib/makura/database.rb
|
|
54
|
+
- lib/makura/design.rb
|
|
55
|
+
- lib/makura/error.rb
|
|
56
|
+
- lib/makura/http_methods.rb
|
|
57
|
+
- lib/makura/layout.rb
|
|
58
|
+
- lib/makura/model.rb
|
|
59
|
+
- lib/makura/plugin/pager.rb
|
|
60
|
+
- lib/makura/server.rb
|
|
61
|
+
- lib/makura/uuid_cache.rb
|
|
62
|
+
- lib/makura.rb
|
|
63
|
+
- makura.gemspec
|
|
64
|
+
has_rdoc: true
|
|
65
|
+
homepage: http://github.com/manveru/makura
|
|
66
|
+
post_install_message:
|
|
67
|
+
rdoc_options: []
|
|
68
|
+
|
|
69
|
+
require_paths:
|
|
70
|
+
- lib
|
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: "0"
|
|
76
|
+
version:
|
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: "0"
|
|
82
|
+
version:
|
|
83
|
+
requirements: []
|
|
84
|
+
|
|
85
|
+
rubyforge_project:
|
|
86
|
+
rubygems_version: 1.2.0
|
|
87
|
+
signing_key:
|
|
88
|
+
specification_version: 2
|
|
89
|
+
summary: Ruby wrapper around the CouchDB REST API.
|
|
90
|
+
test_files: []
|
|
91
|
+
|