makura 2009.05.27

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,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