# frozen_string_literal: true require 'attr_json/attribute_definition' module AttrJson class AttributeDefinition # Attached to a class to record the json attributes registered, # with either AttrJson::Record or AttrJson::Model. # # Think of it as mostly like a hash keyed by attribute name, value # an AttributeDefinition. # # It is expected to be used by AttrJson::Record and AttrJson::Model, # you shouldn't need to interact with it directly. # # It is intentionally immutable to make it harder to accidentally mutate # a registry shared with superclass in a `class_attribute`, instead of # properly assigning a new modified registry. # # self.some_registry_attribute = self.some_registry_attribute.with( # attr_definition_1, attr_definition_2 # ) # # => Returns a NEW AttributeDefinition object # # All references in code to "definition" are to a AttrJson::AttributeDefinition instance. class Registry def initialize(hash = {}) @name_to_definition = hash.dup @store_key_to_definition = {} @name_to_definition.values.each { |d| store_key_index!(d) } @container_attributes_registered = Hash.new { Set.new } end def fetch(key, *args, &block) @name_to_definition.fetch(key.to_sym, *args, &block) end def [](key) @name_to_definition[key.to_sym] end def has_attribute?(key) @name_to_definition.has_key?(key.to_sym) end def type_for_attribute(key) self[key].type end # Can return nil if none found. def store_key_lookup(container_attribute, store_key) @store_key_to_definition[AttrJson.efficient_to_s(container_attribute)] && @store_key_to_definition[AttrJson.efficient_to_s(container_attribute)][AttrJson.efficient_to_s(store_key)] end def definitions # Since we are immutable, we can cache this to avoid allocation in a hot spot @stored_definitions ||= @name_to_definition.values end # Returns all registered attributes as an array of symbols def attribute_names @name_to_definition.keys end def container_attributes @store_key_to_definition.keys.collect { |s| AttrJson.efficient_to_s(s) } end # This is how you register additional definitions, as a non-mutating # return-a-copy operation. def with(*definitions) self.class.new(@name_to_definition).tap do |copied| definitions.each do |defin| copied.add!(defin) end end end # We need to lazily set the container type only the FIRST time # # While also avoiding this triggering ActiveRecord to actually go to DB, # we don't want DB connection forced on boot, that's a problem for many apps, # including that may not have a DB connection in initial development. # (#type_for_attribute forces DB connection) # # AND we need to call container attriubte on SUB-CLASS AGAIN, iff sub-class # has any of it's own new registrations, to make sure we get the right type in # sub-class! # # So we just keep track of whether we've registered ourselves, so we can # first time we need to. # # While current implementation is simple, this has ended up a bit fragile, # a different API that doesn't require us to do this implicitly lazily # might be preferred! But this is what we got for now. def register_container_attribute(attribute_name:, model:) @container_attributes_registered[attribute_name.to_sym] << model end def container_attribute_registered?(attribute_name:, model:) @container_attributes_registered[attribute_name.to_sym].include?(model) end protected def add!(definition) if @name_to_definition.has_key?(definition.name) raise ArgumentError, "Can't add, conflict with existing attribute name `#{definition.name.to_sym}`: #{@name_to_definition[definition.name].original_args}" end @name_to_definition[definition.name.to_sym] = definition store_key_index!(definition) @stored_definitions = nil end def store_key_index!(definition) container_hash = (@store_key_to_definition[AttrJson.efficient_to_s(definition.container_attribute)] ||= {}) if container_hash.has_key?(AttrJson.efficient_to_s(definition.store_key)) existing = container_hash[AttrJson.efficient_to_s(definition.store_key)] raise ArgumentError, "Can't add, store key `#{definition.store_key}` conflicts with existing attribute: #{existing.original_args}" end container_hash[AttrJson.efficient_to_s(definition.store_key)] = definition end end end end