class NRSER::Props::Metadata

@todo document NRSER::Props::ClassMetadata class.

Constants

VARIABLE_NAME

Constants

Public Class Methods

has_metadata?(klass) click to toggle source

Class Methods

# File lib/nrser/props/metadata.rb, line 50
def self.has_metadata? klass
  klass.instance_variable_defined? VARIABLE_NAME
end
metadata_for(klass) click to toggle source
# File lib/nrser/props/metadata.rb, line 55
def self.metadata_for klass
  klass.instance_variable_get VARIABLE_NAME
end
new(klass) click to toggle source

Instantiate a new `NRSER::Props::ClassMetadata`.

# File lib/nrser/props/metadata.rb, line 68
def initialize klass
  @klass = klass
  @props = {}
  @invariants = Set.new
  @storage = nil
end

Public Instance Methods

[](name)
Alias for: get_prop
default_storage() click to toggle source
# File lib/nrser/props/metadata.rb, line 350
def default_storage
  if superclass_has_metadata?
    superclass_metadata.storage
  else
    @storage = NRSER::Props::Storage::InstanceVariable.new
  end
end
each_primary_prop_value_from(values, &block) click to toggle source

Check primary prop values and fill in defaults, yielding `(Prop, VALUE)` to the `&block`.

Used when initializing instances.

@param [#each_pair | each_index] values

Collection of prop values iterable by key/value pairs or by indexed
entries.

@param [Proc<(NRSER::Props::Prop, VALUE)>] block

Block that will receive primary prop and value pairs.

@raise [TypeError]

If a value is does not satisfy it's {NRSER::Props::Prop#type}.

@raise [ArgumentError]

If `values` doesn't respond to `#each_pair` or `#each_index`.

@raise [NameError]

If a value is not provided for a primary prop and a default can not
be created.

@raise [TSort::Cyclic]

If any of the primary prop's {NRSER::Props::Prop#deps} for dependency
cycles.
# File lib/nrser/props/metadata.rb, line 215
  def each_primary_prop_value_from values, &block
    primary_props = props only_primary: true
    
    props_by_names_and_aliases = {}
    primary_props.each_value do |prop|
      [prop.name, *prop.aliases].each do |sym|
        if props_by_names_and_aliases.key? sym
          other_prop = props_by_names_and_aliases[sym]
          
          prop_sym_is = sym == prop.name ? 'name' : 'alias'
          other_prop_sym_is = sym == other_prop.name ? 'name' : 'alias'
          
          raise NRSER::ConflictError.new \
            "Prop",  prop.to_desc, prop_sym_is, sym.inspect,
            "conflicts with", other_prop.to_desc, "of", other_prop_sym_is
        end
        
        props_by_names_and_aliases[sym] = prop
      end
    end
    
    # Normalize values to a `Hash<Symbol, VALUE>` so everything can deal with
    # one form. Default values will be set here as they're resolved and made
    # available to subsequent {Prop#default} calls.
    values_by_name = {}
    
    if values.respond_to? :each_pair
      values.each_pair { |key, value|
        # Figure out the prop name {Symbol}
        name = case key
        when Symbol
          key
        when String
          key.to_sym
        else
          key.to_s.to_sym
        end
        
        # If the `name` corresponds to a primary prop set it in the values by
        # name
        #
        if props_by_names_and_aliases.key? name
          values_by_name[ props_by_names_and_aliases[name].name ] = value
        end
      }
    elsif values.respond_to? :each_index
      indexed = []
      
      primary_props.each_value do |prop|
        indexed[prop.index] = prop unless prop.index.nil?
      end
      
      values.each_index { |index|
        prop = indexed[index]
        values_by_name[prop.name] = values[index] if prop
      }
    else
      raise ArgumentError.new binding.erb <<~END
        `source` argument must respond to `#each_pair` or `#each_index`
        
        Found:
        
            <%= source.pretty_inspect %>
        
      END
    end
    
    # Way to noisy, even for trace
    # logger.trace "Ready to start loading values",
    #   values: values,
    #   values_by_name: values_by_name,
    #   props_by_names_and_aliases: props_by_names_and_aliases.map { |k, v|
    #     [ k, v.to_desc ]
    #   }.to_h
    
    # Topological sort the primary props by their default dependencies.
    #
    NRSER::Graph::TSorter.new(
      primary_props.each_value
    ) { |prop, &on_dep_prop|
      #
      # This block is responsible for receiving a {Prop} and a callback
      # block and invoking that callback block on each of the prop's
      # dependencies (if any).
      #
      prop.deps.each { |name|
        if primary_props.key? name
          on_dep_prop.call primary_props[name]
        else
          raise RuntimeError.new binding.erb <<~END
            Property <%= prop.full_name %> depends on prop `<%= name %>`,
            but no primary prop with that name could be found!
          END
        end
      }
    }.tsort_each do |prop|
      # {Prop} instances will now be yielded in an order that allows any
      # inter-dependencies to be resolved (as long as there weren't dependency
      # cycles, which {NRSER::Graph::TSorter} will raise if it finds)
      
      # If we have a value for the prop, just check that
      if values_by_name.key? prop.name
        prop.check! values_by_name[prop.name]
      else
        # Otherwise, get the default value, providing the values we already
        # know in case the default is a {Proc} that needs some of them.
        #
        # We set that value in `values_by_name` so that subsequent
        # {Prop#default} calls can use it.
        #
        values_by_name[prop.name] = prop.default **values_by_name
      end
      
      # Yield the {Prop} and it's value back to the `&block`
      block.call prop, values_by_name[prop.name]
    end # .tsort_each
  end
freeze() click to toggle source
Calls superclass method
# File lib/nrser/props/metadata.rb, line 372
def freeze
  super()

  if  superclass_has_metadata? &&
      !superclass_metadata.frozen?
    superclass_metadata.freeze
  end
  
end
get_prop(name) click to toggle source
# File lib/nrser/props/metadata.rb, line 89
def get_prop name
  name = name.to_s.to_sym unless name.is_a?( Symbol )
  
  return @props[name] if @props.key?( name )
  
  if superclass_has_metadata?
    superclass_metadata.get_prop name
  else
    nil
  end
end
Also aliased as: []
invariant(type) click to toggle source
# File lib/nrser/props/metadata.rb, line 345
def invariant type
  @invariants.add type
end
invariants(only_own: false) click to toggle source
# File lib/nrser/props/metadata.rb, line 334
def invariants only_own: false
  result = if !only_own && superclass_has_metadata?
    superclass_metadata.invariants only_own: false
  else
    Set.new
  end
  
  result + @invariants
end
prop(name, **opts) click to toggle source

Define a property.

@param [Symbol] name

The name of the property.

@param [Hash{ Symbol => Object }] opts

Constructor options for {NRSER::Props::Prop}.

@return [NRSER::Props::Prop]

The newly created prop, thought you probably don't need it (it's
already all bound up on the class at this point), but why not?
# File lib/nrser/props/metadata.rb, line 153
  def prop name, **opts
    t.non_empty_sym.check! name
    
    if @props.key? name
      raise ArgumentError.new binding.erb <<~END
        Prop <%= name.inspect %> already set for <%= @klass %>:
        
            <%= @props[name].inspect %>
      END
    end
    
    prop = NRSER::Props::Prop.new @klass, name, **opts
    @props[name] = prop
    
    prop.names.each do |name|
      if prop.create_reader? name
        @klass.class_eval do
          define_method name do
            prop.get self
          end
        end
      end
      
      if prop.create_writer? name
        @klass.class_eval do
          define_method "#{ name }=" do |value|
            prop.set self, value
          end
        end
      end
    end
    
    prop
  end
prop_names(only_own: false, only_primary: false) click to toggle source

@todo

Cache / optimize
# File lib/nrser/props/metadata.rb, line 107
def prop_names only_own: false, only_primary: false
  Set.new props( only_own: only_own, only_primary: only_primary ).keys
end
props(only_own: false, only_primary: false) click to toggle source

Get a map of property names to property instances.

@param [Boolean] only_own

Don't include super-class properties.

@param [Boolean] only_primary

Don't include properties that have a {NRSER::Props::Prop#source}.

@return [Hash{ Symbol => NRSER::Props::Prop }]

Hash mapping property name to property instance.
# File lib/nrser/props/metadata.rb, line 123
def props only_own: false, only_primary: false
  result = if !only_own && superclass_has_metadata?
    superclass_metadata.props only_own: only_own,
                              only_primary: only_primary
  else
    {}
  end
  
  if only_primary
    @props.each {|name, prop| result[name] = prop if prop.primary? }
  else
    result.merge! @props
  end
  
  result
end
storage(value = nil) click to toggle source
# File lib/nrser/props/metadata.rb, line 359
def storage value = nil
  if value.nil?
    if @storage.nil?
      default_storage
    else
      @storage
    end
  else
    @storage = value
  end
end
superclass_has_metadata?() click to toggle source

Instance Methods

# File lib/nrser/props/metadata.rb, line 79
def superclass_has_metadata?
  @klass.superclass.respond_to? :metadata
end
superclass_metadata() click to toggle source
# File lib/nrser/props/metadata.rb, line 84
def superclass_metadata
   @klass.superclass.metadata
end