careo-makura 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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.rb +62 -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/makura.gemspec +41 -0
- metadata +91 -0
@@ -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
|