Hal Fulton wrote:
For those curious, what I want to do is establish defaults for
fields in an object, such that:
1. When I dump an object, fields that still have the default
values will be left unspecified.
2. When I load an object, unspecified fields will be given their
default values.
Hi, Hal. Good for you, bringing this up again. We didn't finish this discussion at all and the clock has struck loudly here: time to continue.
In Hobix, I am currently using a mixin for classes which need the above. This mixin relies upon the presence of a property_map method within those classes, which defines the full set of instance variables which I want to output in YAML, along with whether those fields are optional or not.
The mixin also requires default_#{ivar} methods for every instance variable you plan on having a default for. The mixin will use nil as the default, if no such method is found. You could include defaults in the property_map, but I sometimes base the default value of a field on the value of other fields (in the case of file paths), so I use a method for each instead.
So, for example, Hobix blog entries only require @title, @content and @author. Other fields are optional and aren't output to YAML if they are blank.
The Hobix::Entry#property_map basically looks like this:
class Hobix::Entry
include ToYamlExtras
def property_map
[
['@title', :req],
['@author', :req],
['@contributors', :opt],
['@created', :opt],
['@tagline', :opt],
['@summary', :opt],
['@content', :req]
]
end
end
I'm using an Array to ensure ordering of the elements upon output.
Here would be the ToYamlExtras mixin:
module ToYamlExtras
def to_yaml_properties
property_map.find_all do |prop, req|
case req
when :opt
val = nil
if respond_to?( "default_#{ prop[1..-1] }" )
val = method( "default_#{ prop[1..-1] }" ).call
end
val != instance_variable_get( prop )
when :req
true
end
end.
collect { |prop, req| prop }
end
def initialize
apply_defaults
yield self if block_given?
end
def apply_defaults
property_map.each do |prop, req|
name = prop[1..-1]
if instance_variable_get( prop ).nil? and
respond_to?( "default_#{ name }" )
instance_variable_set( prop,
method( "default_#{ name }" ).call )
end
end
self
end
end
As you can see, the mixin simply defines a to_yaml_properties method, which is a method understood by the YAML library. The to_yaml_properties method should return an Array of instance variable names, in the order they are output.
As you can see, the mixin also defines 'initialize', since you'll probably want to apply the defaults even if the object is created programmatically.
You'll also need to hook yourself a YAML type for each class. In the case of the Hobix::Entry class, I'm hooking !hobix.com,2004/entry.
class Hobix::Entry
def to_yaml_type
"!hobix.com,2004/entry"
end
end
YAML::add_domain_type( 'hobix.com,2004', 'entry' ) do |type, val|
YAML::object_maker( Hobix::Entry, val ).apply_defaults
end
A script including this mixin and a few tests of the above can be found at: http://whytheluckystiff.net/ruby/yaml-defaults-mixin.rb
_why