Faceter

Experimental {ROM}[https://github.com/rom-rb/rom]-compatible data mapper, based on the transproc gem.

No monkey-patching, no mutable instances of any class.

100% {mutant}[https://github.com/mbj/mutant]-covered.

Motivation

Basicaly the gem does about the same as the ROM mappers do. But its DSL has a different semantics:

The Faceter is the experimental gem. I've wrote it to check whether this "concept" of procedure-like syntax would work fine and not overkill the recipy with too much details.

Synopsis

To access the data the mapper DSL has two methods, that can be nested deeply:

The mapper also has methods to transform the accessed data (either a field of some tuple or values of some array):

Suppose you need to transform array of nested data:

source = [
  {
    id: 1, name: 'Joe', roles: ['admin'],
    emails: [
      { address: 'joe@doe.com', type: 'job' },
      { address: 'joe@job.com', type: 'job' },
      { address: 'joe@doe.org', type: 'personal' }
    ]
  }
]

To create a mapper, include Faceter to the mapper class and define the sequence of transformations. Transformations will be applied step-by-step to data at a corresponding level.

require "faceter"

class Mapper
  include Faceter

  list do
    field :roles do
      list { fold to: :role }
    end

    rename :emails, to: :contacts
  end

  # Both `ungroup` and `group` work with arrays as a whole. You haven't
  # wrap them to the `list` unless your data are not the arrays of arrays.
  ungroup :role, from: :roles

  group :id, :name, :contacts, to: :user

  list do
    field :user do
      field :contacts do
        list { rename :address, to: :email }
        group :address, to: :emails
      end
    end
  end
end

To apply transformation to the source initialize the mapper and send the source array to its call instance method:

mapper = Mapper.new
mapper.call source
# => [
#      {
#        role: 'admin',
#        users: [
#          {
#            id: 1,
#            name: 'Joe',
#            contacts: [
#              { type: 'job',      emails: ['joe@doe.com', 'joe@job.com'] },
#              { type: 'personal', emails: ['joe@doe.org'] }
#            ]
#          }
#        ]
#      }
#    ]

Instantiating Models

You can do any data transformations using create method, that does the following:

require "faceter"
require "ostruct"

class Mapper
  include Faceter

  list do
    create from: [:id, :name, :email] do |id, name, email|
      OpenStruct.new id: id, name: name, email: email
    end
  end
end

Alternatively, you could wrap the necessary keys and then create new value with the same key:

require "faceter"
require "ostruct"

class Mapper
  include Faceter

  list do
    wrap to: :user # wraps all keys in every tuple to the :user key

    create from: :user do |user|
      OpenStruct.new(user)
    end
  end
end

Both the examples transform the array of tuples:

source = [
  { id: 1, name: "Joe",  email: "joe@doe.com"  },
  { id: 2, name: "Jane", email: "jane@doe.com" }
]

into the array of instances:

mapper = Mapper.new
mapper.call(source) 
# => [
#  #<OpenStruct @id=1, @name="Joe",  @email="joe@doe.com">,
#  #<OpenStruct @id=2, @name="Jane", @email="jane@doe.com">
# ]

Data Serialization

In just the same way you can serialize models at the mapper layer:

source = [
  OpenStruct.new(id: 1, name: "Joe", email: "joe@doe.com"),
  OpenStruct.new(id: 2, name: "Jane", email: "jane@doe.com")
]

class Mapper < Faceter::Mapper
  list do
    # take every item in a list, create the value and assign it back to the item
    create do |item|
      { id: item.id, name: item.name, email: item.email }
    end
  end
end

mapper = Mapper.new
mapper.call source
# => [
#      { id: 1, name: "Joe", email: "joe@doe.com" },
#      { id: 2, name: "Jane", email: "jane@doe.com" }
#    ]

ROM-compatibility

To use the mapper in ROM you can register it as a custom mapper for the corresponding relation:

setup = ROM.setup :memory

setup.relation(:users)
setup.mappers { register(:users, my_mapper: mapper) }

rom = ROM.finalize.env

rom.relation(:users).as(:my_mapper).to_a
# => returns the converted data

Installation

Add this line to your application's Gemfile:

# Gemfile
gem "faceter"

Then execute:

bundle

Or add it manually:

gem install faceter

Compatibility

Tested under rubies compatible to MRI 2.1+. JRuby is supported from the head version only.

Uses RSpec 3.0+ for testing and hexx-suit for dev/test tools collection.

Contributing

Credits

This project is heavily inspired by and based on gems written by Piotr Solnica:

License

See the MIT LICENSE.