Re:map
; an expressive and feature-rich data transformer written in Ruby 3.
It gives the developer the expressive power of JSONPath, without the hassle of using strings. Its compiler is written on top of an immutable, primitive data structure utilizing ruby's refinements & pattern matching capabilities – making it blazingly fast
- Overview
require "remap"
class Mapper < Remap::Base
option :date # <= Custom required value
define do
# Fixed values
set :description, to: value("This is a description")
# Semi-dynamic values
set :date, to: option(:date)
# Required rules
get :friends do
each do
# Post processors
map(:name, to: :id).then do |value:|
"#{value.upcase}!"
end
# Field conditions
get?(:age).if do |age|
(30..50).cover?(age)
end
# Map to a finite set of values
get :phones do
each do
map.enum do
from "iPhone", to: "iOS"
value "iOS", "Android"
otherwise "Unknown"
end
end
end
end
end
# Composable mappers
class Linux < Remap::Base
define do
get :kernel
end
end
class Windows < Remap::Base
define do
get :price
end
end
# Embed mappers
to :os do
map :computer, :operating_system do
embed Linux | Windows
end
end
# Wrapping values in arrays
to :houses do
wrap :array do
map :house
end
end
# Nested paths ($.cars[*].model)
map :cars, all, :model, to: :cars
# Or using the #each iterator
map :cars do
each do
map :model, to: :cars
end
end
end
end
Input hash to be mapped
input = {
house: "100kvm",
friends: [
{
name: "Lisa",
age: 20,
phones: ["iPhone"]
}, {
name: "Jane",
age: 40,
phones: ["Samsung"]
}
],
computer: {
operating_system: {
kernel: :latest
}
},
cars: [
{
owners: [
{
name: "John"
}
]
}
]
}
The expected mapped output
output = {
friends: [
{
id: "LISA!",
phones: ["iOS"]
}, {
age: 40,
id: "JANE!",
phones: ["Unknown"]
}
],
description: "This is a description",
cars: [{ owners: ["John"] }],
houses: ["100kvm"],
date: Date.today,
os: {
kernel: :latest
}
}
Invoking the mapper with input and the date
option
Mapper.call(input, date: Date.today) # => output
Add remap
to your Gemfile
# Use Github as source
source "https://rubygems.pkg.github.com/oleander" do
gem "remap"
end
# Or Rubygems
gem "remap"
Then run bundle install
To create a mapper, inherit from Remap::Base
and define your rules using define
.
class Mapper < Remap::Base
define do
# rules goes here
end
end
Mapper.call(input)
Or use define
method directly on the Remap module
mapper = Remap.define do
# rules goes here
end
mapper.call(input)
The easiest way to get started is using map
.
map
transform a value from one path to another.
class Mapper < Remap::Base
define do
map :name, to: :nickname
end
end
To invoke the mapper, call Mapper.call
with any input, i.e
Mapper.call({ name: "John" }) # => { nickname: "John" }
If the input data doesn't match the defined rule, an exception
will be thrown explaining what went wrong and where.
To prevent this, you can pass a block to .call
.
The mapper will yield failures to the block instead of raising an error.
Mapper.call({ something: "value" }) do |failure|
# ...
end
Use map?
, to?
and get?
to map partial data structures.
class Mapper < Remap::Base
define do
map? :key1
map? :key2
end
end
If one of the two rules succeeds, the mapper returns a value.
Mapper.call({ key1: "value1" }) # ="value1"
Mapper.call({ key2: "value2" }) # ="value2"
If none of the rules succeeds, the mapper invokes the error block.
Mapper.call({ nope: "value" }) do |failure|
# ...
end
Rules can be expressed in a variety of ways to best fit the problem at hand.
The following rules yields the same output
# Flat map
map :person, :name, to: :first_name
# Flat to
to :first_name, map: [:person, :name]
# Nested map
map :person do
map :name do
to :first_name
end
end
# Nested to
to :first_name do
map :person do
map :name
end
end
To select a value and its path, use get
, or get?
.
class Mapper < Remap::Base
define do
get :person
end
end
Mapper.call({ person: "John" }) # => { person: "John" }
Use each
when iterating over arrays and hashes.
class Mapper < Remap::Base
define do
map :people do
each do
map :name
end
end
end
end
Mapper.call({ people: [{ name: "John" }, { name: "Jane" }] }) # => ["John", "Jane"]
Use the all
selector as part of the path instead of each
.
all
is similar to JSONPath’s[*]
selector
class Mapper < Remap::Base
define do
map :people, all, :name
end
end
first
selects the first element in an array and last
the last element.
first
&last
is similar to JSONPath’s[0]
[-1]
selectors
class Mapper < Remap::Base
define do
map :people do
map first, :name, to: :name
end
end
end
Mapper.call({ people: [{ name: "John" }] }) # => { name: "John" }
Selected values can easily be processed before being returned using call-backs.
See
Remap::Rule::Map
for more information
class Mapper < Remap::Base
using Remap::Extensions::Hash
define do
map :people, all do
# Pass a proc
map(:name).then(&:upcase)
# Or pass a block
map(:name).then do |value:|
value.upcase
end
# Manually skip a mapping
map(:name).then do |&error|
error["skip"]
end
# Add conditions
map?(:name).if do |value:|
value.include?("John")
end
map?(:name).if_not do |value:|
value.include?("Lisa")
end
# Pending mappings
map(:name).pending("I'll do this later")
# Define rules for a finite set of values
map(:name).enum do
from "John", to: "Joe"
value "Lisa", "Jane"
otherwise "Unknown"
end
# Get is defined by the Remap::Extensions::Hash refinement
# and allows for a path to be passed. If the path is missing,
# the rule will be ignored in the case of `map?` and `map`
# a detailed error message will be thrown with a detailed path
map(:name).then do |value:|
value.get(:a, :b)
end
end
end
end
The callback context has access to the following values
value
current valueelement
- defined byeach
index
defined byeach
key
defined byto
,map
andeach
on hashesvalues
&input
yields the mapper inputmapper
the current mapper
class Person < Remap::Base
using Remap::Extensions::Enumerable
define do
get :person do
get(:name)
get?(:age).if do |values:|
values.get(:person, :name) == "John"
end
end
end
end
See
Remap::State::Extension#execute
for more details
A mapper can require options using the option
method.
An option can be referenced from within callbacks and via set
.
class Mapper < Remap::Base
option :code
define do
set :secret, to: option(:code)
# Access {code} inside a callback
map(:pin_code, to: :seed).then do |pin, code:|
code**pin
end
end
end
The second argument to Mapper.call
takes a hash and is used as options for the mapper.
Mapper.call({
pin_code: 1234
}, code: 5678) # => { secret: 5678, seed: 3.2*10^10 }
set
can also take a fixed value using the value
method
class Mapper < Remap::Base
define do
set :api_key, to: value("ABC-123")
end
end
Mapper.call(input) # => { api_key: "ABC-123" }
wrap
allows output values to be type casts into an array.
class Mapper < Remap::Base
define do
to :names do
wrap(:array) do
map :name
end
end
end
end
Mapper.call({ name: "John" }) # ={ names: ["John"] }
Mappers can be composed using the |
(or), &
(and) and ^
(xor) operators.
Composed mappers can then be embedded into other mappers using embed
.
class Bicycle < Remap::Base
contract do
required(:gears)
required(:brand)
end
define do
to :bicycle
end
end
class Car < Remap::Base
contract do
required(:hybrid)
required(:fuel)
end
define do
to :car
end
end
class Vehicle < Remap::Base
define do
each do
embed Bicycle | Car
end
end
end
Vehicle.call([
{
gears: 3,
brand: "Rose"
}, {
hybrid: false,
fuel: "Petrol"
}
]) # => [{ bicycle: { gears: 3, brand: "Rose" } }, { car: { hybrid: false, fuel: "Petrol" } }]
TODO
TODO
TODO