Skip to content

Repository files navigation

Constant

Utilities for importing constants and inspecting constants.

Avoid the unintended effects of including constants by aliasing their inner constants instead of accessing them via Ruby mixin.

Constant::Import

Ruby's include statement is often used as an import statement common to other languages. An import is used to provide a shortcut for accessing constants without having to fully qualify the namespace.

Constant::Import does what you reach for include to do as an import: it makes inner constants accessible without having to fully-qualify their names with the outer constant names.

Example

module SomeOrigin
  module SomeInnerModule
    class SomeNestedClass
    end
  end
end

module SomeDestination
  include Constant::Import

  import SomeOrigin
  import SomeOrigin::SomeInnerModule, alias: :Something
end

SomeDestination.const_defined?(SomeOrigin)
# => true

SomeDestination.const_defined?(Something)
# => true

SomeDestination::Something
# => SomeOrigin::SomeInnerModule

SomeDestination::Something::SomeNestedClass
# => SomeOrigin::SomeInnerModule::SomeNestedClass

SomeDestination.const_defined?(SomeInnerModule)
# => false

Because classes are also constants, the import macro can be used with a class as well, letting the class be accessed without fully-qualifying its namespace, or under an aliased name using the alias argument.

module SomeOrigin
  class SomeInnerClass
  end
end

module SomeDestination
  include Constant::Import

  import SomeOrigin::SomeInnerClass, alias: :SomeClass
end

SomeDestination.const_defined?(SomeClass)
# => true

SomeDestination::SomeClass.new
# => #<SomeDestination::SomeClass:0x...>

Importing a Constant

Macro

self.import(origin_constant, only: nil, except: nil, override_ancestor: nil, alias: nil)
include Constant::Import

import SomeOrigin::SomeInnerModule

The import macro is activated by including the Constant::Import module.

The nested constants in the origin constant will be accessible to the destination constant without the destination constant having to use the origin constant's namespace.

If an optional alias is used, the imported constants will be accessed via the alias constant name. The alias name replaces the origin constant name.

import SomeOrigin::SomeInnerModule, alias: :SomeModule

An import names an origin constant rather than a list of names, so the constants it assigns are whatever the origin happens to own. The except and only keywords narrow that set. except omits the names it is given; only declares the imported surface outright, so a constant added to the origin later cannot silently appear at the destination.

import SomeOrigin, except: :SomeInnerModule

import SomeOrigin, only: [:SomeInnerModule, :SomeOtherInnerModule]

Either takes a single name or a list, as a String or a Symbol. The two are not interchangeable in what they tolerate: a name given to except that the origin does not own is simply not there to omit, while a name given to only that the origin does not own raises — a caller who names a constant and does not receive it has been told nothing.

Passing both is accepted, and except does nothing when only is given. For except to remove a name, that name has to be one only kept — which puts it in both lists, and a name in both raises. With an origin owning A, B, and C:

Call Result
only: [:A, :B], except: [:C] imports A and B — identical to only: [:A, :B] alone
only: [:A], except: [:B] imports A — identical to only: [:A] alone
only: [:A, :B], except: [:B] raises
only: [:A], except: [:A] raises

So a call passing both either changes nothing or fails. Use one or the other.

A name already defined on the destination raises rather than being replaced, and so does a name the destination reaches through one of its ancestors. override_ancestor permits the inherited case:

import SomeOrigin, override_ancestor: true

Returns

The list of constants nested in the origin constant that have been made available to the destination constant's namespace.

Parameters

Name Description Type
origin_constant The constant whose inner constants will be made accessible without having to specify the origin constant's name Module or Class
only The names to import, declaring the imported surface rather than inferring it from the origin constant String, Symbol, or Array
except The names to omit from the import String, Symbol, or Array
override_ancestor Whether the import may override a constant the destination constant inherits from an ancestor; defaults to false Boolean
alias Optional constant name to use in the destination constant's namespace to access the origin constant's inner constants Symbol

Failures

An import assigns names the caller did not enumerate, onto a destination they did not inspect, so it refuses rather than replacing anything. Every failure raises Constant::Error.

Condition Message
A name is already defined on the destination constant SomeInnerModule is already defined on SomeDestination (imported from SomeOrigin)
A name is reached through one of the destination constant's ancestors, and override_ancestor is not given SomeInnerModule is inherited by SomeDestination from SomeAncestor (imported from SomeOrigin)
A name given to only is not defined on the origin constant SomeInnerModule is not defined on SomeOrigin
A name is given to both only and except SomeInnerModule is named in both only: and except:

The first two are the point of the refusal. Ruby replaces a constant and warns; it does not warn at all when an assignment shadows an inherited one, so for the second condition the refusal is the only signal there can be.

only and except differ in what they tolerate: a name given to except that the origin constant does not own is simply not there to omit, while the same name given to only raises — a caller who names a constant and does not receive it has been told nothing.

Top Level

At the top level of a script — outside any class or module body, where self is the main object — the macro is activated by the refinement rather than by inclusion:

using Constant::Import

import SomeOrigin

Including Constant::Import at the top level includes it into Object, which would install import on every class in the process. That is refused, with an error directing the use of the refinement.

The destination is Object, the main object's class, which is where Ruby puts constants defined at the top level. The imported constants are resolvable without qualification from anywhere in the process, including from inside unrelated classes. That reach is the ordinary consequence of defining a constant at the top level, and it is broader than any other destination's.

Instance Destinations

Activating the refinement with using Constant::Import makes import available on any object. An instance's import destination is its class:

using Constant::Import

class SomeDestination
end

some_destination = SomeDestination.new

some_destination.import(SomeOrigin)

SomeDestination.const_defined?(:SomeInnerModule)
# => true

The constants are set on the class rather than on the receiver, so every instance of the class accesses them, not only the instance that imported them.

Method Alias

The import macro is a convenience alias for __import_constant. The __import_constant method is the concrete implementation. This mechanism helps protect against a naming conflict with another library that implements a method name as common as "import".

API

self.call(origin_constant, destination_constant, only: nil, except: nil, override_ancestor: nil, alias: nil)
Constant::Import.(SomeOrigin::SomeInnerModule, self)

The nested constants in the origin constant will be accessible to the destination constant without the destination constant having to use the origin constant's namespace.

If an optional alias is used, the imported constants will be accessed via the alias constant name. The alias name replaces the origin constant name.

Constant::Import.(SomeOrigin::SomeInnerModule, self, alias: :SomeModule)

The except and only keywords narrow the set of constants the import assigns — except omitting the names it is given, only declaring the imported surface outright:

Constant::Import.(SomeOrigin, self, except: :SomeInnerModule)

Constant::Import.(SomeOrigin, self, only: [:SomeInnerModule, :SomeOtherInnerModule])

Either takes a single name or a list, as a String or a Symbol. A name given to only that the origin constant does not own raises; a name given to except that it does not own is simply not there to omit.

Passing both is accepted, and except does nothing when only is given. For except to remove a name, that name has to be one only kept — which puts it in both lists, and a name in both raises. With an origin owning A, B, and C:

Call Result
only: [:A, :B], except: [:C] imports A and B — identical to only: [:A, :B] alone
only: [:A], except: [:B] imports A — identical to only: [:A] alone
only: [:A, :B], except: [:B] raises
only: [:A], except: [:A] raises

So a call passing both either changes nothing or fails. Use one or the other.

A name already defined on the destination constant raises rather than being replaced. So does a name the destination constant reaches through one of its ancestors — Ruby emits no warning when an assignment shadows an inherited constant, so nothing else would report it. override_ancestor permits that case:

Constant::Import.(SomeOrigin, self, override_ancestor: true)

Returns

The list of constants nested in the origin constant that have been made available to the destination constant's namespace.

Parameters

Name Description Type
origin_constant The constant whose inner constants will be made accessible without having to specify the origin constant's name Module or Class
destination_constant The constant whose namespace will be able to access the imported origin constant's namespace without fully qualifying it. An object that is not a module or class imports into its class Module, Class, or Object
only The names to import, declaring the imported surface rather than inferring it from the origin constant String, Symbol, or Array
except The names to omit from the import String, Symbol, or Array
override_ancestor Whether the import may override a constant the destination constant inherits from an ancestor; defaults to false Boolean
alias Optional constant name to use in the destination constant's namespace to access the origin constant's inner constants Symbol

Failures

An import assigns names the caller did not enumerate, onto a destination they did not inspect, so it refuses rather than replacing anything. Every failure raises Constant::Error.

Condition Message
A name is already defined on the destination constant SomeInnerModule is already defined on SomeDestination (imported from SomeOrigin)
A name is reached through one of the destination constant's ancestors, and override_ancestor is not given SomeInnerModule is inherited by SomeDestination from SomeAncestor (imported from SomeOrigin)
A name given to only is not defined on the origin constant SomeInnerModule is not defined on SomeOrigin
A name is given to both only and except SomeInnerModule is named in both only: and except:

The refusal reaches the destination the import actually writes to. With alias, that is the alias constant rather than the destination itself, so a name already on the destination does not collide with one imported under an alias.

Including Constant::Import at the top level raises as well, before any import is attempted — see Top Level, above.

The Constant Class

A Constant mediates a resolved Ruby constant and answers questions about it — its name, its namespace, its value, whether a name is defined within it, and what inner constants it contains — so that callers work through it rather than reaching into Ruby's low-level constant methods (const_get, const_set, const_defined?, constants).

Constant is a mixin module included by two subtypes:

  • Constant::Module mediates a module or class.
  • Constant::Literal mediates a constant bound to a non-module ("literal") value.

Both answer the same interface; a Constant::Literal has no inner constants, so the container-style queries come back empty.

module SomeNamespace
  module SomeModule
    SomeInnerModule = Module.new
    SomeInnerLiteral = "some value"
  end
end

Getting a Constant

Constant.get is the class-level accessor. Hand it a module (or class) and it returns the Constant::Module that mediates it; hand it a name and a namespace and it resolves the name, returning whichever subtype the resolved value calls for:

Constant.get(:SomeInnerModule, SomeNamespace::SomeModule)
# => #<Constant::Module value=SomeNamespace::SomeModule::SomeInnerModule>

Constant.get(:SomeInnerLiteral, SomeNamespace::SomeModule)
# => #<Constant::Literal SomeNamespace::SomeModule::SomeInnerLiteral = "some value">

Constant.get("SomeNamespace::SomeModule")
# => #<Constant::Module value=SomeNamespace::SomeModule>

It is the class-level form of the instance #get primitive — the namespace, implicit as self on an instance, is passed as an argument. The namespace defaults to the top level and may itself be given as a name; an inherit: keyword (default false) governs whether resolution follows the ancestor chain.

A name may be a ::-qualified path, resolved segment by segment:

Constant.get("SomeModule::SomeInnerModule", SomeNamespace)
# => the Constant for SomeNamespace::SomeModule::SomeInnerModule

The instance #get does the path resolution by recursing on itself, so each segment resolves against its true parent — a terminal literal is bound with its real enclosing namespace, not the whole path. Traversing into a literal (a non-final segment that resolves to a literal) raises Constant::Error, since a literal has no inner constants.

Returns

The Constant that mediates the value — a Constant::Module for a module or class, or the subtype the resolved name calls for (Constant::Module or Constant::Literal).

Parameters

Name Description Type
value A module or class to mediate, or a constant name (optionally a ::-qualified path) to resolve Module, Class, String, or Symbol
namespace The namespace a name is resolved in; defaults to the top level (Object) Module, Class, String, Symbol, or Constant
inherit Whether resolution follows the ancestor chain; defaults to false Boolean

Failures

Raises Constant::Error when the name is not defined in the namespace, and when a ::-path traverses into a literal — a non-final segment that resolves to a literal constant, which has no inner constants to continue through.

Direct construction from a value you already hold goes to the subtype constructors — Constant::Module.build / Constant::Literal.build — which normalize their inputs and delegate to new, the strict initializer.

Coercion

Constant() is the coercion form — Ruby's Integer() / Array() idiom — an idempotent front door over get. It is a refinement, activated per file with using Constant::Coerce, so it never touches global scope unless a file opts in:

using Constant::Coerce

Constant(SomeNamespace::SomeModule)
# => #<Constant::Module value=SomeNamespace::SomeModule>

Constant("SomeInnerModule", SomeNamespace::SomeModule)
# => resolves the name in the namespace

Constant(nil)
# => TypeError: can't convert nil into Constant

It delegates the real work to Constant.get (taking the same namespace/inherit:) and adds only its own three concerns as a coercion: an already-Constant value is returned unchanged — a coercion is idempotent — and a value that is neither a module, a name, nor a Constant raises TypeError, mirroring Integer(nil). An un-resolvable name still raises Constant::Error, exactly as get does.

Returns

The Constant for the value — an already-Constant value unchanged, otherwise whatever Constant.get returns (Constant::Module or Constant::Literal).

Parameters

Name Description Type
value A Constant (returned unchanged), a module or class, or a constant name to resolve Constant, Module, Class, String, or Symbol
namespace The namespace a name is resolved in, passed through to Constant.get; defaults to the top level (Object) Module, Class, String, Symbol, or Constant
inherit Whether resolution follows the ancestor chain, passed through to Constant.get; defaults to false Boolean

Failures

Raises TypeError for a value that is neither a module, a name, nor a Constant — the coercion's own failure, mirroring Integer(nil).

Raises Constant::Error for a name that cannot be resolved, which is Constant.get's failure passed through unchanged.

Queries

The examples below use a constant obtained from Constant.get:

constant = Constant.get(SomeNamespace::SomeModule)

#value

constant.value
# => SomeNamespace::SomeModule (the mediated Ruby value)

Returns

The Ruby value the Constant mediates — the module or class for a Constant::Module, or the bound value for a Constant::Literal.

#name

constant.name
# => "SomeModule" (the final segment, a String)

Returns

The final segment of the mediated constant's name, as a String; nil for an anonymous module.

#full_name

constant.full_name
# => "SomeNamespace::SomeModule" (the ::-qualified name, a String)

Returns

The full ::-qualified name of the mediated constant, as a String; nil for an anonymous module.

#namespace

constant.namespace
# => the containing Constant (Constant.get(SomeNamespace))

Returns

The Constant::Module mediating the containing namespace — the Constant::Module for Object when the constant is top-level, and nil for an anonymous module.

#get

#get resolves an inner constant to the Constant that mediates it:

constant.get(:SomeInnerModule)
# => #<Constant::Module value=SomeNamespace::SomeModule::SomeInnerModule>

constant.get(:SomeInnerLiteral)
# => #<Constant::Literal SomeNamespace::SomeModule::SomeInnerLiteral = "some value">

Returns

The Constant mediating the resolved inner constant (Constant::Module or Constant::Literal).

Parameters

Name Description Type
name The inner constant name to resolve, optionally a ::-qualified path String or Symbol
inherit Whether resolution follows the ancestor chain; defaults to false Boolean

Failures

Raises Constant::Error when the name is not defined in the mediated module, and when a ::-path traverses into a literal — a non-final segment that resolves to a literal constant, which has no inner constants to continue through.

#constants and #constant_names

#constants and #constant_names list a module's inner constants — as Constant objects and as name Strings, respectively. By default they include only the module-valued inners; include_literal_constants: true also includes the literal-valued ones.

constant.constants
# => [#<Constant::Module value=SomeNamespace::SomeModule::SomeInnerModule>]

constant.constant_names
# => ["SomeInnerModule"]

constant.constant_names(include_literal_constants: true)
# => ["SomeInnerModule", "SomeInnerLiteral"]

Returns

#constants — the Constant objects mediating the selected inner constants. #constant_names — their names, as Strings. The default selection is the module-valued inner constants; literal-valued ones are included when include_literal_constants is true.

Parameters

Name Description Type
include_literal_constants Whether to include inner constants bound to non-module (literal) values; defaults to false Boolean
inherit Whether to include inherited inner constants; defaults to false Boolean

#defined?

#defined? reports whether a name or module is defined within the mediated module (a Constant::Literal always answers false). It never raises.

constant.defined?(:SomeInnerModule)
# => true

Returns

true if the name or module is defined within the mediated module, otherwise false; a Constant::Literal always returns false. Never raises.

Parameters

Name Description Type
name_or_module A constant name to test for definition, or a module to test for containment (by identity) within the mediated module String, Symbol, or Module
inherit Whether the search follows the ancestor chain; defaults to false Boolean

Constant.defined?

The class-level Constant.defined? is a name-existence predicate that never raises. The name may be a ::-path; a path that runs through a literal is simply not defined (false), never an error.

Constant.defined?("SomeNamespace::SomeModule")
# => true

Returns

true if the name is defined in the namespace, otherwise false. A ::-path that runs through a literal is false, not an error. Never raises.

Parameters

Name Description Type
name The constant name to test, optionally a ::-qualified path String or Symbol
namespace The namespace to test in; defaults to the top level (Object) Module, Class, String, Symbol, or Constant
inherit Whether the search follows the ancestor chain; defaults to false Boolean

Equality

Two Constants are equal when they identify the same constant: a Constant::Module by the module it mediates, a Constant::Literal by its binding location (namespace and name).

Constant.get(SomeNamespace::SomeModule) == Constant.get(SomeNamespace::SomeModule)
# => true

Returns

#== and #eql? return true when both Constants identify the same constant, otherwise false; a non-Constant operand compares false without raising. #hash returns an Integer consistent with equality, so equal Constants dedupe in a Set and interchange as Hash keys.

Parameters

Name Description Type
other The object to compare against Object

Defining a Constant

Constant::Define assigns a value to a constant name within a destination constant's namespace, returning the newly-defined constant. Given no value, it creates a new module — a fresh namespace — which is how Constant::Import uses it to create an alias target; given a value, it assigns that value (for example, a literal). It can be used directly to define a constant.

Example

module SomeDestination
end

some_constant = Constant::Define.(:SomeConstant, SomeDestination)

SomeDestination.const_defined?(:SomeConstant)
# => true

SomeDestination::SomeConstant.equal?(some_constant)
# => true

Giving a value assigns that value rather than creating a module:

some_literal = Constant::Define.(:SomeLiteral, SomeDestination, "some value")
# => "some value"

SomeDestination::SomeLiteral
# => "some value"

API

self.call(constant_name, destination_constant, constant_value=nil)
Constant::Define.("SomeConstant", SomeDestination)
Constant::Define.("SomeLiteral", SomeDestination, "some value")

constant_value is assigned to constant_name in the destination constant's namespace. When it is omitted (or nil), a new module is created and assigned.

Returns

The newly-defined constant — the assigned constant_value, or the new module created when no value is given.

Parameters

Name Description Type
constant_name The name the new constant is assigned to in the destination constant's namespace String or Symbol
destination_constant The constant whose namespace the new constant is defined in Module or Class
constant_value The value assigned to the constant name; when omitted, a new module is created and assigned Object

License

The Constant library is released under the MIT License.

About

Utilities for working with constants. Avoid the unintended effects of including constants by aliasing their inner constants.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages