lib/mattock/configurable.rb in mattock-0.2.13 vs lib/mattock/configurable.rb in mattock-0.3.0

- old
+ new

@@ -8,22 +8,199 @@ #Mattock also includes a yard-extension that will document settings of a #Configurable # #@example (see ClassMethods) module Configurable - RequiredField = Object.new - class << RequiredField - def to_s - "<unset>" + class FieldMetadata + attr_accessor :name, :default_value + + DEFAULT_PROPERTIES = { + :copiable => true, + :proxiable => true, + :required => false, + :runtime => false, + :defaulting => true, + } + def initialize(name, value) + @name = name + @default_value = value + @properties = DEFAULT_PROPERTIES.clone end def inspect - to_s + set_props = DEFAULT_PROPERTIES.keys.find_all do |prop| + @properties[prop] + end + "Field: #{name}: #{default_value.inspect} #{set_props.inspect}" end + + def validate_property_name(name) + unless DEFAULT_PROPERTIES.has_key?(name) + raise "Invalid field property #{name.inspect} - valid are: #{DEFAULT_PROPERTIES.keys.inspect}" + end + end + + def is?(property) + validate_property_name(property) + @properties[property] + end + + def is_not?(property) + validate_property_name(property) + !@properties[property] + end + alias isnt? is_not? + + def is(property) + validate_property_name(property) + @properties[property] = true + self + end + + def is_not(property) + validate_property_name(property) + @properties[property] = false + self + end + alias isnt is_not + + def ivar_name + "@#{name}" + end + + def writer_method + "#{name}=" + end + + def reader_method + name + end + + def immediate_value_on(instance) + instance.instance_variable_get(ivar_name) + end + + def value_on(instance) + value = immediate_value_on(instance) + if ProxyValue === value + value.field.value_on(value.source) + else + value + end + end + + def set_on?(instance) + return false unless instance.instance_variable_defined?(ivar_name) + value = immediate_value_on(instance) + if name == :destination_path + end + if ProxyValue === value + value.field.set_on?(value.source) + else + true + end + end + + def unset_on?(instance) + !set_on?(instance) + end + + def missing_on?(instance) + return false unless is?(:required) + if instance.respond_to?(:runtime?) and !instance.runtime? + return runtime_missing_on?(instance) + else + return !set_on?(instance) + end + end + + def runtime_missing_on?(instance) + return false if is?(:runtime) + return true unless instance.instance_variable_defined?(ivar_name) + value = immediate_value_on(instance) + if ProxyValue === value + value.field.runtime_missing_on?(value.source) + else + false + end + end end - RequiredField.freeze + class ProxyValue + def initialize(source, field) + @source, @field = source, field + end + attr_reader :source, :field + + def inspect + "#{self.class.name.split(':').last}: #{value}" + end + end + + class ProxyDecorator + def initialize(configurable) + @configurable = configurable + end + + def method_missing(name, *args, &block) + super unless block.nil? and args.empty? + super unless @configurable.respond_to?(name) + return ProxyValue.new(@configurable, @configurable.class.field_metadata(name)) + end + end + + class FieldProcessor + def initialize(source) + @source = source + @field_names = filter(source.class.field_names) + end + attr_accessor :field_names + attr_reader :source + + def filter_attribute + raise NotImplementedError + end + + def filter(field_names) + field_names.find_all do |name| + source.class.field_metadata(name).is?(filter_attribute) + end + end + + def value(field) + source.__send__(field.reader_method) + end + + def to(target) + field_names.each do |name| + field = source.class.field_metadata(name) + next unless target.respond_to?(field.writer_method) + target.__send__(field.writer_method, value(field)) + end + end + end + + class SettingsCopier < FieldProcessor + def filter_attribute + :copiable + end + + def value(field) + field.immediate_value_on(source) + end + end + + class SettingsProxier < FieldProcessor + def filter_attribute + :proxiable + end + + def value(field) + ProxyValue.new(source, field) + end + end + #Describes class level DSL & machinery for working with configuration #managment. # #@example # class ConfExample @@ -44,179 +221,264 @@ # ce.hoo #=> nil # ce.hoo = "hallo" # ce.check_required #=> raises error because :must and :foo aren't set module ClassMethods def default_values - @default_values ||= {} + @default_values ||= [] end - def set_defaults_on(instance) + def field_names + names = default_values.map{|field| field.name} if Configurable > superclass - superclass.set_defaults_on(instance) + names | superclass.field_names + else + names end - default_values.each_pair do |name,value| - instance.__send__("#{name}=", value) - if Configurable === value - value.class.set_defaults_on(value) - end - end end - def missing_required_fields_on(instance) - missing = [] - if Configurable > superclass - missing = superclass.missing_required_fields_on(instance) + def field_metadata(name) + field = default_values.find{|field| field.name == name} + if field.nil? and Configurable > superclass + superclass.field_metadata(name) + else + field end - default_values.each_pair do |name,value| - set_value = instance.__send__(name) - if value == RequiredField and set_value == RequiredField - missing << name - next - end - if Configurable === set_value - missing += set_value.class.missing_required_fields_on(set_value).map do |field| - [name, field].join(".") - end - end - end - return missing end - def copy_settings(from, to) - if Configurable > superclass - superclass.copy_settings(from, to) - end - default_values.keys.each do |field| - begin - to.__send__("#{field}=", from.__send__(field)) - rescue NoMethodError - #shrug it off - end - end - end - - def to_hash(obj) - hash = if Configurable > superclass - superclass.to_hash(obj) - else - {} - end - hash.merge( Hash[default_values.keys.zip(default_values.keys.map{|key| - begin - obj.__send__(key) - rescue NoMethodError - end - }).to_a]) - end - #Creates an anonymous Configurable - useful in complex setups for nested #settings #@example SSH options # setting :ssh => nested(:username => "me", :password => nil) - def nested(hash=nil) - obj = Class.new(Struct).new - obj.settings(hash || {}) - return obj + def nested(hash=nil, &block) + nested = Class.new(Struct) + nested.settings(hash || {}) + if block_given? + nested.instance_eval(&block) + end + return nested end #Quick list of setting fields with a default value of nil. Useful #especially with {CascadingDefinition#resolve_configuration} def nil_fields(*names) names.each do |name| setting(name, nil) end + self end alias nil_field nil_fields #List fields with no default for with a value must be set before #definition. def required_fields(*names) names.each do |name| setting(name) end + self end alias required_field required_fields + RequiredField = Object.new.freeze + #Defines a setting on this class - much like a attr_accessible call, but #allows for defaults and required settings def setting(name, default_value = RequiredField) name = name.to_sym - attr_accessor(name) - if default_values.has_key?(name) and default_values[name] != default_value - warn "Changing default value of #{self.name}##{name} from #{default_values[name].inspect} to #{default_value.inspect}" + metadata = + if default_value == RequiredField + FieldMetadata.new(name, nil).is(:required).isnt(:defaulting) + else + FieldMetadata.new(name, default_value) + end + + attr_writer(name) + define_method(metadata.reader_method) do + value = metadata.value_on(self) end - default_values[name] = default_value + + if existing = default_values.find{|field| field.name == name} and existing.default_value != default_value + source_line = caller.drop_while{|line| /#{__FILE__}/ =~ line}.first + warn "Changing default value of #{self.name}##{name} from #{existing.default_value.inspect} to #{default_value.inspect}" + " (at: #{source_line})" + end + default_values << metadata + metadata end + def runtime_required_fields(*names) + names.each do |name| + runtime_setting(name) + end + self + end + alias runtime_required_field runtime_required_fields + + def runtime_setting(name, default_value = RequiredField) + setting(name, default_value).is(:runtime) + end + #@param [Hash] hash Pairs of name/value to be converted into # setting/default def settings(hash) hash.each_pair do |name, value| setting(name, value) end return self end + alias runtime_settings settings + def set_defaults_on(instance) + if Configurable > superclass + superclass.set_defaults_on(instance) + end + default_values.each do |field| + next unless field.is? :defaulting + value = field.default_value + if Module === value and Configurable > value + value = value.new + value.class.set_defaults_on(value) + end + instance.__send__(field.writer_method, value) + end + end + + def missing_required_fields_on(instance) + missing = [] + if Configurable > superclass + missing = superclass.missing_required_fields_on(instance) + end + default_values.each do |field| + if field.missing_on?(instance) + missing << field.name + else + set_value = instance.__send__(field.reader_method) + if Configurable === set_value + missing += set_value.class.missing_required_fields_on(set_value).map do |field| + [name, field].join(".") + end + end + end + end + return missing + end + + def copy_settings(from, to, &block) + if Configurable > superclass + superclass.copy_settings(from, to, &block) + end + default_values.each do |field| + begin + value = + if block_given? + yield(from, field) + else + from.__send__(field.reader_method) + end + if Configurable === value + value = value.clone + end + to.__send__(field.writer_method, value) + rescue NoMethodError + #shrug it off + end + end + end + + def to_hash(obj) + hash = if Configurable > superclass + superclass.to_hash(obj) + else + {} + end + hash.merge( Hash[default_values.map{|field| + begin + value = obj.__send__(field.reader_method) + value = + case value + when Configurable + value.to_hash + else + value + end + [field.name, value] + rescue NoMethodError + end + }]) + end + + def included(mod) mod.extend ClassMethods end end extend ClassMethods + def copy_settings + SettingsCopier.new(self) + end + def copy_settings_to(other) - self.class.copy_settings(self, other) + copy_settings.to(other) self end + def proxy_settings + SettingsProxier.new(self) + end + + def proxy_settings_to(other) + proxy_settings.to(other) + end + def to_hash self.class.to_hash(self) end + def unset_defaults_guard + raise "Tried to check required settings before running setup_defaults" + end + #Call during initialize to set default values on settings - if you're using #Configurable outside of Mattock, be sure this gets called. def setup_defaults + def self.unset_defaults_guard + end + self.class.set_defaults_on(self) self end #Checks that all required fields have be set, otherwise raises an error #@raise RuntimeError if any required fields are unset def check_required + unset_defaults_guard missing = self.class.missing_required_fields_on(self) unless missing.empty? raise "Required field#{missing.length > 1 ? "s" : ""} #{missing.map{|field| field.to_s.inspect}.join(", ")} unset on #{self.inspect}" end self end + def proxy_value + ProxyDecorator.new(self) + end + + #XXX deprecate def unset?(value) - value == RequiredField + value.nil? end - def setting(name, default_value = nil) - self.class.setting(name, default_value) - instance_variable_set("@#{name}", default_value) + def field_unset?(name) + self.class.field_metadata(name).unset_on?(self) end - def settings(hash) - hash.each_pair do |name, value| - setting(name, value) + def fail_unless_set(name) + if self.class.field_metadata(name).unset_on?(self) + raise "Assertion failed: Field #{name} unset" end - return self + true end - - def required_fields(*names) - self.class.required_fields(*names) - self - end - alias required_field required_fields - - def nil_fields(*names) - self.class.nil_fields(*names) - self - end - alias nil_field nil_fields class Struct include Configurable end end