lib/observed/configurable.rb
module Observed
# Indicates that classes included this module to have attributes which are configurable.
# `configurable` means that the attributes can be configured via named parameters of
# the constructor and the `configure` instance method of the class included this module.
module Configurable
def initialize(args={})
configure(args)
end
def configure(args={})
if @attributes
@attributes.merge! args
else
@attributes ||= args.dup
end
self
end
# @param [String|Symbol] name
def has_attribute_value?(name)
!! get_attribute_value(name)
end
# @param [String|Symbol] name
# @return [Object] In order of precedence, the value of the instance variable named `"@" + name`,
# or the value `@attributes[name]`, or the default value for the attribute named `name`
def get_attribute_value(name)
instance_variable_get("@#{name.to_s}") || @attributes[name] || self.class.defaults[name]
end
module ClassMethods
# @param [String|Symbol] name
def attribute(name, options={})
unless instance_methods.include? name.intern
define_method(name) do
get_attribute_value(name) || fail_for_not_configured_parameter(name)
end
end
default_value = options && options[:default]
default name => default_value if default_value
end
def default(args)
@defaults = defaults.merge(args)
end
def defaults
@defaults ||= {}
end
def create(args)
self.new(args)
end
# Inherits the default values stored in @defaults to the sub-class
def inherited(klass)
super if defined? super
klass.default defaults
end
end
module ModuleMethods
# @param [String|Symbol] name
def attribute(name, options={})
@attributes ||= {}
@attributes = @attributes.merge(name => options)
end
def attributes
@attributes ||
fail(<<EOS
#{self} includes Observed::Configurable. Though, no attributes are configured for #{self}.
We don't need to include Observed::Configurable, or it might be a bug?
EOS
)
end
def included(klass)
ensure_configurable klass
attributes.each do |name, options|
klass.attribute name, options
end
end
def ensure_configurable(klass)
unless klass.include? Configurable
fail "The class #{klass} must include Observed::Configurable to include #{self}"
end
end
end
class NotConfiguredError < RuntimeError; end
class << self
def included(klass)
if klass.is_a? Class
klass.extend ClassMethods
else
klass.extend ModuleMethods
end
end
end
private
def fail_for_not_configured_parameter(name)
fail NotConfiguredError.new("The parameter `#{name}` is not configured. attributes=#{@attributes}, defaults=#{self.class.defaults}")
end
end
end