How to Build a ROM Adapter » Adapters
ROM makes very little assumptions about its adapters that's why it is simple to build a custom adapter that will provide access to a specific datasource.
A ROM adapter must provide the following components:
ROM::Gatewaysubclass that implements required interfaceROM::Relationsubclass that exposes adapter-specific interface for queries and writing
In addition to that the adapter may also provide:
ROM::Commands::Createsubclass forcreateoperationROM::Commands::Updatesubclass forupdateoperationROM::Commands::Deletesubclass fordeleteoperation
Let's build an adapter for a plain Ruby array, because why not.
Gateway
Adapter's gateway is used by ROM to retrieve datasets and inject them into adapter's relations as their data-access backends. Here's a simple implementation:
require 'rom'
module ROM
  module ArrayAdapter
    class Gateway < ROM::Gateway
      attr_reader :datasets
      def initialize
        @datasets = Hash.new { |h, k| h[k] = [] }
      end
      def dataset(name)
        datasets[name]
      end
      def dataset?(name)
        datasets.key?(name)
      end
    end
  end
end
gateway = ROM::ArrayAdapter::Gateway.new
users = gateway.dataset(:users)
tasks = gateway.dataset(:tasks)
gateway.dataset?(:users) # true
gateway.dataset?(:tasks) # true
This allows ROM to ask for specific datasets from your gateway.
Relation
Adapter-specific relation must exist because it can provide various features that only make sense for a concrete adapter. It can automatically forward method calls to the underlaying dataset in order to expose "native" interface to the relation.
Since our datasets are just arrays, we can expose various array methods to the
relation using forward macro:
module ROM
  module ArrayAdapter
    class Relation < ROM::Relation
      # we must configure adapter identifier here
      adapter :array
      forward :select, :reject
    end
  end
end
users = gateway.dataset(:users)
users << { name: 'Jane' }
users << { name: 'John' }
relation = ROM::ArrayAdapter::Relation.new(gateway.dataset(:users))
relation.select { |tuple| tuple[:name] == 'Jane' }.inspect
# #<ROM::ArrayAdapter::Relation dataset=[{:name=>"Jane"}]>
Please remember about setting
adapteridentifier - it is used by ROM to infer component types specific to a given adapter. It's essential during the setup.
Registering Your Adapter
The adapter must register itself under specific identifier which then can be used to set up ROM components for that particular adapter.
To register your adapter:
ROM.register_adapter(:array, ROM::ArrayAdapter)
This is it! Now our array adapter can be setup using ROM:
configuration= ROM::Configuration.new(:array)
class Users < ROM::Relation[:array]
  def by_name(name)
    select { |user| user[:name] == name }
  end
end
configuration.register_relation(Users)
rom = ROM.container(configuration)
users = rom.gateways[:default].dataset(:users)
users << { name: 'Jane' }
users << { name: 'John' }
rom.relations[:users].by_name('Jane').to_a
# [{:name=>"Jane"}]
Commands
Adapter commands are optional because you don't always want to change data in a given datastore. If your datastore supports create/update/delete operations you can provide an interface for that using commands.
ROM adheres to the CQRS but it doesn't enforce it, this means that relations do implement CRUD and commands are just thin wrappers around CUD and they depend on relations.
By convention all command classes live under ROM::YourAdapter::Commands
namespace.
Common Command Behavior
Every ROM command has a couple of features available out-of-the-box:
relation- returns current relation for the current commandsource- original relation that was injected to the current command initially>>(other)- composes one command with anotherwith(input)- auto-curries a command with provided inputcombine(*others)- builds a command graph with other commands as nodesone?- returns true if a command returns a single tuplemany?- returns true if a command returns more than one tuple
Extending Relation for Commands
Commands will require an interface to insert, delete and update data and also
count.
Let's provide that:
module ROM
  module ArrayAdapter
    class Relation < ROM::Relation
      adapter :array
      # reading
      forward :select, :reject
      # writing
      forward :<<, :delete
      def count
        dataset.size
      end
    end
  end
end
Commands::Create
To implement a create command:
require 'rom/commands/create' # require what you require!
module ROM
  module ArrayAdapter
    module Commands
      class Create < ROM::Commands::Create
        # Just like in case of Relation, we must configure adapter identifier
        adapter :array
        def execute(tuples)
          tuples.each { |tuple| relation << tuple }
        end
      end
    end
  end
end
users = ROM::ArrayAdapter::Relation.new(gateway.dataset(:users))
create_users = ROM::ArrayAdapter::Commands::Create.new(users)
create_users.call([{ name: 'Jane' }])
puts users.to_a.inspect
# [{:name=>"Jane"}]
Commands::Delete
To implement a delete command:
require 'rom/commands/delete'
module ROM
  module ArrayAdapter
    module Commands
      class Delete < ROM::Commands::Delete
        adapter :array
        def execute
          relation.each { |tuple| source.delete(tuple) }
        end
      end
    end
  end
end
delete_users = ROM::ArrayAdapter::Commands::Delete.new(users)
delete_users.call
puts users.to_a.inspect
# []
Notice that here delete command yields tuples from its current relation but
deletes it from the source relation, since this is our canonical source of
data.
Commands::Update
To implement an update command:
require 'rom/commands/update'
module ROM
  module ArrayAdapter
    module Commands
      class Update < ROM::Commands::Update
        adapter :array
        def execute(attributes)
          relation.each { |tuple| tuple.update(attributes) }
        end
      end
    end
  end
end
update_users = ROM::ArrayAdapter::Commands::Update.new(users)
update_users.call(age: 21)
puts users.to_a.inspect
# [{:name=>"Jane", :age=>21}]
Here we simply rely on Hash#update which mutates tuples using the input
attributes.
Putting It All Together
Once your command classes are defined ROM will pick them up from your namespace and they will be available during setup:
configuration = ROM::Configuration.new(:array)
class Users < ROM::Relation[:array]
  def by_name(name)
    select { |user| user[:name] == name }
  end
end
class CreateUser < ROM::Commands::Create[:array]
  relation :users
  register_as :create
end
class UpdateUser < ROM::Commands::Update[:array]
  relation :users
  result :one
  register_as :update
end
class DeleteUser < ROM::Commands::Delete[:array]
  relation :users
  result :one
  register_as :delete
end
configuration.register_relation(Users)
configuration.register_command(CreateUser)
configuration.register_command(UpdateUser)
configuration.register_command(DeleteUser)
rom = ROM.create_container(configuration)
create_users = rom.commands[:users][:create]
update_user = rom.commands[:users][:update]
delete_user = rom.commands[:users][:delete]
create_users.call([{ name: 'Jane' }, { name: 'John' }])
puts rom.relations[:users].to_a.inspect
# [{:name=>"Jane"}, {:name=>"John"}]
puts rom.relations[:users].by_name('Jane').to_a.inspect
# [{:name=>"Jane"}]
update_user.by_name('Jane').call(name: 'Jane Doe')
puts rom.relations[:users].to_a.inspect
# [{:name=>"Jane Doe"}, {:name=>"John"}]
delete_user.by_name('John').call
puts rom.relations[:users].to_a.inspect
# [{:name=>"Jane Doe"}]