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.
@@ -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