/
yuki.rb
322 lines (272 loc) · 7.67 KB
/
yuki.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
require File.join(File.dirname(__FILE__), *%w(store abstract))
require File.join(File.dirname(__FILE__),'cast_system')
# A wrapper for tokyo-x products for persistence of ruby objects
module Yuki
class InvalidAdapter < Exception; end
def self.included(base)
base.send :include, Yuki::Resource
end
module Resource
def self.included(base)
base.send :include, InstanceMethods
base.class_eval { @store = nil }
base.instance_eval { alias __new__ new }
base.extend ClassMethods
base.extend Validations
base.instance_eval {
has :type
has :pk
}
end
module Callbacks
def before_save(); end
def after_save(); end
def before_delete(); end
def after_delete(); end
end
module ClassMethods
attr_reader :db
# assign the current storage adapter and config
def store(adapter, opts = {})
@db = (case adapter
when :cabinet then use_cabinet
when :tyrant then use_tyrant
else raise(
InvalidAdapter.new(
'Invalid Adapter. Try :cabinet or :tyrant.'
)
)
end).new(opts)
end
def inherited(c)
c.instance_variable_set(:@db, @db.dup || nil)
end
# Redefines #new method in order to build the object
# from a hash. Assumes a constructor that takes a hash or a
# no-args constructor
def new(attrs = {})
begin
__new__(attrs).from_hash(attrs)
rescue
__new__.from_hash(attrs)
end
end
# Returns all of the keys for the class's store
def keys
db.keys
end
# Gets all instances matching query criteria
# :limit
# :conditions => [[:attr, :cond, :expected]]
def filter(opts = {})
build(db.filter(opts))
end
alias_method :all, :filter
def union(opts = {})
build(db.union(opts))
end
def soft_delete!
has :deleted, :timestamp
define_method(:delete!) {
self['deleted'] = Time.now
self.save!
}
end
# Gets an instance by key
def get(key)
val = db[key]
build(val)[0] if val && val[type_desc]
end
# Updates an instance by key the the given attrs hash
def put(key, attrs)
db[key] = attrs
val = db[key]
build(val)[0] if val && val[type_desc]
end
# An object Type descriminator
# This is implicitly differentiates
# what a class a hash is associated with
def type_desc
'type'
end
# Attribute definition api.
# At a minimum this method expects the name
# of the attribute.
#
# This method also specifies type information
# about attributes. The default type is :default
# which is a String. Other valid options for type
# are.
# :numeric
# :timestamp
# :float
# :regex
#
# opts can be
# :default - defines a default value to return if a value
# is not supplied
# :mutable - determines if the attr should be mutable.
# true or false. (false is default)
#
# TODO
# opts planened to be supported in the future are
# :alias - altername name
# :collection - true or false
#
def has(attr, type = :string, opts = {})
if type.is_a?(Hash)
opts.merge!(type)
type = :string
end
define_methods(attr, type, opts)
end
# Builds one or more instance's of the class from
# a hash or array of hashes
def build(hashes)
[hashes].flatten.inject([]) do |list, hash|
type = hash[type_desc] || self.to_s.split('::').last
cls = resolve(type)
list << cls.new(hash) if cls
list
end if hashes
end
# Resolves a class given a string or hash
# If given a hash, the expected format is
# { :foo => { :type => :Bar, ... } }
# or
# "Bar"
def resolve(cls_def)
if cls_def.is_a? Hash
class_key = cls_def.keys.first
clazz = resolve(cls_def[class_key][:type])
else
clazz = begin
cls_def.to_s.split("::").inject(Object) { |obj, const|
obj.const_get(const)
} unless cls_def.to_s.strip.empty?
rescue NameError => e
puts "given #{cls_def} got #{e.inspect}"
raise e
end
end
end
def define_methods(attr, type, opts = {})
default_val = opts.delete(:default)
mutable = opts.delete(:mutable) || false
casted, uncasted = :"cast_#{attr}", :"uncast_#{attr}"
define_method(casted) { |val| cast(val, type) }
define_method(uncasted) { uncast(self[attr], type) }
define_method(attr) { self[attr] || default_val }
define_method(:"#{attr}=") { |v| self[attr] = v } if mutable
define_method(:"#{attr}?") { self[attr] }
end
private
def use_cabinet
Yuki::Store::TokyoCabinet
end
def use_tyrant
Yuki::Store::TokyoTyrant
end
end
module Validations
# config opts
# :msg => the display message
def validates_presence_of(attr, config={})
unless(send(attr.to_sym))
add_error(
"invalid #{attr}",
(config[:msg] || "#{attr} is required")
)
end
end
end
module InstanceMethods
include CastSystem
include Callbacks
def save!
before_save
validate!
raise(
Exception.new("Object not valid. #{formatted_errors}")
) unless valid?
val = if(key)
db[key] = self.to_h
else
db << self.to_h
end
data.merge!('pk' => (val[:pk] || val['pk']))
after_save
self
end
def delete!
before_delete
db.delete!(key)
after_delete
self
end
def errors
@errors ||= {}
end
def add_error(k,v)
errors.merge!({k,v})
end
def formatted_errors
errors.inject([]) { |errs, (k,v)|
errs << v
}.join(', ')
end
def valid?
errors.empty?
end
def validate!; end
def key
data['pk']
end
def to_h
data.inject({}) do |h, (k, v)|
typed_val = method("uncast_#{k}").call
h[k.to_s] = typed_val
h
end if data
end
def from_hash(h)
type = { self.class.type_desc => self.class.to_s }
h.merge!(type) unless h.include? self.class.type_desc
h.each { |k, v|
if attr_defined?(k)
self[k.to_s] = v
else
p "#{k} is undef! for #{self.inspect}"
end
} if h
self
end
# access attr as if model was hash
def [](attr)
data[attr.to_s]
end
# specifies the object 'type' to serialize
def type
self['type'] || self.class
end
protected
def db
self.class.db
end
def attrs
data.dup
end
private
def []=(attr, val)
val = method("cast_#{attr}").call(val)
data[attr.to_s] = val
end
def attr_defined?(attr)
respond_to?(:"cast_#{attr}")
end
def data
@data ||= {}
end
end
end
end