ohac-ditz 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/ditz/model.rb ADDED
@@ -0,0 +1,339 @@
1
+ require 'yaml'
2
+ require "yaml_waml"
3
+ if RUBY_VERSION >= '1.9.0'
4
+ require 'digest/sha1'
5
+ else
6
+ require 'sha1'
7
+ end
8
+ require "ditz/lowline"; include Lowline
9
+ require "ditz/util"
10
+
11
+ class Time
12
+ alias :old_to_yaml :to_yaml
13
+ def to_yaml(opts = {})
14
+ self.utc.old_to_yaml(opts)
15
+ end
16
+ end
17
+
18
+ module Ditz
19
+
20
+ class ModelError < StandardError; end
21
+
22
+ class ModelObject
23
+ def initialize
24
+ @values = {}
25
+ @serialized_values = {}
26
+ self.class.fields.map { |f, opts| @values[f] = [] if opts[:multi] }
27
+ end
28
+
29
+ ## override me and throw ModelErrors if necessary
30
+ def validate! whence, context
31
+ end
32
+
33
+ ## yamlability
34
+ def self.yaml_domain; "ditz.rubyforge.org,2008-03-06" end
35
+ def self.yaml_other_thing; name.split('::').last.dcfirst end
36
+ def to_yaml_type; "!#{self.class.yaml_domain}/#{self.class.yaml_other_thing}" end
37
+ def self.inherited subclass
38
+ YAML.add_domain_type(yaml_domain, subclass.yaml_other_thing) do |type, val|
39
+ o = subclass.new
40
+ val.each do |k, v|
41
+ m = "__serialized_#{k}="
42
+ if o.respond_to? m
43
+ o.send m, v
44
+ end
45
+ end
46
+ o.class.fields.each do |f, opts|
47
+ m = "__serialized_#{f}"
48
+ if opts[:multi] && o.send(m).nil?
49
+ o.send(m + '=', [])
50
+ end
51
+ end
52
+ o.unchanged!
53
+ o
54
+ end
55
+ end
56
+
57
+ ## override these two to model per-field transformations between disk and
58
+ ## memory.
59
+ ##
60
+ ## convert disk form => memory form
61
+ def deserialized_form_of field, value
62
+ value
63
+ end
64
+
65
+ ## convert memory form => disk form
66
+ def serialized_form_of field, value
67
+ value
68
+ end
69
+
70
+ ## Add a field to a model object
71
+ ##
72
+ ## The options you specify here determine how the field is populated when an
73
+ ## instance of this object is created. Objects can be created interactively,
74
+ ## with #create_interactively, or non-interactively, with #create, and the
75
+ ## creation mode, combined with these options, determine how the field is
76
+ ## populated on a new model object.
77
+ ##
78
+ ## The default behavior is to simply prompt the user with the field name when
79
+ ## in interactive mode, and to raise an exception if the value is not passed
80
+ ## to #create in non-interactive mode.
81
+ ##
82
+ ## Options:
83
+ ## :interactive_generator => a method name or Proc that will be called to
84
+ ## return the value of this field, if the model object is created
85
+ ## interactively.
86
+ ## :generator => a method name or Proc that will be called to return the
87
+ ## value of this field. If the model object is created interactively, and
88
+ ## a :interactive_generator option is specified, that will be used instead.
89
+ ## :multi => a boolean determining whether the field has multiple values,
90
+ ## i.e., is an array. If created with :ask => false, will be initialized
91
+ ## to [] instead of to nil. Additionally, the model object will have
92
+ ## #add_<field> and #drop_<field> methods.
93
+ ## :ask => a boolean determining whether, if the model object is created
94
+ ## interactively, the user will be prompted for the value of this field.
95
+ ## TRUE BY DEFAULT. If :interactive_generator or :generator are specified,
96
+ ## those will be called instead.
97
+ ##
98
+ ## If this is true, non-interactive creation
99
+ ## will raise an exception unless the field value is passed as an argument.
100
+ ## If this is false, non-interactive creation will initialize this to nil
101
+ ## (or [] if this field is additionally marked :multi) unless the value is
102
+ ## passed as an argument.
103
+ ## :prompt => a string to display to the user when prompting for the field
104
+ ## value during interactive creation. Not used if :generator or
105
+ ## :interactive_generator is specified.
106
+ ## :multiline => a boolean determining whether to prompt the user for a
107
+ ## multiline answer during interactive creation. Default false. Not used
108
+ ## if :generator or :interactive_generator is specified.
109
+ ## :default => a default value when prompting for the field value during
110
+ ## interactive creation. Not used if :generator, :interactive_generator,
111
+ ## :multiline, or :default_generator is specified.
112
+ ## :default_generator => a method name or Proc which will be called to
113
+ ## generate the default value when prompting for the field value during
114
+ ## interactive creation. Not used if :generator, :interactive_generator,
115
+ ## or :multiline is specified.
116
+ ## :nil_ok => a boolean determining whether, if created in non-interactive
117
+ ## mode and the value for this field is not passed in, (or is passed in
118
+ ## as nil), that's ok. Default is false. This is not necessary if :ask =>
119
+ ## false is specified; it's only necessary for fields that you want an
120
+ ## interactive prompt for, but a nil value is fine.
121
+ def self.field name, opts={}
122
+ @fields ||= [] # can't use a hash because we need to preserve field order
123
+ raise ModelError, "field with name #{name} already defined" if @fields.any? { |k, v| k == name }
124
+ @fields << [name, opts]
125
+
126
+ if opts[:multi]
127
+ single_name = name.to_s.sub(/s$/, "") # oh yeah
128
+ define_method "add_#{single_name}" do |obj|
129
+ array = send(name)
130
+ raise ModelError, "already has a #{single_name} with name #{obj.name.inspect}" if obj.respond_to?(:name) && array.any? { |o| o.name == obj.name }
131
+ changed!
132
+ @serialized_values.delete name
133
+ array << obj
134
+ end
135
+
136
+ define_method "drop_#{single_name}" do |obj|
137
+ return unless send(name).delete obj
138
+ @serialized_values.delete name
139
+ changed!
140
+ obj
141
+ end
142
+ end
143
+
144
+ define_method "#{name}=" do |o|
145
+ changed!
146
+ @serialized_values.delete name
147
+ @values[name] = o
148
+ end
149
+
150
+ define_method "__serialized_#{name}=" do |o|
151
+ changed!
152
+ @values.delete name
153
+ @serialized_values[name] = o
154
+ end
155
+
156
+ define_method "__serialized_#{name}" do
157
+ @serialized_values[name]
158
+ end
159
+
160
+ define_method name do
161
+ return @values[name] if @values.member?(name)
162
+ @values[name] = deserialized_form_of name, @serialized_values[name]
163
+ end
164
+ end
165
+
166
+ def self.field_names; @fields.map { |name, opts| name } end
167
+ class << self
168
+ attr_reader :fields, :values, :serialized_values
169
+ end
170
+
171
+ def self.changes_are_logged
172
+ define_method(:changes_are_logged?) { true }
173
+ field :log_events, :multi => true, :ask => false
174
+ define_method(:log) do |what, who, comment|
175
+ add_log_event([Time.now, who, what, comment || ""])
176
+ self
177
+ end
178
+ define_method(:last_event_time) { log_events.empty? ? nil : log_events.last[0] }
179
+ end
180
+
181
+ def self.from fn
182
+ returning YAML::load_file(fn) do |o|
183
+ raise ModelError, "error loading from yaml file #{fn.inspect}: expected a #{self}, got a #{o.class}" unless o.class == self
184
+ o.pathname = fn if o.respond_to? :pathname=
185
+ o.validate! :load, []
186
+ end
187
+ end
188
+
189
+ def to_s
190
+ "<#{self.class.name}: " + self.class.field_names.map { |f| "#{f}: " + send(f).inspect }.join(", ") + ">"
191
+ end
192
+
193
+ def inspect; to_s end
194
+
195
+ ## depth-first search on all reachable ModelObjects. fuck yeah.
196
+ def each_modelobject
197
+ seen = {}
198
+ to_see = [self]
199
+ until to_see.empty?
200
+ cur = to_see.pop
201
+ seen[cur] = true
202
+ yield cur
203
+ cur.class.field_names.each do |f|
204
+ val = cur.send(f)
205
+ next if seen[val]
206
+ if val.is_a?(ModelObject)
207
+ to_see.push val
208
+ elsif val.is_a?(Array)
209
+ to_see += val.select { |v| v.is_a?(ModelObject) }
210
+ end
211
+ end
212
+ end
213
+ end
214
+
215
+ def save! fn
216
+ #FileUtils.mv fn, "#{fn}~", :force => true rescue nil
217
+ File.open(fn, "w") { |f| f.puts to_yaml }
218
+ unchanged!
219
+ self
220
+ end
221
+
222
+ def to_yaml opts={}
223
+ ret = YAML::quick_emit(object_id, opts) do |out|
224
+ out.map(taguri, nil) do |map|
225
+ self.class.fields.each do |f, fops|
226
+ v = if @serialized_values.member?(f)
227
+ @serialized_values[f]
228
+ else
229
+ @serialized_values[f] = serialized_form_of f, @values[f]
230
+ end
231
+
232
+ map.add f.to_s, v
233
+ end
234
+ end
235
+ end
236
+ YamlWaml.decode(ret)
237
+ end
238
+
239
+ def changed?; @changed ||= false end
240
+ def changed!; @changed = true end
241
+ def unchanged!; @changed = false end
242
+
243
+ class << self
244
+ ## creates the object, prompting the user when necessary. can take
245
+ ## a :with => { hash } parameter for pre-filling model fields.
246
+ ##
247
+ ## can also take a :defaults_from => obj parameter for pre-filling model
248
+ ## fields from another object with (some of) those fields. kinda like a
249
+ ## bizarre interactive copy constructor.
250
+ def create_interactively opts={}
251
+ o = self.new
252
+ generator_args = opts[:args] || []
253
+ @fields.each do |name, field_opts|
254
+ val = if opts[:with] && opts[:with][name]
255
+ opts[:with][name]
256
+ else
257
+ found, v = generate_field_value(o, field_opts, generator_args, :interactive => true)
258
+ if found
259
+ v
260
+ else
261
+ q = field_opts[:prompt] || name.to_s.capitalize
262
+ if field_opts[:multiline]
263
+ ## multiline options currently aren't allowed to have a default
264
+ ## value, so just ask.
265
+ ask_multiline_or_editor q
266
+ else
267
+ default = if opts[:defaults_from] && opts[:defaults_from].respond_to?(name) && (x = opts[:defaults_from].send(name))
268
+ x
269
+ else
270
+ default = generate_field_default o, field_opts, generator_args
271
+ end
272
+ ask q, :default => default
273
+ end
274
+ end
275
+ end
276
+ o.send "#{name}=", val
277
+ end
278
+ o.validate! :create, generator_args
279
+ o
280
+ end
281
+
282
+ ## creates the object, filling in fields from 'vals', and throwing a
283
+ ## ModelError when it can't find all the requisite fields
284
+ def create vals={}, generator_args=[]
285
+ o = self.new
286
+ @fields.each do |fname, fopts|
287
+ x = vals[fname]
288
+ x = vals[fname.to_s] if x.nil?
289
+ val = unless x.nil?
290
+ x
291
+ else
292
+ found, x = generate_field_value(o, fopts, generator_args, :interactive => false)
293
+ if found
294
+ x
295
+ elsif !fopts[:nil_ok]
296
+ raise ModelError, "missing required field #{fname.inspect} on #{self.name} object (got #{vals.keys.inspect})"
297
+ end
298
+ end
299
+ o.send "#{fname}=", val unless val.nil?
300
+ end
301
+ o.validate! :create, generator_args
302
+ o
303
+ end
304
+
305
+ private
306
+
307
+ ## get the value for a field if it can be automatically determined
308
+ ## returns [success, value] (because a successful value can be nil)
309
+ def generate_field_value o, field_opts, args, opts={}
310
+ gen = if opts[:interactive]
311
+ field_opts[:interactive_generator] || field_opts[:generator]
312
+ else
313
+ field_opts[:generator]
314
+ end
315
+
316
+ if gen.is_a? Proc
317
+ [true, gen.call(*args)]
318
+ elsif gen
319
+ [true, o.send(gen, *args)]
320
+ elsif field_opts[:ask] == false # nil counts as true here
321
+ [true, field_opts[:default] || (field_opts[:multi] ? [] : nil)]
322
+ else
323
+ [false, nil]
324
+ end
325
+ end
326
+
327
+ def generate_field_default o, field_opts, args
328
+ if field_opts[:default_generator].is_a? Proc
329
+ field_opts[:default_generator].call(*args)
330
+ elsif field_opts[:default_generator]
331
+ o.send field_opts[:default_generator], *args
332
+ elsif field_opts[:default]
333
+ field_opts[:default]
334
+ end
335
+ end
336
+ end
337
+ end
338
+
339
+ end