ROM 5.1 released

We're happy to announce the release of the rom suite 5.1.0! This release includes a couple of bug fixes and mostly focuses on improvements in the ROM::Transformer API and adds plugin APIs to ROM::Repository and ROM::Changeset. Let's take a look at these nice additions!

Improved Transformer API

Currently ROM has two different APIs for defining custom mappers. One, called ROM::Mapper, is the old API that goes back to the very early days of the project. In version 4.0 a new API was introduced called ROM::Transformer, with the intention to eventually replace ROM::Mapper as Transformer API is more straight-forward and powerful than the original one.

In 5.1.0 we've made significant improvements that make using transformer-based mappers even better than before. Here's how it looks like in rom 5.1.0 with its new map method:

class JSONSerializer < ROM::Transformer
  map do
    nest(:address, %i[city street zipcode])
    deep_stringify_keys
  end
end

You can easily check out how it works by simply creating an instance and calling it with some data as input:

json_serializer = JSONSerializer.new

json_serializer.(
  [{ name: "Jane", city: "Cracow", street: "Street 1/2", zipcode: "12-345" }]
)
# => [{"name"=>"Jane",
#      "address"=>{"city"=>"Cracow", "street"=>"Street 1/2", "zipcode"=>"12-345"}
#    }]

Another big improvement is support for instance methods as mapping functions. Sometimes you need to access mapper's state in order to perform some mapping, this is where instance methods come in handy:

class JSONSerializer < ROM::Transformer
  map do
    # map zipcode using the corresponding instance method
    map_value(:zipcode, &:normalize_zipcode)
    nest(:address, %i[city street zipcode])
    deep_stringify_keys
  end

  def normalize_zipcode(zipcode)
    # do whatever you need
  end
end

Transformers are typically registered within a rom container, this part has been simplified too and you can now do it in one line, which is consistent with other rom components. If we wanted to register our JSONSerializer to be a mapper used by the users relation, we could simply do this now:

class JSONSerializer < ROM::Transformer
  relation :users, as: :json

  map do
    nest(:address, %i[city street zipcode])
    deep_stringify_keys
  end
end

Then the transformer will be available under :json identifier:

# assuming `users` is our relation
users.map_with(:json).to_a

Plugin API for Repository and Changeset

Starting from rom 5.1.0 you can write plugins for repositories and changesets. This is a huge improvement that you should find useful whether you're integrating rom with your library or just want to DRY-up code in an application. Currently there are no built-in plugins for repositories or changesets, but it's very likely we'll be adding some in the near future.

Here's an example of a repository plugin that sets "default scope" for a repository root relation:

module DefaultScope
  class ScopedRelation < Module
    attr_reader :name, :view

    def initialize(name, view)
      @name = name
      @view = view
      define_relation_reader
    end

    private

    def define_relation_reader
      relation_view = view

      define_method(name) do
        super().public_send(relation_view)
      end
    end
  end

  def self.apply(repo, view:)
    repo.prepend(ScopedRelation.new(repo.root, view))
  end
end

ROM.plugins do
  register :default_scope, DefaultScope, type: :repository
end

Now we can enable our plugin in a root repository:

class PostRepo < ROM::Repository[:posts]
  use :default_scope, view: :published
end

That's it - the repository will always use posts scoped to its published view.

Release information

This is a backward-compatible release, which means upgrading should not break your code. If you have any issues please report them. Please refer to 5.1.0 CHANGELOG for more information.