careo-makura 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -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