dry-rb/dry-types

View on GitHub
lib/dry/types/module.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# frozen_string_literal: true

require 'dry/core/deprecations'
require 'dry/types/builder_methods'

module Dry
  module Types
    # Export types registered in a container as module constants.
    # @example
    #   module Types
    #     include Dry::Types(:strict, :coercible, :nominal, default: :strict)
    #   end
    #
    #   Types.constants
    #   # => [:Class, :Strict, :Symbol, :Integer, :Float, :String, :Array, :Hash,
    #   #     :Decimal, :Nil, :True, :False, :Bool, :Date, :Nominal, :DateTime, :Range,
    #   #     :Coercible, :Time]
    #
    # @api public
    class Module < ::Module
      def initialize(registry, *args, **kwargs)
        @registry = registry
        check_parameters(*args, **kwargs)
        constants = type_constants(*args, **kwargs)
        define_constants(constants)
        extend(BuilderMethods)

        if constants.key?(:Nominal)
          singleton_class.send(:define_method, :included) do |base|
            super(base)
            base.instance_exec(const_get(:Nominal, false)) do |nominal|
              extend Dry::Core::Deprecations[:'dry-types']
              const_set(:Definition, nominal)
              deprecate_constant(:Definition, message: 'Nominal')
            end
          end
        end
      end

      # @api private
      def type_constants(*namespaces, default: Undefined, **aliases)
        if namespaces.empty? && aliases.empty? && Undefined.equal?(default)
          default_ns = :Strict
        elsif Undefined.equal?(default)
          default_ns = Undefined
        else
          default_ns = Inflector.camelize(default).to_sym
        end

        tree = registry_tree

        if namespaces.empty? && aliases.empty?
          modules = tree.select { |_, v| v.is_a?(::Hash) }.map(&:first)
        else
          modules = (namespaces + aliases.keys).map { |n| Inflector.camelize(n).to_sym }
        end

        tree.each_with_object({}) do |(key, value), constants|
          if modules.include?(key)
            name = aliases.fetch(Inflector.underscore(key).to_sym, key)
            constants[name] = value
          end

          constants.update(value) if key == default_ns
        end
      end

      # @api private
      def registry_tree
        @registry_tree ||= @registry.keys.each_with_object({}) { |key, tree|
          type = @registry[key]
          *modules, const_name = key.split('.').map { |part|
            Inflector.camelize(part).to_sym
          }
          next if modules.empty?

          modules.reduce(tree) { |br, name| br[name] ||= {} }[const_name] = type
        }.freeze
      end

      private

      # @api private
      def check_parameters(*namespaces, default: Undefined, **aliases)
        referenced = namespaces.dup
        referenced << default unless false.equal?(default) || Undefined.equal?(default)
        referenced.concat(aliases.keys)

        known = @registry.keys.map { |k|
          ns, *path = k.split('.')
          ns.to_sym unless path.empty?
        }.compact.uniq

        (referenced.uniq - known).each do |name|
          raise ArgumentError,
                "#{name.inspect} is not a known type namespace. "\
                "Supported options are #{known.map(&:inspect).join(', ')}"
        end
      end

      # @api private
      def define_constants(constants, mod = self)
        constants.each do |name, value|
          case value
          when ::Hash
            if mod.const_defined?(name, false)
              define_constants(value, mod.const_get(name, false))
            else
              m = ::Module.new
              mod.const_set(name, m)
              define_constants(value, m)
            end
          else
            mod.const_set(name, value)
          end
        end
      end
    end
  end
end