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