lib/rom/repository.rb in rom-repository-0.2.0 vs lib/rom/repository.rb in rom-repository-0.3.0

- old
+ new

@@ -1,60 +1,221 @@ require 'rom/support/deprecations' require 'rom/support/options' +require 'rom/repository/class_interface' require 'rom/repository/mapper_builder' -require 'rom/repository/loading_proxy' +require 'rom/repository/relation_proxy' +require 'rom/repository/command_compiler' +require 'rom/repository/root' +require 'rom/repository/changeset' + module ROM + # Abstract repository class to inherit from + # + # A repository provides access to composable relations and commands. Its job is + # to provide application-specific data that is already materialized, so that + # relations don't leak into your application layer. + # + # Typically, you're going to work with Repository::Root that are configured to + # use a single relation as its root, and compose aggregates and use commands + # against the root relation. + # + # @example + # class MyRepo < ROM::Repository[:users] + # relations :users, :tasks + # + # def users_with_tasks + # users.combine_children(tasks: tasks).to_a + # end + # end + # + # rom = ROM.container(:sql, 'sqlite::memory') do |conf| + # conf.default.create_table(:users) do + # primary_key :id + # column :name, String + # end + # + # conf.default.create_table(:tasks) do + # primary_key :id + # column :user_id, Integer + # column :title, String + # end + # end + # + # my_repo = MyRepo.new(rom) + # my_repo.users_with_tasks + # + # @see Repository::Root + # + # @api public class Repository - # Abstract repository class to inherit from + # @deprecated + class Base < Repository + def self.inherited(klass) + super + Deprecations.announce(self, 'inherit from Repository instead') + end + end + + extend ClassInterface + + # @!attribute [r] container + # @return [ROM::Container] The container used to set up a repo + attr_reader :container + + # @!attribute [r] relations + # @return [RelationRegistry] The relation proxy registry used by a repo + attr_reader :relations + + # @!attribute [r] mappers + # @return [MapperBuilder] The auto-generated mappers for repo relations + attr_reader :mappers + + # Initializes a new repo by establishing configured relation proxies from + # the passed container # - # TODO: rename this to Repository once deprecated Repository from rom core is gone + # @param container [ROM::Container] The rom container with relations and optional commands # # @api public - include Options + def initialize(container) + @container = container + @mappers = MapperBuilder.new + @relations = RelationRegistry.new do |registry, relations| + self.class.relations.each do |name| + relation = container.relation(name) - option :mapper_builder, reader: true, default: proc { MapperBuilder.new } + proxy = RelationProxy.new( + relation, name: name, mappers: mappers, registry: registry + ) - # Define which relations your repository is going to use + instance_variable_set("@#{name}", proxy) + + relations[name] = proxy + end + end + end + + # @overload command(type, relation) + # Returns a command for a relation # - # @example - # class MyRepo < ROM::Repository::Base - # relations :users, :tasks - # end + # @example + # repo.command(:create, repo.users) # - # my_repo = MyRepo.new(rom_env) + # @param type [Symbol] The command type (:create, :update or :delete) + # @param relation [RelationProxy] The relation for which command should be built for # - # my_repo.users - # my_repo.tasks + # @overload command(options) + # Builds a command for a given relation identifier # - # @return [Array<Symbol>] + # @example + # repo.command(create: :users) # + # @param options [Hash<Symbol=>Symbol>] A type => rel_name map + # + # @overload command(rel_name) + # Returns command registry for a given relation identifier + # + # @example + # repo.command(:users)[:my_custom_command] + # + # @param rel_name [Symbol] The relation identifier from the container + # + # @return [CommandRegistry] + # + # @overload command(rel_name, &block) + # Yields a command graph composer for a given relation identifier + # + # @param rel_name [Symbol] The relation identifier from the container + # + # @return [ROM::Command] + # # @api public - def self.relations(*names) - if names.any? - attr_reader(*names) - @relations = names + def command(*args, **opts, &block) + all_args = args + opts.to_a.flatten + + if all_args.size > 1 + commands.fetch_or_store(all_args.hash) do + compile_command(*args, **opts) + end else - @relations + container.command(*args, &block) end end - # @api private - def initialize(env, options = {}) - super - self.class.relations.each do |name| - proxy = LoadingProxy.new( - env.relation(name), name: name, mapper_builder: mapper_builder - ) - instance_variable_set("@#{name}", proxy) + # @overload changeset(name, attributes) + # Returns a create changeset for a given relation identifier + # + # @example + # repo.changeset(:users, name: "Jane") + # + # @param name [Symbol] The relation container identifier + # @param attributes [Hash] + # + # @return [Changeset::Create] + # + # @overload changeset(name, restriction_arg, attributes) + # Returns an update changeset for a given relation identifier + # + # @example + # repo.changeset(:users, 1, name: "Jane Doe") + # + # @param name [Symbol] The relation container identifier + # @param restriction_arg [Object] The argument passed to restricted view + # + # @return [Changeset::Update] + # + # @api public + def changeset(*args) + if args.size == 2 + name, data = args + elsif args.size == 3 + name, pk, data = args + else + raise ArgumentError, 'Repository#changeset accepts 2 or 3 arguments' end + + relation = relations[name] + + if pk + Changeset::Update.new(relation, data, primary_key: pk) + else + Changeset::Create.new(relation, data) + end end - class Base < Repository - def self.inherited(klass) - super - Deprecations.announce(self, 'inherit from Repository instead') + private + + # Local command cache + # + # @api private + def commands + @__commands__ ||= Concurrent::Map.new + end + + # Build a new command or return existing one + # + # @api private + def compile_command(*args, mapper: nil, use: nil, **opts) + type, name = args + opts.to_a.flatten(1) + + relation = name.is_a?(Symbol) ? relations[name] : name + + ast = relation.to_ast + adapter = relations[relation.name].adapter + + if mapper + mapper_instance = container.mappers[relation.name.relation][mapper] + else + mapper_instance = mappers[ast] end + + command = CommandCompiler[container, type, adapter, ast, use] + command >> mapper_instance + end + + # @api private + def map_tuple(relation, tuple) + relations[relation.name].mapper.([tuple]).first end end end