makura 2009.05.27

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,64 @@
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(type, file_or_function)
38
+ return unless file_or_function
39
+
40
+ if file_or_function =~ /function\(.*\)/
41
+ function = file_or_function
42
+ else
43
+ parts = file_or_function.split('::')
44
+ file = parts.pop
45
+ filename = File.join(parts, type.to_s, "#{file}.js")
46
+
47
+ if pathname = PATH.find{|pa| File.file?(File.join(pa, filename)) }
48
+ function = File.read(File.join(pathname, filename))
49
+ end
50
+ end
51
+
52
+ instance_variable_set("@#{type}", function) if function
53
+ end
54
+
55
+ def save
56
+ @design[@name] = self.to_hash
57
+ @design.save
58
+ end
59
+
60
+ def to_hash
61
+ {:map => @map, :reduce => @reduce, :makura_options => @options}
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,370 @@
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 save
88
+ return if not valid? if respond_to?(:valid)
89
+ save!
90
+ end
91
+
92
+ def save!
93
+ hash = self.to_hash
94
+
95
+ self.class.makura_relation.each do |kind, relation_hash|
96
+ relation_hash.each do |key, value|
97
+ hash[key.to_s] = hash[key.to_s] #._id
98
+ end
99
+ end
100
+
101
+ response = self.class.database.save(hash)
102
+ self._rev = response['rev']
103
+ self._id = response['id']
104
+
105
+ return self
106
+ end
107
+
108
+ # path, file, args = {})
109
+ def attach(*args)
110
+ self.class.database.put_attachment(self, *args)
111
+ end
112
+
113
+ # delete attachment by name.
114
+ # we make sure the parameter is given and a nonempty string to avoid
115
+ # destroying the document itself
116
+ def detach(name)
117
+ name.strip!
118
+ return if name.empty?
119
+ self.class.database.request(:delete, "#{_id}/#{name}", :rev => _rev)
120
+ rescue Makura::Error::Conflict
121
+ self['_rev'] = self.class[self._id]['_rev']
122
+ retry
123
+ end
124
+
125
+ def destroy
126
+ self.class.database.delete(_id, :rev => _rev)
127
+ end
128
+
129
+ def ==(obj)
130
+ self.class == obj.class and self._id == obj._id
131
+ end
132
+
133
+ def hash
134
+ @_hash.hash
135
+ end
136
+
137
+ def eql?(other)
138
+ other == self && other.hash == self.hash
139
+ end
140
+
141
+ def clone
142
+ hash = @_hash.dup
143
+ hash.delete('_id')
144
+ hash.delete('_rev')
145
+ self.class.new(hash)
146
+ end
147
+ end
148
+
149
+ module SingletonMethods
150
+ attr_accessor :defaults, :makura_relation, :property_type
151
+
152
+ def plugin(name)
153
+ require "makura/plugin/#{name}".downcase
154
+
155
+ name = name.to_s.capitalize
156
+ mod = Makura::Plugin.const_get(name)
157
+
158
+ include(mod::InstanceMethods) if defined?(mod::InstanceMethods)
159
+ extend(mod::SingletonMethods) if defined?(mod::SingletonMethods)
160
+ end
161
+
162
+ def database=(name)
163
+ @database = Makura::Model.server.database(name)
164
+ end
165
+
166
+ def database
167
+ @database || Makura::Model.database
168
+ end
169
+
170
+ def properties(*names)
171
+ names.each{|name| property(name) }
172
+ end
173
+
174
+ def property(name, opts = {})
175
+ name = name.to_s
176
+ defaults[name] = default = opts.delete(:default) if opts[:default]
177
+ property_type[name] = type = opts.delete(:type) if opts[:type]
178
+
179
+ if type == Time
180
+ code = "
181
+ def #{name}()
182
+ Time.at(@_hash[#{name.dump}].to_i)
183
+ end
184
+ def #{name}=(obj)
185
+ @_hash[#{name.dump}] = obj.to_i
186
+ end"
187
+ class_eval(code)
188
+ else
189
+ code = "
190
+ def #{name}() @_hash[#{name.dump}] end
191
+ def #{name}=(obj) @_hash[#{name.dump}] = obj end"
192
+ end
193
+
194
+ class_eval(code)
195
+ end
196
+
197
+ def id(name)
198
+ @id = name
199
+ class_eval("
200
+ alias #{name} _id
201
+ alias #{name}= _id=")
202
+ end
203
+
204
+ def belongs_to(name, model = nil)
205
+ name = name.to_s
206
+ klass = (model || name.capitalize).to_s
207
+ @makura_relation[:belongs_to][name] = klass
208
+
209
+ class_eval("
210
+ def #{name}()
211
+ @#{name} ||= #{klass}[self[#{name.dump}]]
212
+ end
213
+ def #{name}=(obj)
214
+ if obj.respond_to?(:_id)
215
+ @_hash[#{name.dump}] = obj._id
216
+ else
217
+ @_hash[#{name.dump}] = obj
218
+ end
219
+ end")
220
+ end
221
+
222
+ def has_many(name, model = nil)
223
+ name = name.to_s
224
+ klass = (model || name.capitalize).to_s
225
+ @makura_relation[:has_many][name] = klass
226
+
227
+ class_eval("
228
+ def #{name}()
229
+ @#{name} ||= #{klass}[self[#{name.dump}]]
230
+ end
231
+ def #{name}=(obj)
232
+ return unless obj
233
+ raise RuntimeError, 'You many not assign here'
234
+ end")
235
+ end
236
+
237
+ def [](id, rev = nil)
238
+ new(database[id, rev])
239
+ rescue Error::ResourceNotFound
240
+ nil
241
+ end
242
+
243
+ def design
244
+ @design ||= Design.new(name.to_s, database)
245
+ end
246
+
247
+ def layout(name, opts = {})
248
+ design[name] = layout = Layout.new(name, design)
249
+ unless opts[:map] or opts[:reduce]
250
+ prefix = self.name.gsub(/\B[A-Z][^A-Z]/, '_\&')
251
+ end
252
+
253
+ map_name = opts[:map] || "#{prefix}_#{name}".downcase
254
+ reduce_name = opts[:reduce] || "#{prefix}_#{name}".downcase
255
+
256
+ layout.load_map(map_name)
257
+ layout.load_reduce(reduce_name)
258
+
259
+ return layout
260
+ end
261
+
262
+ def proto_layout(common, name, opts = {})
263
+ design[name] = layout = Layout.new(name, design)
264
+
265
+ map_name = opts.delete(:map) || "#{self.name}_#{common}".downcase
266
+ reduce_name = opts.delete(:reduce) || "#{self.name}_#{common}".downcase
267
+
268
+ layout.load_proto_map(map_name, opts)
269
+ layout.load_proto_reduce(reduce_name, opts)
270
+
271
+ return layout
272
+ end
273
+
274
+ def save
275
+ design.save
276
+ end
277
+
278
+ # +opts+ must include a :keys or 'keys' key with something that responds
279
+ # to #to_a as value
280
+ #
281
+ # Usage given a map named `Post/by_tags' that does something like:
282
+ #
283
+ # for(t in doc.tags){ emit([doc.tags[t]], null); }
284
+ #
285
+ # You can use this like:
286
+ #
287
+ # keys = ['ruby', 'couchdb']
288
+ # Post.multi_fetch(:by_tags, :keys => keys)
289
+ #
290
+ # And it will return all docs with the tags 'ruby' OR 'couchdb'
291
+ # This can be extended to match even more complex things
292
+ #
293
+ # for(t in doc.tags){ emit([doc.author, doc.tags[t]], null); }
294
+ #
295
+ # Now we do
296
+ #
297
+ # keys = [['manveru', 'ruby'], ['mika', 'couchdb']]
298
+ # Post.multi_fetch(:by_tags, :keys => keys)
299
+ #
300
+ # This will return all docs match following:
301
+ # ((author == 'manveru' && tags.include?('ruby')) ||
302
+ # (author == 'mika' && tags.include?('couchdb')))
303
+ #
304
+ # Of course you can add as many keys as you like:
305
+ #
306
+ # keys = [['manveru', 'ruby'],
307
+ # ['manveru', 'couchdb'],
308
+ # ['mika', 'design']]
309
+ # ['mika', 'couchdb']]
310
+ # Post.multi_fetch(:by_tags, :keys => keys)
311
+ #
312
+ #
313
+ # From http://wiki.apache.org/couchdb/HTTP_view_API
314
+ # A JSON structure of {"keys": ["key1", "key2", ...]} can be posted to
315
+ # any user defined view or _all_docs to retrieve just the view rows
316
+ # matching that set of keys. Rows are returned in the order of the keys
317
+ # specified. Combining this feature with include_docs=true results in
318
+ # the so-called multi-document-fetch feature.
319
+
320
+ def multi_fetch(name, opts = {})
321
+ keys = opts.delete(:keys) || opts.delete('keys')
322
+ opts.merge!(:payload => {'keys' => Array(keys)})
323
+ hash = database.post("_view/#{self}/#{name}", opts)
324
+ convert_raw(hash['rows'])
325
+ end
326
+
327
+ def multi_fetch_with_docs(name, opts = {})
328
+ opts.merge!(:include_docs => true, :reduce => false)
329
+ multi_fetch(name, opts)
330
+ end
331
+ alias multi_document_fetch multi_fetch_with_docs
332
+
333
+ # It is generally recommended not to include the doc in the emit of the
334
+ # map function but to use include_docs=true.
335
+ # To make using this approach more convenient use this method.
336
+
337
+ def view_with_docs(name, opts = {})
338
+ opts.merge!(:include_docs => true, :reduce => false)
339
+ view(name, opts)
340
+ end
341
+
342
+ alias view_docs view_with_docs
343
+
344
+ def view(name, opts = {})
345
+ flat = opts.delete(:flat)
346
+ hash = database.view("#{Makura.escape(self)}/_view/#{name}", opts)
347
+
348
+ convert_raw(hash['rows'], flat)
349
+ end
350
+
351
+ def convert_raw(rows, flat = false)
352
+ rows.map do |row|
353
+ value = row['doc'] || row['value']
354
+
355
+ if value.respond_to?(:to_hash)
356
+ if not flat and type = value['type']
357
+ Makura.constant(type).new(value)
358
+ else
359
+ row
360
+ end
361
+ elsif not row['key']
362
+ value
363
+ else
364
+ row
365
+ end
366
+ end
367
+ end
368
+ end
369
+ end
370
+ end
@@ -0,0 +1,68 @@
1
+ module Makura
2
+ module Plugin
3
+ # Automatically store/retrieve values for different languages.
4
+ # Useful especially if combined with the locale gem.
5
+ #
6
+ # Default language is 'en'.
7
+ #
8
+ # Usage:
9
+ #
10
+ # class Page
11
+ # include Makura::Model
12
+ #
13
+ # plugin :localize
14
+ # localized :title, :text
15
+ # end
16
+ #
17
+ # page = Page.new
18
+ # page.title = 'english title'
19
+ # page.title # => 'english title'
20
+ #
21
+ # page.language = 'de'
22
+ # page.title # => nil
23
+ # page.title = 'deutscher titel'
24
+ # page.title # => 'deutscher titel'
25
+ #
26
+ # page.language = 'en'
27
+ # page.title # => 'english title'
28
+ module Localize
29
+ LOCALIZE_GET = '
30
+ def %key%
31
+ raise(ArgumentError, "No language set") unless language
32
+ self["%key%_#{language}"] || self["%key%_#{self.class.default_language}"]
33
+ end'.strip
34
+
35
+ LOCALIZE_SET = '
36
+ def %key%=(data)
37
+ raise(ArgumentError, "No language set") unless language
38
+ self["%key%_#{language}"] = data
39
+ end'.strip
40
+
41
+ module SingletonMethods
42
+ def localized(*keys)
43
+ keys.each do |key|
44
+ key = key.to_s
45
+ class_eval(LOCALIZE_GET.gsub('%key%', key))
46
+ class_eval(LOCALIZE_SET.gsub('%key%', key))
47
+ end
48
+ end
49
+
50
+ def default_language=(dl)
51
+ @default_language = dl
52
+ end
53
+
54
+ def default_language
55
+ @default_language ||= 'en'
56
+ end
57
+ end
58
+
59
+ module InstanceMethods
60
+ attr_writer :language
61
+
62
+ def language
63
+ @language || self.class.default_language
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end