# encoding: utf-8
# Module methods for the modules serving as +NameMagic+
# namespaces. What is a +NameMagic+ namespace? For a class that
# includes +NameMagic+, namespace is the "civil registry" of all
# instances, both named and nameless. For this purpose, namespace
# has variable +@instances+. The registry of instances is a hash of
# pairs { instance => name }. Nameless instances have
# _nil_ value instead of name in the registry. In general Ruby,
# namespace would mean that the module holds the instances in
# constants looking like Namespace::Name. In Ruby core, we
# can see such behavior with Struct class, which has native
# constant magic and stores its instances in constants using Struct
# as a namespace. NameMagic used to perform this constant
# assignment in the namespace prior to YSupport version ~2.0. This
# is one of the reasons why names of instances must start with a
# capital letter and be usable as constant names. Since YSupport
# version 2.0+, NameMagic does no constant assignment on its own --
# all constant assignments are left to the user. In this way,
# NameMagic now sees constant assignment purely as a way to learn
# instance names intended by the user.
#
# The instance registry is accessible via +#instances+
# method. Individual instances can be queried for by +#instance+
# method, eg. by their names. Until Matz provides the possibility
# of constant magic in every class, which I requested some time
# ago, the registry of instances will remain the essential part,
# without which +NameMagic wouldn't work.
#
# === Life cycle of instances of +NameMagic+ user classes
#
# Let us consider for example Human class that uses NameMagic.
#
# class Human
# require 'y_support/name_magic'
# include NameMagic
# end
#
# Life cycle of Human in instances of begins, unsurprisingly, by
# instantiation. All instances are created nameless:
#
# newborn = Human.new
# newborn.name #=> nil
#
# User has several ways of naming the instances. Typical for
# +NameMagic+ is naming by constant assignment:
#
# Fred = newborn
# newborn.name #=> :Fred
#
# +NameMagic makes it possible to supply :name parameter directly
# to the #new method. Such instances are named immediately:
#
# newborn = Human.new name: "Joe"
# newborn.name #=> :Joe
#
# Another way is to name the instances using #name= method.
#
# newborn = Human.new
# newborn.name #=> nil
# newborn.name = "Mike"
# newborn.name #=> Mike
#
# Just for the record, we have created three instances:
#
# Human.instances #=> [Fred, Joe, Mike]
#
# In other words, at some point in their life, instances may or may
# not undergo baptism, which involves a complicated procedure of
# searching all existing Ruby modules for constants to which
# nameless instances of the class in question are
# assigned. Baptized instances then know their names, and can be
# accessed by their names through the instance registry:
#
# Human.instance( "Mike" ) #=> Mike
#
# Namespace gives the user 2 hook methods, #instantiation_exec and
# #exec_when_naming. The first one is executed upon instantiation
# and passed one argument, the new instance. The second one is
# executed when the namespace baptizes a new instance, and passed
# three arguments: suggested name, instance, and previous name if
# any. (Renaming instances may require special care.) Consequantly,
# you should define unary block with #instantiation_exec and
# ternary one with #exec_when_naming. Example:
#
# Human.instantiation_exec do |instance|
# puts "Instance with object id #{instance.object_id} created!"
# end
# newborn = Human.new #=> Instance with object id 75756140 created!
#
# The naming hook can also be used to censor and modify the
# intended name. Consider the following censorship:
#
# Human.exec_when_naming do |name, instance, old_name|
# fail NameError, "#{name.capitalize} is not a saint in the " +
# "Church of Emacs!" unless name.end_with? "gnucius"
# "St_IGNUcius"
# end
#
# Now we can no longer use ordinary names, we have to use names of
# saints in the Church of Emacs!
#
# newborn.name = "Dave"
# #=> NameError: Dave is not a saint in the Church of Emacs!
#
# Name Ignucius is OK, but the censor corrects it to St_IGNUcius:
#
# newborn.name = "Ignucius" #=> St_IGNUcius
#
# Life cycle of an instance ends when it is deleted from the
# instance registry and garbage-collected (unless something else
# holds a reference to it). Example:
#
# Human.instances #=> [Fred, Joe, Mike, St_IGNUcius]
# Human.forget "St_IGNUcius"
# Human.instances #=> [Fred, Joe, Mike]
#
# St. IGNUcius has just been deleted from the registry.
#
# Human.forget_all_instances
# Human.instances #=> []
#
# All Human instances have now been deleted from the registry, but
# only Joe and Mike are garbage-collected, because Fred is still
# assigned to a constant. Fred still exists, but he and his name
# has been deleted from the instance registry.
#
# Fred #=> #
#
# We could even re-register Fred by recreating his entry, although
# this is far from the way +NameMagic+ works in everyday life.
#
# Human.__instances__.merge! Fred => :Fred
# Human.instances #=> [Fred]
#
# === Avidity of the instances
#
# After the offered name is checked and modified by the name set
# hook closure, there is one more remaining problem to worry about:
# Whether the name is already used by another instance in the same
# namespace. If the name is taken, the ensuing action depends on
# whether the instance being named is _avid_. Avid instances are
# so eager to get a name, that they will steal the offered name for
# themselves even if other instances already use the name, making
# the conflicting instance nameless in the process. In +NameMagic+,
# it turns out to be convenient to make the new instances avid by
# default, unless the name was explicitly supplied to the
# constructor by +:name+ argument, or avidity suppressed by setting
# +:name_avid option to _false_.
#
# Techincally, avid instances are registered as an array kept by
# the namespace under the variable +@avid_instances+.
#
# === Forgetting instances
#
# As mentioned earlier, namespace can de-register, or forget
# instances. For this purpose, see methods +#forget+, +#__forget__,
# +#forget_nameless_instances+, +#forget_all_instances+.
#
# === Ersatz constant magic
#
# To imitate built-in constant magic of some Ruby classes,
# +NamespaceMethods+ provides ersatz method +#const_magic+, that
# searches all the modules in the object space for the pertinent
# instances newly assigned to constants. Method +#const_magic+ is
# called automatically before executing almost every public method
# of +NameMagic+, thus keeping the "civil registry"
# up-to-date. While not exactly computationally efficient, it tends
# to make the user code more readable and pays off in most
# usecases. For efficiency, we are looking forward to the
# +#const_assigned+ hook promised by Ruby core team...
#
# The namespace method versions that _do_ _not_ perform ersatz
# constant magic are generally denoted by underlines: Eg. methods
# +#__instances__+ and +#__forget__+ do not perform constant magic,
# while +#instances+ and +#forget+ do.
#
module NameMagic::Namespace
# Orders the namespace to disallow unnaming instances. As a
# consequence, the instances' names will now be permanent.
#
def permanent_names!
@permanent_names = true
end
# Inquirer whether unnaming instances has been disallowed in
# the namespace.
#
def permanent_names?
@permanent_names
end
# Presents the instances registered in this namespace.
#
def instances *args
const_magic
__instances__.keys
end
# Presents namespace-owned +@instances+ hash. The hash consists
# of pairs { instance => instance_name }
. Unnamed
# instances have +nil+ value instead of their name. This method
# does not trigger +#const_magic+.
#
def __instances__
@instances ||= {}
end
# Avid instances registered in this namespace. ("Avid" means that
# the instance will steal (overwrite) a name from another
# instance, should there be a conflict. The method does not
# trigger +#const_magic+.
#
def __avid_instances__
@avid_instances ||= []
end
# Returns the instance identified by the argument.
#
def instance arg
# In @instances hash, nil value denotes nameless instances!
fail TypeError,
"Nil is not an instance identifier!" if arg.nil?
# Get the list of all instances.
ii = instances
# If arg belongs to the list, just return it back.
return arg if ii.any? { |i| arg.equal? i }
# Assume that arg is an instance name.
name = arg.to_sym
registry = __instances__
ii.find { |i| registry[ i ] == name } or
fail NameError, "No instance #{arg} in #{self}!"
end
# Searches all the modules in the the object space for constants
# referring to receiver class objects, and names the found
# instances accordingly. Internally, it works by invoking
# private procedure +#search_all_modules. The return value is
# the remaining number of nameless instances.
#
def const_magic
return 0 if nameless_instances.size == 0
search_all_modules
return nameless_instances.size
end
# Returns those instances, whose name is nil. This method does
# not trigger #const_magic.
#
def nameless_instances *args
__instances__.select { |key, val| val.nil? }.keys
end
# Removes the specified instance from the registry. Note that
# this is different from "unnaming" an instance by setting
# inst.name = nil
, which makes the instance
# anonymous, but still registered.
#
def forget instance, *args
instance = begin
instance instance
rescue ArgumentError
return nil # nothing to forget
end
ɴ = instance.nil? ? nil : instance.name
# namespace.send :remove_const, ɴ if ɴ
__instances__.delete( instance )
__avid_instances__.delete( instance )
return instance
end
# Removes the specified instance from the registry, without
# performing #const_magic first. The argument should be a
# registered instance. Returns instance name for forgotten named
# instances, _nil_ for forgotten nameless instances, and _false_
# if the argument was not a registered instance.
#
def __forget__ instance
return false unless __instances__.keys.include? instance
# namespace.send :remove_const, instance.name if instance.name
__avid_instances__.delete( instance )
__instances__.delete instance
end
# Removes all anonymous instances from the registry.
#
def forget_nameless_instances
const_magic # #nameless_instances doesn't trigger it
nameless_instances.each { |instance|
__instances__.delete( instance )
__avid_instances__.delete( instance )
}
end
# Clears references to all the instances.
#
def forget_all_instances
instances.map { |instance| __forget__ instance }
# constants( false ).each { |sym|
# namespace.send :remove_const, sym if
# const_get( sym ).is_a? self }
end
# Registers a block to execute when a new instance of the
# +NameMagic+ user class is created. (In other words, this method
# provides user class'es instantiation hook.) Expects a unary
# block, whose argument is the new instance. Return value of the
# block is unimportant. The block will be executed in the context
# of the user class. If no block is given, the method returns the
# previously defined block, if any, or a default block that does
# nothing.
#
def instantiation_exec &block
@instantiation_exec = block if block
@instantiation_exec ||= -> instance { }
end
# Note: This alias must stay while the dependencies need it.
alias new_instance_hook instantiation_exec
# Registers a block to execute just prior to naming of an
# instance. (In other words, this method provides user class'es
# naming hook.) The block will be executed in the context of the
# user class and will be supplied three ordered arguments:
# suggested name, instance, and previous name. The block should
# thus be written as ternary, expecting these three arguments.
# The block can be used to validate / censor the suggested name
# and for this reason, it should return the censored name that
# will actually be requested for the instance. (Of course, just
# like there is no duty to use this hook, if you do use it, there
# is likewise no duty to censor the suggested name in it. You can
# just return the suggested name unchanged from the block.) The
# point is that the return value of the block will actually be
# used to name the instance. If no block is given, the method
# returns the previously defined block, if any, or a default
# block that does nothing and returns the suggested name without
# any changes.
#
def exec_when_naming &block
@exec_when_naming = block if block
@exec_when_naming ||=
-> name, instance, previous_name=nil { name }
end
# Note: This alias must stay while the dependencies need it.
alias name_set_hook exec_when_naming
# Registers a block to execute just prior to unnaming of an
# instance. (In other words, this method provides user class'es
# unnaming hook.) The block will be executed in the context of
# the user class and will be supplied two ordered arguments:
# instance and its previous name. The block can thus be written
# as up to binary. Return value of the block is unimportant. If
# no block is given, the method returns the block defined
# earlier, if any, or a default block that does nothing.
#
def exec_when_unnaming &block
@exec_when_unnaming = block if block
@exec_when_unnaming ||= -> instance, previous_name=nil { }
end
# Checks whether a name is acceptable as a constant name.
#
def validate_name name
# Note that the #try method (provided by 'y_support/literate')
# allows us to call the methods of name without mentioning
# it explicitly as the receiver, and it also allows us to
# raise errors without explicitly constructing the error
# messages. Thus, chars.first actually means name.chars.first.
# Error message (when error occurs) is constructed from
# the #try description and the #note strings, which act at
# the same time as code comments. End of advertisement for
# 'y_support/literate'.
#
name.to_s.try "to validate the suggested instance name" do
note "rejecting non-capitalized names"
fail NameError unless ( ?A..?Z ) === chars.first
note "rejecting names with spaces"
fail NameError if chars.include? ' '
end
# Return value is the validated name.
return name
end
private
# Searches all modules for user class instances.
#
def search_all_modules
# Set up the list of object ids to search. These are ids
# of all unnamed registered instances.
todo = ( nameless_instances + __avid_instances__ )
.map( &:object_id )
.uniq
# Browse all modules in the deep ObjectSpace for those ids.
ObjectSpace.each_object Module do |ɱ|
ɱ.constants( false ).each do |const_ß|
# Some constants cause unexpected problems. The line
# below is the result of trial-and-error programming
# and I am afraid to delete it quite yet.
ɱ == Object and case const_ß
when :Config then next
when :TimeoutError then next
end
# Those constants that raise certain errors upon attempts
# to access their contents are handled by this
# begin-rescue-end statement.
begin
instance = ɱ.const_get( const_ß )
rescue LoadError, StandardError
next # go on to the next constant
end
# We now go on to the next iteration of the loop if the
# constant which we are checking does not refer to the
# object with id we are searching for.
next unless todo.include? instance.object_id
# At this point, we have ascertained that the constant
# we are looking at contains unnamed instance.
if instance.avid? then
begin
# Name it rudely.
instance.name! const_ß
ensure
# Remove the "avid" flag from the instance.
instance.make_not_avid!
end
else
# Avid flag is not set, name the instance politely.
#
# Note that instances of NameMagic user classes are
# created avid. So if the anonymous instance is not
# avid at this point, it means it must have lost its
# avidity either being a previously named and now
# unnamed instance, or by the user explicitly invoking
# its #make_not_avid! method. In the first case, we do
# not want to see NameErrors raised ad infinitum just
# because there is some now-unnamed instance assigned
# to some forgotten constant in the deep namespace.
# In the second case, we must assume that the user
# takes the responsibility. So we will swallow NameError
# here:
begin
instance.name = const_ß
rescue NameError
end
end
# Remove the instance object id from todo list.
todo.delete instance.object_id
# Quit looping once todo list is empty.
break if todo.empty?
end
end
end
end # module NameMagic::NamespaceMethods