careo-makura 0.1
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.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
|