# frozen_string_literal: true require 'dry/equalizer' require 'rom/types' require 'rom/attribute' require 'rom/schema/associations_dsl' module ROM class Schema # Schema DSL exposed as `schema { .. }` in relation classes # # @api public class DSL < BasicObject KERNEL_METHODS = %i(extend method).freeze KERNEL_METHODS.each { |m| define_method(m, ::Kernel.instance_method(m)) } extend Initializer # @!attribute [r] relation # @return [Relation::Name] The name of the schema's relation param :relation # @!attribute [r] inferrer # @return [Inferrer] Optional attribute inferrer option :inferrer, default: -> { DEFAULT_INFERRER } # @!attribute [r] schema_class # @return [Class] Schema class that should be instantiated option :schema_class, default: -> { Schema } # @!attribute [r] attr_class # @return [Class] Attribute class that should be used option :attr_class, default: -> { Attribute } # @!attribute [r] adapter # @return [Symbol] The adapter identifier used in gateways option :adapter, default: -> { :default } # @!attribute [r] attributes # @return [Hash] A hash with attribute names as # keys and attribute representations as values. # # @see [Schema.build_attribute_info] attr_reader :attributes # @!attribute [r] plugins # @return [Hash] A hash with schema plugins enabled in a schema attr_reader :plugins # @!attribute [r] definition # @return [Proc] Definition block passed to DSL attr_reader :definition # @!attribute [r] associations_dsl # @return [AssociationDSL] Associations defined within a block attr_reader :associations_dsl # @api private def initialize(*, &block) super @attributes = {} @plugins = {} @definition = block end # Defines a relation attribute with its type and options. # # When only options are given, type is left as nil. It makes # sense when it is used alongside an schema inferrer, which will # populate the type. # # @see Relation.schema # # @api public def attribute(name, type_or_options, options = EMPTY_HASH) if attributes.key?(name) ::Kernel.raise ::ROM::AttributeAlreadyDefinedError, "Attribute #{ name.inspect } already defined" end attributes[name] = build_attribute_info(name, type_or_options, options) end # Define associations for a relation # # @example # class Users < ROM::Relation[:sql] # schema(infer: true) do # associations do # has_many :tasks # has_many :posts # has_many :posts, as: :priority_posts, view: :prioritized # belongs_to :account # end # end # end # # class Posts < ROM::Relation[:sql] # schema(infer: true) do # associations do # belongs_to :users, as: :author # end # end # # view(:prioritized) do # where { priority <= 3 } # end # end # # @return [AssociationDSL] # # @api public def associations(&block) @associations_dsl = AssociationsDSL.new(relation, &block) end # Builds a representation of the information needed to create an # attribute. It returns a hash with `:type` and `:options` keys. # # @return [Hash] # # @see [Schema.build_attribute_info] # # @api private def build_attribute_info(name, type_or_options, options = EMPTY_HASH) type, options = if type_or_options.is_a?(::Hash) [nil, type_or_options] else [build_type(type_or_options, options), options] end Schema.build_attribute_info( type, options.merge(name: name) ) end # Builds a type instance from base type and meta options # # @return [Dry::Types::Type] Type instance # # @api private def build_type(type, options = EMPTY_HASH) if options[:read] type.meta(source: relation, read: options[:read]) elsif type.optional? && type.meta[:read] type.meta(source: relation, read: type.meta[:read].optional) else type.meta(source: relation) end.meta(Attribute::META_OPTIONS.map { |opt| [opt, options[opt]] if options.key?(opt) }.compact.to_h) end # Specify which key(s) should be the primary key # # @api public def primary_key(*names) names.each do |name| attributes[name][:type] = attributes[name][:type].meta(primary_key: true) end self end # Enables for the schema # # @param [Symbol] plugin_name Plugin name # @param [Hash] options Plugin options # # @api public def use(plugin_name, options = ::ROM::EMPTY_HASH) plugin = ::ROM.plugin_registry[:schema].fetch(plugin_name, adapter) app_plugin(plugin, options) end # @api private def app_plugin(plugin, options = ::ROM::EMPTY_HASH) plugin.extend_dsl(self) @plugins[plugin.name] = [plugin, plugin.config.to_hash.merge(options)] end # @api private def call(&block) instance_exec(&block) if block instance_exec(&definition) if definition schema_class.define(relation, opts) do |schema| plugins.values.each do |plugin, options| plugin.apply_to(schema, options) end end end # @api private def plugin_options(plugin) @plugins[plugin][1] end private # Return schema opts # # @return [Hash] # # @api private def opts opts = { attributes: attributes.values, inferrer: inferrer, attr_class: attr_class } if associations_dsl { **opts, associations: associations_dsl.call } else opts end end end end end