Class: Ditz::ModelObject

Inherits:
Object show all
Defined in:
lib/ditz/model.rb

Direct Known Subclasses

Component, Config, Issue, Label, Project, Release

Defined Under Namespace

Classes: ModelError

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeModelObject

Returns a new instance of ModelObject.



19
20
21
22
23
# File 'lib/ditz/model.rb', line 19

def initialize
  @values = {}
  @serialized_values = {}
  self.class.fields.map { |f, opts| @values[f] = [] if opts[:multi] }
end

Class Attribute Details

.fieldsObject (readonly)

Returns the value of attribute fields.



162
163
164
# File 'lib/ditz/model.rb', line 162

def fields
  @fields
end

.serialized_valuesObject (readonly)

Returns the value of attribute serialized_values.



162
163
164
# File 'lib/ditz/model.rb', line 162

def serialized_values
  @serialized_values
end

.valuesObject (readonly)

Returns the value of attribute values.



162
163
164
# File 'lib/ditz/model.rb', line 162

def values
  @values
end

Class Method Details

.changes_are_loggedObject



165
166
167
168
# File 'lib/ditz/model.rb', line 165

def self.changes_are_logged
  define_method(:changes_are_logged?) { true }
  field :log_events, :multi => true, :ask => false
end

.create(vals = {}, generator_args = []) ⇒ Object

creates the object, filling in fields from ‘vals’, and throwing a ModelError when it can’t find all the requisite fields



272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/ditz/model.rb', line 272

def create vals={}, generator_args=[]
  o = self.new
  @fields.each do |fname, fopts|
    val = if(x = vals[fname] || vals[fname.to_s])
      x
    elsif(found, x = generate_field_value(o, fopts, generator_args, :interactive => false)) && found
      x
    elsif !fopts[:nil_ok]
      raise ModelError, "missing required field #{fname.inspect} on #{self.name} object (got #{vals.keys.inspect})"
    end
    o.send "#{fname}=", val if val
  end
  o
end

.create_interactively(opts = {}) ⇒ Object

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.



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
# File 'lib/ditz/model.rb', line 242

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, v = generate_field_value(o, field_opts, generator_args, :interactive => true)) && found
      v
    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_smartly 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

.field(name, opts = {}) ⇒ Object

Add a field to a model object

The options you specify here determine how the field is populated when an instance of this object is created. Objects can be created interactively, with #create_interactively, or non-interactively, with #create, and the creation mode, combined with these options, determine how the field is populated on a new model object.

The default behavior is to simply prompt the user with the field name when in interactive mode, and to raise an exception if the value is not passed to #create in non-interactive mode.

Options:

 :interactive_generator => a method name or Proc that will be called to
   return the value of this field, if the model object is created
   interactively.
 :generator => a method name or Proc that will be called to return the
   value of this field. If the model object is created interactively, and
   a :interactive_generator option is specified, that will be used instead.
 :multi => a boolean determining whether the field has multiple values,
   i.e., is an array. If created with :ask => false, will be initialized
   to [] instead of to nil. Additionally, the model object will have
   #add_<field> and #drop_<field> methods.
 :ask => a boolean determining whether, if the model object is created
   interactively, the user will be prompted for the value of this field.
   TRUE BY DEFAULT. If :interactive_generator or :generator are specified,
   those will be called instead.

   If this is true, non-interactive creation
   will raise an exception unless the field value is passed as an argument.
   If this is false, non-interactive creation will initialize this to nil
   (or [] if this field is additionally marked :multi) unless the value is
   passed as an argument.
:prompt => a string to display to the user when prompting for the field
  value during interactive creation. Not used if :generator or
  :interactive_generator is specified.
:multiline => a boolean determining whether to prompt the user for a
  multiline answer during interactive creation. Default false. Not used
  if :generator or :interactive_generator is specified.
:default => a default value when prompting for the field value during
  interactive creation. Not used if :generator, :interactive_generator,
  :multiline, or :default_generator is specified.
:default_generator => a method name or Proc which will be called to
  generate the default value when prompting for the field value during
  interactive creation. Not used if :generator, :interactive_generator,
  or :multiline is specified.
:nil_ok => a boolean determining whether, if created in non-interactive
  mode and the value for this field is not passed in, (or is passed in
  as nil), that's ok. Default is false. This is not necessary if :ask =>
  false is specified; it's only necessary for fields that you want an
  interactive prompt for, but a nil value is fine.

Raises:



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
# File 'lib/ditz/model.rb', line 115

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 send(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

.field_namesObject



160
# File 'lib/ditz/model.rb', line 160

def self.field_names; @fields.map { |name, opts| name } end

.from(fn) ⇒ Object



170
171
172
173
174
175
# File 'lib/ditz/model.rb', line 170

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=
  end
end

.inherited(subclass) ⇒ Object



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/ditz/model.rb', line 29

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.class.fields.each do |f, opts|
      m = "__serialized_#{f}"
      if opts[:multi] && o.send(m).nil?
        o.send(m + '=', [])
      end
    end
    o.unchanged!
    o
  end
end

.yaml_domainObject

yamlability



26
# File 'lib/ditz/model.rb', line 26

def self.yaml_domain; "ditz.rubyforge.org,2008-03-06" end

.yaml_other_thingObject



27
# File 'lib/ditz/model.rb', line 27

def self.yaml_other_thing; name.split('::').last.dcfirst end

Instance Method Details

#changed!Object



232
# File 'lib/ditz/model.rb', line 232

def changed!; @changed = true end

#changed?Boolean

Returns:

  • (Boolean)


231
# File 'lib/ditz/model.rb', line 231

def changed?; @changed ||= false end

#deserialized_form_of(field, value) ⇒ Object

override these two to model per-field transformations between disk and memory.

convert disk form => memory form



55
56
57
# File 'lib/ditz/model.rb', line 55

def deserialized_form_of field, value
  value
end

#each_modelobjectObject

depth-first search on all reachable ModelObjects. fuck yeah.



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/ditz/model.rb', line 184

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

#inspectObject



181
# File 'lib/ditz/model.rb', line 181

def inspect; to_s end

#log(what, who, comment) ⇒ Object



226
227
228
229
# File 'lib/ditz/model.rb', line 226

def log what, who, comment
  add_log_event([Time.now, who, what, comment || ""])
  self
end

#save!(fn) ⇒ Object



203
204
205
206
207
# File 'lib/ditz/model.rb', line 203

def save! fn
  #FileUtils.mv fn, "#{fn}~", :force => true rescue nil
  File.open(fn, "w") { |f| f.puts to_yaml }
  self
end

#serialized_form_of(field, value) ⇒ Object

convert memory form => disk form



60
61
62
# File 'lib/ditz/model.rb', line 60

def serialized_form_of field, value
  value
end

#to_sObject



177
178
179
# File 'lib/ditz/model.rb', line 177

def to_s
  "<#{self.class.name}: " + self.class.field_names.map { |f| "#{f}: " + send(f).inspect }.join(", ") + ">"
end

#to_yaml(opts = {}) ⇒ Object



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/ditz/model.rb', line 209

def to_yaml opts={}
  ret = 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
  YamlWaml.decode(ret)
end

#to_yaml_typeObject



28
# File 'lib/ditz/model.rb', line 28

def to_yaml_type; "!#{self.class.yaml_domain}/#{self.class.yaml_other_thing}" end

#unchanged!Object



233
# File 'lib/ditz/model.rb', line 233

def unchanged!; @changed = false end