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:
attr_reader :datasets
@datasets = Hash.new { h[k] = [] }
end
datasets[name]
end
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:
# 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[:name] == 'Jane' }.inspect
# #<ROM::ArrayAdapter::Relation dataset=[{:name=>"Jane"}]>
Warning
Please remember about setting adapter identifier - 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)
[:array]
select { 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:
adapter :array
# reading
forward :select, :reject
# writing
forward :<<, :delete
dataset.size
end
end
end
end
Commands::Create
To implement a create command:
# require what you require!
# Just like in case of Relation, we must configure adapter identifier
adapter :array
tuples.each { 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:
adapter :array
relation.each { 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:
adapter :array
relation.each { 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)
[:array]
select { user[:name] == name }
end
end
[:array]
relation :users
register_as :create
end
[:array]
relation :users
result :one
register_as :update
end
[: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"}]