ohac-ditz 0.5.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.
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