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
|
require 'yaml'
require 'sha1'
require "lowline"; include Lowline
require "util"
class Time
alias :old_to_yaml :to_yaml
def to_yaml(opts = {})
self.utc.old_to_yaml(opts)
end
end
module Ditz
class ModelObject
class ModelError < StandardError; end
def initialize
@values = {}
@serialized_values = {}
self.class.fields.map { |f, opts| @values[f] = [] if opts[:multi] }
end
## yamlability
def self.yaml_domain; "ditz.rubyforge.org,2008-03-06" end
def self.yaml_other_thing; name.split('::').last.dcfirst end
def to_yaml_type; "!#{self.class.yaml_domain}/#{self.class.yaml_other_thing}" end
def self.inherited subclass
YAML.add_domain_type(yaml_domain, subclass.yaml_other_thing) do |type, val|
o = subclass.new
val.each do |k, v|
m = "__serialized_#{k}="
if o.respond_to? m
o.send m, v
else
$stderr.puts "warning: unknown field #{k.inspect} in YAML for #{type}; ignoring"
end
end
o.unchanged!
o
end
end
## override these two to model per-field transformations between disk and
## memory.
##
## convert disk form => memory form
def deserialized_form_of field, value
@serialized_values[field]
end
## convert memory form => disk form
def serialized_form_of field, value
@values[field]
end
## add a new field to a model object
def self.field name, opts={}
@fields ||= [] # can't use a hash because we need to preserve field order
raise ModelError, "field with name #{name} already defined" if @fields.any? { |k, v| k == name }
@fields << [name, opts]
if opts[:multi]
single_name = name.to_s.sub(/s$/, "") # oh yeah
define_method "add_#{single_name}" do |obj|
array = send(name)
raise ModelError, "already has a #{single_name} with name #{obj.name.inspect}" if obj.respond_to?(:name) && array.any? { |o| o.name == obj.name }
changed!
@serialized_values.delete name
array << obj
end
define_method "drop_#{single_name}" do |obj|
return unless @values[name].delete obj
@serialized_values.delete name
changed!
obj
end
end
define_method "#{name}=" do |o|
changed!
@serialized_values.delete name
@values[name] = o
end
define_method "__serialized_#{name}=" do |o|
changed!
@values.delete name
@serialized_values[name] = o
end
define_method "__serialized_#{name}" do
@serialized_values[name]
end
define_method name do
return @values[name] if @values.member?(name)
@values[name] = deserialized_form_of name, @serialized_values[name]
end
end
def self.field_names; @fields.map { |name, opts| name } end
class << self
attr_reader :fields, :values, :serialized_values
end
def self.changes_are_logged
define_method(:changes_are_logged?) { true }
field :log_events, :multi => true, :ask => false
end
def self.from fn
returning YAML::load_file(fn) do |o|
raise ModelError, "error loading from yaml file #{fn.inspect}: expected a #{self}, got a #{o.class}" unless o.class == self
o.pathname = fn if o.respond_to? :pathname=
o.class.fields.each do |f, opts|
m = "__serialized_#{f}"
if opts[:multi] && o.send(m).nil?
$stderr.puts "Warning: corrected nil multi-field #{f}"
o.send "#{m}=", []
end
end
end
end
def to_s
"<#{self.class.name}: " + self.class.field_names.map { |f| "#{f}: " + (@values[f].to_s || @serialized_values[f]).inspect }.join(", ") + ">"
end
def inspect; to_s end
## depth-first search on all reachable ModelObjects. fuck yeah.
def each_modelobject
seen = {}
to_see = [self]
until to_see.empty?
cur = to_see.pop
seen[cur] = true
yield cur
cur.class.field_names.each do |f|
val = cur.send(f)
next if seen[val]
if val.is_a?(ModelObject)
to_see.push val
elsif val.is_a?(Array)
to_see += val.select { |v| v.is_a?(ModelObject) }
end
end
end
end
def save! fn
#FileUtils.mv fn, "#{fn}~", :force => true rescue nil
File.open(fn, "w") { |f| f.puts to_yaml }
self
end
def to_yaml opts={}
YAML::quick_emit(object_id, opts) do |out|
out.map(taguri, nil) do |map|
self.class.fields.each do |f, fops|
v = if @serialized_values.member?(f)
@serialized_values[f]
else
@serialized_values[f] = serialized_form_of f, @values[f]
end
map.add f.to_s, v
end
end
end
end
def log what, who, comment
add_log_event([Time.now, who, what, comment || ""])
self
end
def changed?; @changed ||= false end
def changed!; @changed = true end
def unchanged!; @changed = false end
class << self
## creates the object, prompting the user when necessary. can take
## a :with => { hash } parameter for pre-filling model fields.
##
## can also take a :defaults_from => obj parameter for pre-filling model
## fields from another object with (some of) those fields. kinda like a
## bizarre interactive copy constructor.
def create_interactively opts={}
o = self.new
generator_args = opts[:args] || []
@fields.each do |name, field_opts|
val = if opts[:with] && opts[:with][name]
opts[:with][name]
elsif(found, x = generate_field_value(o, field_opts, generator_args)) && found
x
else
q = field_opts[:prompt] || name.to_s.capitalize
if field_opts[:multiline]
## multiline options currently aren't allowed to have a default
## value, so just ask.
ask_multiline q
else
default = if opts[:defaults_from] && opts[:defaults_from].respond_to?(name) && (x = opts[:defaults_from].send(name))
x
else
default = generate_field_default o, field_opts, generator_args
end
ask q, :default => default
end
end
o.send "#{name}=", val
end
o
end
## creates the object, filling in fields from 'vals', and throwing a
## ModelError when it can't find all the requisite fields
def create generator_args, vals={}
o = self.new
@fields.each do |name, opts|
val = if vals[name]
vals[name]
elsif(found, x = generate_field_value(o, opts, generator_args)) && found
x
else
raise ModelError, "missing required field #{name}"
end
o.send "#{name}=", val
end
o
end
private
## get the value for a field if it can be automatically determined
## returns [success, value] (because a successful value can be ni)
def generate_field_value o, opts, args
if opts[:generator].is_a? Proc
[true, opts[:generator].call(*args)]
elsif opts[:generator]
[true, o.send(opts[:generator], *args)]
elsif opts[:ask] == false # nil counts as true here
[true, opts[:default] || (opts[:multi] ? [] : nil)]
else
[false, nil]
end
end
def generate_field_default o, opts, args
if opts[:default_generator].is_a? Proc
opts[:default_generator].call(*args)
elsif opts[:default_generator]
o.send opts[:default_generator], *args
elsif opts[:default]
opts[:default]
end
end
end
end
end
|