lib/rom/schema.rb in rom-2.0.2 vs lib/rom/schema.rb in rom-3.0.0.beta1
- old
+ new
@@ -1,89 +1,289 @@
require 'dry-equalizer'
-require 'rom/support/constants'
+require 'rom/schema/type'
require 'rom/schema/dsl'
require 'rom/association_set'
module ROM
# Relation schema
#
# @api public
class Schema
EMPTY_ASSOCIATION_SET = AssociationSet.new(EMPTY_HASH).freeze
+ DEFAULT_INFERRER = proc { [EMPTY_ARRAY, EMPTY_ARRAY].freeze }
+ MissingAttributesError = Class.new(StandardError) do
+ def initialize(name, attributes)
+ super("missing attributes in #{name.inspect} schema: #{attributes.map(&:inspect).join(', ')}")
+ end
+ end
+
include Dry::Equalizer(:name, :attributes, :associations)
include Enumerable
# @!attribute [r] name
# @return [Symbol] The name of this schema
attr_reader :name
# @!attribute [r] attributes
- # @return [Hash] The hash with schema attribute types
+ # @return [Array] Array with schema attributes
attr_reader :attributes
# @!attribute [r] associations
# @return [AssociationSet] Optional association set (this is adapter-specific)
attr_reader :associations
# @!attribute [r] inferrer
# @return [#call] An optional inferrer object used in `finalize!`
attr_reader :inferrer
- # @!attribute [r] primary_key
- # @return [Array<Dry::Types::Definition] Primary key array
- attr_reader :primary_key
+ # @api private
+ attr_reader :options
- alias_method :to_h, :attributes
+ # @api private
+ attr_reader :relations
+ alias_method :to_ary, :attributes
+
+ # @api public
+ def self.define(name, type_class: Type, attributes: EMPTY_ARRAY, associations: EMPTY_ASSOCIATION_SET, inferrer: DEFAULT_INFERRER)
+ new(
+ name,
+ attributes: attributes(attributes, type_class),
+ associations: associations,
+ inferrer: inferrer,
+ type_class: type_class
+ )
+ end
+
# @api private
- def initialize(name, attributes, inferrer: nil, associations: EMPTY_ASSOCIATION_SET)
+ def self.attributes(attributes, type_class)
+ attributes.map { |type| type_class.new(type) }
+ end
+
+ # @api private
+ def initialize(name, options)
@name = name
- @attributes = attributes
- @associations = associations
- @inferrer = inferrer
+ @options = options
+ @attributes = options[:attributes] || EMPTY_ARRAY
+ @associations = options[:associations]
+ @inferrer = options[:inferrer] || DEFAULT_INFERRER
+ @relations = options[:relations] || EMPTY_HASH
end
+ # Abstract method for creating a new relation based on schema definition
+ #
+ # This can be used by views to generate a new relation automatically.
+ # In example a schema can project a relation, join any additional relations
+ # if it uncludes attributes from other relations etc.
+ #
+ # Default implementation is a no-op and it simply returns back untouched relation
+ #
+ # @param [Relation]
+ #
+ # @return [Relation]
+ #
+ # @api public
+ def call(relation)
+ relation
+ end
+
# Iterate over schema's attributes
#
- # @yield [Dry::Data::Type]
+ # @yield [Schema::Type]
#
# @api public
def each(&block)
- attributes.each_value(&block)
+ attributes.each(&block)
end
+ # @api public
+ def empty?
+ attributes.size == 0
+ end
+
+ # @api public
+ def to_h
+ each_with_object({}) { |attr, h| h[attr.name] = attr }
+ end
+
# Return attribute
#
# @api public
- def [](name)
- attributes.fetch(name)
+ def [](key, src = name.to_sym)
+ attr =
+ if count_index[key].equal?(1)
+ name_index[key]
+ else
+ source_index[src][key]
+ end
+
+ unless attr
+ raise(KeyError, "#{key.inspect} attribute doesn't exist in #{src} schema")
+ end
+
+ attr
end
+ # Project a schema to include only specified attributes
+ #
+ # @param [*Array] names Attribute names
+ #
+ # @return [Schema]
+ #
+ # @api public
+ def project(*names)
+ new(names.map { |name| name.is_a?(Symbol) ? self[name] : name })
+ end
+
+ # Exclude provided attributes from a schema
+ #
+ # @param [*Array] names Attribute names
+ #
+ # @return [Schema]
+ #
+ # @api public
+ def exclude(*names)
+ project(*(map(&:name) - names))
+ end
+
+ # Project a schema with renamed attributes
+ #
+ # @param [Hash] mapping The attribute mappings
+ #
+ # @return [Schema]
+ #
+ # @api public
+ def rename(mapping)
+ new_attributes = map do |attr|
+ alias_name = mapping[attr.name]
+ alias_name ? attr.aliased(alias_name) : attr
+ end
+
+ new(new_attributes)
+ end
+
+ # Project a schema with renamed attributes using provided prefix
+ #
+ # @param [Symbol] prefix The name of the prefix
+ #
+ # @return [Schema]
+ #
+ # @api public
+ def prefix(prefix)
+ new(map { |attr| attr.prefixed(prefix) })
+ end
+
+ # @api public
+ def wrap(prefix = name.dataset)
+ new(map { |attr| attr.wrapped(prefix) })
+ end
+
# Return FK attribute for a given relation name
#
# @return [Dry::Types::Definition]
#
# @api public
def foreign_key(relation)
- detect { |attr| attr.meta[:foreign_key] && attr.meta[:relation] == relation }
+ detect { |attr| attr.foreign_key? && attr.target == relation }
end
+ # Return primary key attributes
+ #
+ # @return [Array<Schema::Type>]
+ #
+ # @api public
+ def primary_key
+ select(&:primary_key?)
+ end
+
+ # Merge with another schema
+ #
+ # @param [Schema] other Other schema
+ #
+ # @return [Schema]
+ #
+ # @api public
+ def merge(other)
+ new(attributes + other.attributes)
+ end
+ alias_method :+, :merge
+
+ # Return if a schema includes an attribute with the given name
+ #
+ # @param [Symbol] name The name of the attribute
+ #
+ # @return [Boolean]
+ #
+ # @api public
+ def key?(name)
+ ! attributes.detect { |attr| attr.name == name }.nil?
+ end
+
# This hook is called when relation is being build during container finalization
#
# When block is provided it'll be called just before freezing the instance
# so that additional ivars can be set
#
# @return [self]
#
# @api private
- def finalize!(gateway = nil, &block)
+ def finalize!(gateway: nil, relations: nil, &block)
return self if frozen?
- @attributes = inferrer.call(name.dataset, gateway) if inferrer
- @primary_key = select { |attr| attr.meta[:primary_key] == true }
+ inferred, missing = inferrer.call(name, gateway)
+
+ attr_names = map(&:name)
+ inferred_attrs = self.class.attributes(inferred, type_class).
+ reject { |attr| attr_names.include?(attr.name) }
+
+ attributes.concat(inferred_attrs)
+
+ missing_attributes = missing - map(&:name)
+
+ if missing_attributes.size > 0
+ raise MissingAttributesError.new(name, missing_attributes)
+ end
+
+ options[:relations] = @relations = relations
+
block.call if block
+
+ count_index
+ name_index
+ source_index
+
freeze
+ end
+
+ private
+
+ # @api private
+ def count_index
+ @count_index ||= map(&:name).map { |name| [name, count { |attr| attr.name == name }] }.to_h
+ end
+
+ # @api private
+ def name_index
+ @name_index ||= map { |attr| [attr.name, attr] }.to_h
+ end
+
+ # @api private
+ def source_index
+ @source_index ||= select(&:source).
+ group_by(&:source).
+ map { |src, grp| [src.to_sym, grp.map { |attr| [attr.name, attr] }.to_h] }.
+ to_h
+ end
+
+ # @api private
+ def type_class
+ options.fetch(:type_class)
+ end
+
+ # @api private
+ def new(attributes)
+ self.class.new(name, options.merge(attributes: attributes))
end
end
end