module Attributor class Model < Hash # FIXME: this is not the way to fix this. Really we should add finalize! to Models. undef :timeout undef :format undef :test rescue nil if RUBY_ENGINE =~ /^jruby/ # We are "forced" to require it here (in case hasn't been yet) to make sure the added methods have been applied require 'java' # Only to then delete them, to make sure we don't have them clashing with any attributes undef java, javax, org, com end # Remove undesired methods inherited from Hash undef :size undef :keys undef :values undef :has_key? @key_type = Symbol @value_type = Object @key_attribute = Attribute.new(@key_type) @value_attribute = Attribute.new(@value_type) def self.inherited(klass) k = self.key_type ka = self.key_attribute v = self.value_type va = self.value_attribute klass.instance_eval do @saved_blocks = [] @options = {} @keys = {} @key_type = k @value_type = v @key_attribute = ka @value_attribute = va @error = false end end # Define accessors for attribute of given name. # # @param name [::Symbol] attribute name # def self.define_accessors(name) name = name.to_sym self.define_reader(name) self.define_writer(name) end def self.define_reader(name) module_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{name} @contents[:#{name}] end RUBY end def self.define_writer(name) context = ["assignment","of(#{name})"].freeze module_eval do define_method(name.to_s + "=") do |value| self.set(name, value, context: context) end end end def self.check_option!(name, value) case name when :identity raise AttributorException, "Invalid identity type #{value.inspect}" unless value.kind_of?(::Symbol) :ok # FIXME ... actually do something smart, that doesn't break lazy attribute creation when :reference :ok # FIXME ... actually do something smart when :dsl_compiler :ok # FIXME ... actually do something smart when :dsl_compiler_options :ok else super end end def self.generate_subcontext(context, subname) context + [subname] end def self.example(context=nil, **values) context ||= ["#{self.name || 'Struct'}-#{rand(10000000)}"] context = Array(context) if self.keys.any? result = self.new result.extend(ExampleMixin) result.lazy_attributes = self.example_contents(context, result, values) else result = self.new end result end def initialize(data = nil) if data loaded = self.class.load(data) @contents = loaded.attributes else @contents = {} end end # TODO: memoize validation results here, but only after rejiggering how we store the context. # Two calls to validate() with different contexts should return get the same errors, # but with their respective contexts. def validate(context=Attributor::DEFAULT_ROOT_CONTEXT) raise AttributorException, "validation conflict" if @validating @validating = true context = [context] if context.is_a? ::String self.class.attributes.each_with_object(Array.new) do |(sub_attribute_name, sub_attribute), errors| sub_context = self.class.generate_subcontext(context,sub_attribute_name) value = self.__send__(sub_attribute_name) if value.respond_to?(:validating) # really, it's a thing with sub-attributes next if value.validating end errors.push *sub_attribute.validate(value, sub_context) end ensure @validating = false end def attributes @contents end def respond_to_missing?(name,*) attribute_name = name.to_s attribute_name.chomp!('=') return true if self.class.attributes.key?(attribute_name.to_sym) super end def method_missing(name, *args) attribute_name = name.to_s attribute_name.chomp!('=') if self.class.attributes.has_key?(attribute_name.to_sym) self.class.define_accessors(attribute_name) return self.__send__(name, *args) end super end def dump(context: Attributor::DEFAULT_ROOT_CONTEXT, **opts) return CIRCULAR_REFERENCE_MARKER if @dumping @dumping = true self.attributes.each_with_object({}) do |(name, value), result| attribute = self.class.attributes[name] # skip dumping undefined attributes unless attribute warn "WARNING: Trying to dump unknown attribute: #{name.inspect} with context: #{context.inspect}" next end result[name.to_sym] = attribute.dump(value, context: context + [name] ) end ensure @dumping = false end end end