dphaener/kanji

View on GitHub
lib/kanji/type/class_interface.rb

Summary

Maintainability
A
2 hrs
Test Coverage
require "dry-validation"
require "graphql"
require "dry/core/class_builder"
require "dry/core/constants"
require "dry/core/inflector"
require "kanji/type/attribute"
require "kanji/graph/register_object"
require "kanji/graph/register_mutation"
require "kanji/type/attribute_definer"

module Kanji
  class Type
    module ClassInterface
      include Dry::Core::Constants

      attr_reader :_attributes, :_name, :_description, :_repo_name,
        :_associations

      def graphql_type(klass)
        Kanji::Graph::RegisterObject.new(
          attributes: klass._attributes + klass._associations,
          name: klass._name,
          description: klass._description
        ).call
      end

      def inherited(klass)
        super

        klass.instance_variable_set(:@_attributes, [])
        klass.instance_variable_set(:@_values, {})
        klass.instance_variable_set(:@_associations, [])

        klass.attribute(:id, Kanji::Types::Int, "The primary key")

        TracePoint.trace(:end) do |t|
          if klass == t.self
            self.finalize(klass)
            t.disable
          end
        end
      end

      def finalize(klass)
        klass.register :graphql_type, graphql_type(klass)
        klass.register :schema, -> { klass.register_schema }
        klass.register :value_object, -> { klass.create_value_object }
        klass.instance_variable_set(:@_repo_name, get_repo_name(klass))
      end

      def get_repo_name(klass)
        name = Dry::Core::Inflector.underscore(klass._name)
        Dry::Core::Inflector.pluralize(name).to_sym
      end

      def name(name)
        @_name = name
      end

      def description(description)
        @_description = description
      end

      def attribute(name, type = nil, description = nil, **kwargs, &block)
        if @_attributes.map(&:name).include?(name)
          fail AttributeError, "Attribute #{name} is already defined"
        else
          @_attributes <<
            AttributeDefiner.new(name, type, description, kwargs, &block).call
        end
      end

      def assoc(name, type = nil, description = nil, **kwargs, &block)
        if @_associations.map(&:name).include?(name)
          fail AttributeError, "Association #{name} is already defined"
        else
          @_associations <<
          AttributeDefiner.new(name, type, description, kwargs, &block).call
        end
      end

      def demodulized_type_name
        @_demodulized_type_name ||= Dry::Core::Inflector.demodulize(self.to_s)
      end

      def create(&block)
        register :create_mutation do
          Kanji::Graph::RegisterMutation.new(
            return_type: resolve(:graphql_type),
            attributes: @_attributes.reject { |attr| attr.name == :id },
            name: "Create#{demodulized_type_name}Mutation",
            description: "Create a new #{demodulized_type_name}.",
            resolve: block
          ).call
        end
      end

      def update(&block)
        register :update_mutation do
          Kanji::Graph::RegisterMutation.new(
            return_type: resolve(:graphql_type),
            attributes: @_attributes,
            name: "Update#{demodulized_type_name}Mutation",
            description: "Update an instance of #{demodulized_type_name}.",
            resolve: block
          ).call
        end
      end

      def destroy(&block)
        register :destroy_mutation do
          Kanji::Graph::RegisterMutation.new(
            return_type: resolve(:graphql_type),
            attributes: [@_attributes.find { |attr| attr.name == :id }],
            name: "Destroy#{demodulized_type_name}Mutation",
            description: "Destroy a #{demodulized_type_name}.",
            resolve: block
          ).call
        end
      end

      def register_schema
        attributes = _attributes

        Dry::Validation.JSON do
          configure { config.type_specs = true }

          attributes.each do |attribute|
            if attribute.options[:required]
              required(attribute.name, attribute.type).filled
            else
              optional(attribute.name, attribute.type).maybe
            end
          end
        end
      end

      def create_value_object
        builder = Dry::Core::ClassBuilder.new(
          name: "#{instance_variable_get(:@_name)}Value",
          parent: Dry::Struct::Value
        )
        klass = builder.call

        klass.constructor_type(:schema)

        instance_variable_get(:@_attributes).each do |attribute|
          klass.attribute(attribute.name, attribute.type)
        end

        klass
      end
    end
  end
end