lib/rom/relation.rb in rom-0.5.0 vs lib/rom/relation.rb in rom-0.6.0.beta1
- old
+ new
@@ -1,60 +1,199 @@
+require 'set'
+require 'rom/relation/registry_reader'
+require 'rom/relation/lazy'
+require 'rom/relation/curried'
+
module ROM
# Base relation class
#
- # Relation is a proxy for the dataset object provided by the adapter, it
- # forwards every method to the dataset that's why "native" interface of the
- # underlying adapter is available in the relation. This interface, however, is
- # considered private and should not be used outside of the relation instance.
+ # Relation is a proxy for the dataset object provided by the repository. It
+ # forwards every method to the dataset, which is why the "native" interface of
+ # the underlying repository is available in the relation. This interface,
+ # however, is considered private and should not be used outside of the
+ # relation instance.
#
# ROM builds sub-classes of this class for every relation defined in the env
- # for easy inspection and extensibility - every adapter can provide extensions
+ # for easy inspection and extensibility - every repository can provide extensions
# for those sub-classes but there is always a vanilla relation instance stored
# in the schema registry.
#
- # Relation instances also have access to the experimental ROM::RA interface
- # giving in-memory relational operations that are very handy, especially when
- # dealing with joined relations or data coming from different sources.
- #
# @api public
class Relation
- include Charlatan.new(:dataset)
- include Equalizer.new(:header, :dataset)
+ extend ClassMacros
- class << self
- # Relation methods that were defined inside setup.relation DSL
- #
- # @return [Array<Symbol>]
- #
- # @api private
- attr_accessor :relation_methods
+ include Options
+ include Equalizer.new(:dataset)
+
+ defines :repository, :dataset, :register_as, :exposed_relations
+
+ repository :default
+
+ attr_reader :name, :dataset, :exposed_relations
+
+ # Register adapter relation subclasses during setup phase
+ #
+ # In adition those subclasses are extended with an interface for accessing
+ # relation registry and to define `register_as` setting
+ #
+ # @api private
+ def self.inherited(klass)
+ super
+
+ return if self == ROM::Relation
+
+ klass.class_eval do
+ include ROM::Relation::RegistryReader
+
+ dataset(default_name)
+ exposed_relations Set.new
+
+ def self.register_as(value = Undefined)
+ if value == Undefined
+ @register_as || dataset
+ else
+ super
+ end
+ end
+
+ def self.method_added(name)
+ super
+ exposed_relations << name if public_instance_methods.include?(name)
+ end
+ end
+
+ ROM.register_relation(klass)
end
- # @return [Array] relation base header
+ # Return adapter-specific relation subclass
#
+ # @example
+ # ROM::Relation[:memory]
+ # # => ROM::Memory::Relation
+ #
+ # @return [Class]
+ #
+ # @api public
+ def self.[](type)
+ ROM.adapters.fetch(type).const_get(:Relation)
+ end
+
+ # Dynamically define a method that will forward to the dataset and wrap
+ # response in the relation itself
+ #
+ # @example
+ # class SomeAdapterRelation < ROM::Relation
+ # forward :super_query
+ # end
+ #
+ # @api public
+ def self.forward(*methods)
+ methods.each do |method|
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{method}(*args, &block)
+ __new__(dataset.__send__(:#{method}, *args, &block))
+ end
+ RUBY
+ end
+ end
+
+ # Return default relation name used for `register_as` setting
+ #
+ # @return [Symbol]
+ #
# @api private
- attr_reader :header
+ def self.default_name
+ return unless name
+ Inflector.underscore(name).gsub('/', '_').to_sym
+ end
- # Hook to finalize a relation after its instance was created
+ # Build relation registry of specified descendant classes
#
+ # This is used by the setup
+ #
+ # @param [Hash] repositories
+ # @param [Array] descendants a list of relation descendants
+ #
+ # @return [Hash]
+ #
# @api private
- def self.finalize(_env, _relation)
- # noop
+ def self.registry(repositories, descendants)
+ registry = {}
+
+ descendants.each do |klass|
+ # TODO: raise a meaningful error here and add spec covering the case
+ # where klass' repository points to non-existant repo
+ repository = repositories.fetch(klass.repository)
+ dataset = repository.dataset(klass.dataset)
+
+ relation = klass.new(dataset, __registry__: registry)
+
+ name = klass.register_as
+
+ if registry.key?(name)
+ raise RelationAlreadyDefinedError,
+ "Relation with `register_as #{name.inspect}` registered more " \
+ "than once"
+ end
+
+ registry[name] = relation
+ end
+
+ registry.each_value do |relation|
+ relation.class.finalize(registry, relation)
+ end
+
+ registry
end
# @api private
- def initialize(dataset, header = dataset.header)
+ def initialize(dataset, options = {})
+ @dataset = dataset
+ @name = self.class.dataset
+ @exposed_relations = self.class.exposed_relations
super
- @header = header.dup.freeze
end
+ # Hook to finalize a relation after its instance was created
+ #
+ # @api private
+ def self.finalize(_env, _relation)
+ # noop
+ end
+
# Yield dataset tuples
#
# @yield [Hash]
#
# @api private
def each(&block)
return to_enum unless block
- dataset.each(&block)
+ dataset.each { |tuple| yield(tuple) }
+ end
+
+ # Materialize relation into an array
+ #
+ # @return [Array<Hash>]
+ #
+ # @api public
+ def to_a
+ to_enum.to_a
+ end
+
+ # @api private
+ def repository
+ self.class.repository
+ end
+
+ # @api public
+ def to_lazy(*args)
+ Lazy.new(self, *args)
+ end
+
+ private
+
+ # @api private
+ def __new__(dataset, new_opts = {})
+ self.class.new(dataset, options.merge(new_opts))
end
end
end