require'hashie/hash'require'set'moduleHashie# A Dash is a 'defined' or 'discrete' Hash, that is, a Hash# that has a set of defined keys that are accessible (with# optional defaults) and only those keys may be set or read.## Dashes are useful when you need to create a very simple# lightweight data object that needs even fewer options and# resources than something like a DataMapper resource.## It is preferrable to a Struct because of the in-class# API for defining properties as well as per-property defaults.classDash<HashincludeHashie::Extensions::PrettyInspectalias_method:to_s,:inspect# Defines a property on the Dash. Options are# as follows:## * <tt>:default</tt> - Specify a default value for this property,# to be returned before a value is set on the property in a new# Dash.## * <tt>:required</tt> - Specify the value as required for this# property, to raise an error if a value is unset in a new or# existing Dash. If a Proc is provided, it will be run in the# context of the Dash instance. If a Symbol is provided, the# property it represents must not be nil. The property is only# required if the value is truthy.## * <tt>:message</tt> - Specify custom error message for required property#defself.property(property_name,options={})properties<<property_nameifoptions.key?(:default)defaults[property_name]=options[:default]elsifdefaults.key?(property_name)defaults.deleteproperty_nameendunlessinstance_methods.map(&:to_s).include?("#{property_name}=")define_method(property_name){|&block|self.[](property_name,&block)}property_assignment="#{property_name}=".to_symdefine_method(property_assignment){|value|self.[]=(property_name,value)}endifdefined?@subclasses@subclasses.each{|klass|klass.property(property_name,options)}endcondition=options.delete(:required)ifconditionmessage=options.delete(:message)||"is required for #{name}."required_properties[property_name]={condition: condition,message: message}elsefailArgumentError,'The :message option should be used with :required option.'ifoptions.key?(:message)endendclass<<selfattr_reader:properties,:defaultsattr_reader:required_propertiesendinstance_variable_set('@properties',Set.new)instance_variable_set('@defaults',{})instance_variable_set('@required_properties',{})defself.inherited(klass)super(@subclasses||=Set.new)<<klassklass.instance_variable_set('@properties',properties.dup)klass.instance_variable_set('@defaults',defaults.dup)klass.instance_variable_set('@required_properties',required_properties.dup)end# Check to see if the specified property has already been# defined.defself.property?(name)properties.include?nameend# Check to see if the specified property is# required.defself.required?(name)required_properties.key?nameend# You may initialize a Dash with an attributes hash# just like you would many other kinds of data objects.definitialize(attributes={},&block)super(&block)self.class.defaults.each_pairdo|prop,value|self[prop]=beginval=value.dupifval.is_a?(Proc)val.arity==1?val.call(self):val.callelsevalendrescueTypeErrorvalueendendinitialize_attributes(attributes)assert_required_attributes_set!endalias_method:_regular_reader,:[]alias_method:_regular_writer,:[]=private:_regular_reader,:_regular_writer# Retrieve a value from the Dash (will return the# property's default value if it hasn't been set).def[](property)assert_property_exists!propertyvalue=super(property)# If the value is a lambda, proc, or whatever answers to call, eval the thing!ifvalue.is_a?Procself[property]=value.call# Set the result of the call as a valueelseyieldvalueifblock_given?valueendend# Set a value on the Dash in a Hash-like way. Only works# on pre-existing properties.def[]=(property,value)assert_property_required!property,valueassert_property_exists!propertysuper(property,value)enddefmerge(other_hash)new_dash=dupother_hash.eachdo|k,v|new_dash[k]=block_given??yield(k,self[k],v):vendnew_dashenddefmerge!(other_hash)other_hash.eachdo|k,v|self[k]=block_given??yield(k,self[k],v):vendselfenddefreplace(other_hash)other_hash=self.class.defaults.merge(other_hash)(keys-other_hash.keys).each{|key|delete(key)}other_hash.each{|key,value|self[key]=value}selfenddefupdate_attributes!(attributes)initialize_attributes(attributes)self.class.defaults.each_pairdo|prop,value|self[prop]=beginvalue.duprescueTypeErrorvalueendifself[prop].nil?endassert_required_attributes_set!endprivatedefinitialize_attributes(attributes)attributes.each_pairdo|att,value|self[att]=valueendifattributesenddefassert_property_exists!(property)fail_no_property_error!(property)unlessself.class.property?(property)enddefassert_required_attributes_set!self.class.required_properties.each_keydo|required_property|assert_property_set!(required_property)endenddefassert_property_set!(property)fail_property_required_error!(property)ifsend(property).nil?&&required?(property)enddefassert_property_required!(property,value)fail_property_required_error!(property)ifvalue.nil?&&required?(property)enddeffail_property_required_error!(property)failArgumentError,"The property '#{property}' #{self.class.required_properties[property][:message]}"enddeffail_no_property_error!(property)failNoMethodError,"The property '#{property}' is not defined for #{self.class.name}."enddefrequired?(property)returnfalseunlessself.class.required?(property)condition=self.class.required_properties[property][:condition]caseconditionwhenProcthen!!(instance_exec(&condition))whenSymbolthen!!(send(condition))else!!(condition)endendendend