lib/rom/repository.rb in rom-repository-1.0.0.beta3 vs lib/rom/repository.rb in rom-repository-1.0.0.rc1

- old
+ new

@@ -10,27 +10,19 @@ require 'rom/repository/session' 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 + # A repository provides access to composable relations, commands and changesets. + # 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 + # Typically, you're going to work with Repository::Root that is configured to + # use a single relation as its root, and compose aggregates and use changesets and 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 @@ -38,19 +30,40 @@ # conf.default.create_table(:tasks) do # primary_key :id # column :user_id, Integer # column :title, String # end + # + # conf.relation(:users) do + # associations do + # has_many :tasks + # end + # end # end # - # my_repo = MyRepo.new(rom) - # my_repo.users_with_tasks + # class UserRepo < ROM::Repository[:users] + # relations :tasks # + # def users_with_tasks + # aggregate(:tasks).to_a + # end + # end + # + # user_repo = UserRepo.new(rom) + # user_repo.users_with_tasks + # # @see Repository::Root # # @api public class Repository + # Mapping for supported changeset classes used in #changeset(type => relation) method + CHANGESET_TYPES = { + create: Changeset::Create, + update: Changeset::Update, + delete: Changeset::Delete + }.freeze + extend ClassInterface # @!attribute [r] container # @return [ROM::Container] The container used to set up a repo attr_reader :container @@ -61,11 +74,12 @@ # @!attribute [r] mappers # @return [MapperBuilder] The auto-generated mappers for repo relations attr_reader :mappers - # @api private + # @!attribute [r] commmand_compiler + # @return [Method] Function for compiling commands bound to a repo instance attr_reader :command_compiler # Initializes a new repo by establishing configured relation proxies from # the passed container # @@ -89,10 +103,12 @@ end end @command_compiler = method(:command) end + # Return a command for a relation + # # @overload command(type, relation) # Returns a command for a relation # # @example # repo.command(:create, repo.users) @@ -136,10 +152,12 @@ else container.command(*args, &block) end end + # Return a changeset for a relation + # # @overload changeset(name, attributes) # Return a create changeset for a given relation identifier # # @example # repo.changeset(:users, name: "Jane") @@ -168,63 +186,105 @@ # # @param [Class] changeset_class Custom changeset class # # @return [Changeset] # + # @overload changeset(opts) + # Return a changeset object using provided changeset type and relation + # + # @example + # repo.changeset(delete: repo.users.where { id > 10 }) + # + # @param [Hash<Symbol=>Relation] opts Command type => Relation config + # + # @return [Changeset] + # # @api public def changeset(*args) opts = { command_compiler: command_compiler } if args.size == 2 name, data = args elsif args.size == 3 name, pk, data = args elsif args.size == 1 - type = args[0] + if args[0].is_a?(Class) + klass = args[0] - if type.is_a?(Class) && type < Changeset - return type.new(relations[type.relation], opts) + if klass < Changeset + return klass.new(relations[klass.relation], opts) + else + raise ArgumentError, "+#{klass.name}+ is not a Changeset subclass" + end else type, relation = args[0].to_a[0] end else raise ArgumentError, 'Repository#changeset accepts 1-3 arguments' end if type - if type.equal?(:delete) - Changeset::Delete.new(relation, opts) - end + klass = CHANGESET_TYPES.fetch(type) { + raise ArgumentError, "+#{type.inspect}+ is not a valid changeset type. Must be one of: #{CHANGESET_TYPES.keys.inspect}" + } + + klass.new(relation, opts) else relation = relations[name] if pk - Changeset::Update.new(relation, opts.merge(__data__: data, primary_key: pk)) + Changeset::Update.new(relation.by_pk(pk), opts.update(__data__: data)) else - Changeset::Create.new(relation, opts.merge(__data__: data)) + Changeset::Create.new(relation, opts.update(__data__: data)) end end end - # TODO: document me, please + # Open a database transaction # - # @api public - def session(&block) - session = Session.new(self) - yield(session) - transaction { session.commit! } - end - - # TODO: document me, please + # @example commited transaction + # user = transaction do |t| + # create(changeset(name: 'Jane')) + # end # + # user + # # => #<ROM::Struct[User] id=1 name="Jane"> + # + # @example with a rollback + # user = transaction do |t| + # changeset(name: 'Jane').commit + # t.rollback! + # end + # + # user + # # nil + # # @api public def transaction(&block) container.gateways[:default].transaction(&block) end + # Return a string representation of a repository object + # + # @return [String] + # # @api public def inspect %(#<#{self.class} relations=[#{self.class.relations.map(&:inspect).join(' ')}]>) + end + + # Start a session for multiple changesets + # + # TODO: this is partly done, needs tweaks in changesets so that we can gather + # command results and return them in a nice way + # + # @!visibility private + # + # @api public + def session(&block) + session = Session.new(self) + yield(session) + transaction { session.commit! } end private # Local command cache