Announcing rom-factory

We're happy to announce a new project that we've been working on - rom-factory. The project was originally started by Jānis Miezītis back in 2016, and then it was moved to rom-rb organization in March 2017. As you can probably guess, rom-factory is a data generator library, similar to FactoryBot (previously known as FactoryGirl) or Fabrication. It's built on top of rom-rb and has a sweet integration with Faker gem.

Let's see how it looks like.

Factories

In rom-factory you can define as many factories as you want, we do not store them under one singleton object. After you got rom container set up, you can easily configure a factory:

MyFactory = ROM::Factory.configure do |c|
  c.rom = your_rom_container
end

AnotherFactory = ROM::Factory.configure do |c|
  c.rom = your_rom_container
end

After setting up a factory, you can start defining your builders:

MyFactory.define(:user) do |f|
  f.name "Jane"
  f.email "jane@doe.org"
end

You can ask for an in-memory struct, in which case a primary key value will be auto-generated:

MyFactory.structs[:user]
# #<ROM::Struct::User id=1 name="Jane" email="jane@doe.org"

...or you can ask for a struct which will be persisted in your database:

MyFactory[:user]
# #<ROM::Struct::User id=1 name="Jane" email="jane@doe.org"

Dynamic values with sequences, re-using other values and faker

Having static values is often not enough, which is why rom-factory has a couple of neat features which allow you to define dynamic values. The first one is sequencing:

MyFactory.define(:user) do |f|
  f.sequence(:email) { |n| "user-#{n}@rom-rb.org" }
end

You can also re-use values from other attributes:

MyFactory.define(:user) do |f|
  f.name "Jane"
  f.email { |name| "#{name}@rom-rb.org" }
end

We also added support for faker, which makes defining builders more concise:

MyFactory.define(:user) do |f|
  f.name { fake(:name) }
  f.email { fake(:internet, :email) }
  f.age { fake(:number, :between, 10, 100) }
end

Associations

Currently has_many, has_one and belongs_to are supported. Here are a couple examples:

MyFactory.define(:user) do |f|
  f.name { fake(:name) }
  f.email { fake(:internet, :email) }

  # this will use :group builder to create a group for a user
  f.association(:group)

  # this will create 2 posts for a user
  f.association(:posts, count: 2)
end

Extending existing builders

You can define a builder by extending another one, for example you may have a user and an admin, which sets admin attribute to true:

MyFactory.define(:user) do |f|
  f.name { fake(:name) }
  f.email { fake(:internet, :email) }
  f.age { fake(:number, :between, 10, 100) }
  f.admin false
end

MyFactory.define(admin: :user) do |f|
  f.admin true
end

Status & Roadmap

This is still in beta phase, current release is 0.5.0. We're planning to turn this into a pure data generator which doesn't assume any specific persistence backend (currently it uses and requires rom-core). On top of this, we want to add support for persistence backends. Once this is done, we'll have 1.0.0 ready.

For now, give it a try and tell us what you think. If you have any questions or problems, reach out on our discussion forum.

Useful links:


ROM 4.0.0 released

After over six months of work, we're pleased to announce the release of rom-rb 4.0.0! This is a major release which brings many improvements and new features. One of the biggest priorities of this release was to solidify and improve automatic mapping capabilities, by extending core API with features that were previously implemented only in rom-repository. This means that the most advanced features, such as automatic mappers or inferring struct objects, are now part of core API, and it makes using rom-rb much simpler. Apart from this, we also added Association API to the core, which enables associations for all adapters, and you can define associations between different databases.

Here are some of the highlights of 4.0.0 release.

Automatic mapping

Starting with 4.0.0, rom-rb can infer mappers on-the-fly for all relations. It can generate mappers for "flat" relations, as well as combined or wrapped relations. You don't have to use any structs (aka models) to use this feature, it works with plain hashes too.

Let's say we have users and tasks relations:

class Users < ROM::Relation[:sql]
  schema(infer: true) do
    associations do
      has_many :tasks
    end
  end
end

class Tasks < ROM::Relation[:sql]
  schema(infer: true) do
    associations do
      belongs_to :user
    end
  end
end

Now we can load data using these relations easily, including preloaded association data:

> users.first
=> {:id=>1, :name=>"Jane"}

> users.combine(:tasks).first
=> {:id=>1, :name=>"Jane", :tasks=>[{:id=>1, :user_id=>1, :title=>"Jane's task"}]}

> tasks.wrap(:user).first
=> {:id=>1, :user_id=>1, :title=>"Jane's task", :user=>{:id=>1, :name=>"Jane"}}

Auto-struct mapping

Relations can now automatically infer struct objects based on their schema information. This feature can be enabled via auto_struct setting and it is enabled by default, when you use relations through repositories. Let's tweak previous example to use this feature:

class Users < ROM::Relation[:sql]
  schema(infer: true) do
    associations do
      has_many :tasks
    end
  end

  auto_struct true
end

class Tasks < ROM::Relation[:sql]
  schema(infer: true) do
    associations do
      belongs_to :user
    end
  end

  auto_struct true
end

Now we will get convenient struct objects back:

> users.first
=> #<ROM::Struct::User id=1 name="Jane">

> users.combine(:tasks).first
=> #<ROM::Struct::User id=1 name="Jane" tasks=[#<ROM::Struct::Task id=1 user_id=1 title="Jane's task">]>

> tasks.wrap(:user).first
=> #<ROM::Struct::Task id=1 user_id=1 title="Jane's task" user=#<ROM::Struct::User id=1 name="Jane">>

Support for custom struct classes

This is probably the biggest enhancement, you can now configure your own struct_namespace where your own struct classes are defined, and there's no need to define attributes. This can be called "Active Record mode" (in a good way!). Resulting struct objects are still decoupled from the database, their structure is based on relation data, which can be projected anyhow you want. This means that we have dynamic struct objects, but without 1:1 mapping between your database schema and their attributes.

Let's say you decide to put your own struct classes under Entities module. This module can be configured as the struct_namespace, and rom mappers will automatically find classes, matching relation names. Here's an example:

module Entities
  class User < ROM::Struct
    def task_titles
      tasks.map(&:title)
    end
  end
end

class Users < ROM::Relation[:sql]
  schema(infer: true) do
    associations do
      has_many :tasks
    end
  end

  auto_struct true
  struct_namespace Entities
end

class Tasks < ROM::Relation[:sql]
  schema(infer: true) do
    associations do
      belongs_to :user
    end
  end

  auto_struct true
  struct_namespace Entities
end

Now we will get instances of your own struct class:

> jane = users.combine(:tasks).first
> jane.task_titles
=> ["Jane's task"]

You can learn more about this feature in our updated docs

Standalone changesets

Changesets are now provided by a separate rom-changeset gem and they no longer need repositories. Relations are extended by :changeset plugin, which adds Relation#changeset method. This makes using changesets more straightforward, here's an example:

users.changeset(:create, name: "John").commit
# {:id=>2, :name=>"John"}

user_changeset = users.by_pk(2).changeset(:update, name: "John Doe")

user_changeset.diff?
# => true

user_changeset.diff
# => {:name=>"John Doe"}

user_changeset.commit
# {:id=>2, :name=>"John Doe"}

See Changeset documentation for more information.

...and more

There are dozens of other improvements and new features, to quickly summarize few more:

  • Experimental auto-migration feature known from DataMapper project - you will hear more about this soon!
  • You no longer need to define relations in repository classes
  • You can define custom association views with non-standard combine/join keys
  • Configuration uses an event bus now, which you can use to hook into setup process and enable additional features. Our plugin system uses it already.
  • New APIs have been added to rom-sql, including SQL::Relation#exists, SQL::Relation#each_batch, SQL::Relation#import and SQL::Relation#explain (for PG)
  • SQL conditions can be negated by using idiomatic ! operator, ie users.where { !admin.is(true) }

Release information and upgrading

This is a major release with breaking changes. Please refer to the upgrade guide for more information. As part of 4.0.0, following gems have been released:

If you're having problems with the upgrade, please seek for help on discussion forum.

Thank you :)

Thank you to all our contributors and supporters. Special thanks go to Nikita Shilnikov for his amazing work on making rom-sql better, implementing auto-migrations feature and helping with development of core APIs and addressing issues!

This has been the biggest effort so far, and it's probably the most important release we've had. We'll continue working on bug-fix and minor upgrades soon (there are already PRs opened with new features!), so stay tuned.

Check out rom-rb 4.0.0 and tell us what you think!


  • 1 of 8