Re:map
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
Quick start
``` ruby 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
ruby
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
ruby
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
ruby
Mapper.call(input, date: Date.today) # => output
Installation
Add remap
to your Gemfile
``` ruby # Use Github as source source “https://rubygems.pkg.github.com/oleander” do gem “remap” end
Or Rubygems
gem “remap” ```
Then run bundle install
Introduction
To create a mapper, inherit from Remap::Base
and define your rules using define
.
``` ruby class Mapper < Remap::Base define do # rules goes here end end
Mapper.call(input) ```
Or use define
method directly on the Remap module
``` ruby 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.
ruby
class Mapper < Remap::Base
define do
map :name, to: :nickname
end
end
To invoke the mapper, call Mapper.call
with any input, i.e
ruby
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.
ruby
Mapper.call({ something: "value" }) do |failure|
# ...
end
Use map?
, to?
and get?
to map partial data structures.
ruby
class Mapper < Remap::Base
define do
map? :key1
map? :key2
end
end
If one of the two rules succeeds, the mapper returns a value.
ruby
Mapper.call({ key1: "value1" }) # ="value1"
Mapper.call({ key2: "value2" }) # ="value2"
If none of the rules succeeds, the mapper invokes the error block.
ruby
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
``` ruby # 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?
.
``` ruby class Mapper < Remap::Base define do get :person end end
Mapper.call({ person: “John” }) # => { person: “John” } ```
Use each
when iterating over arrays and hashes.
``` ruby 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”] ```
Selectors
Use the all
selector as part of the path instead of each
.
all
is similar to JSONPath’s[*]
selector
ruby
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
``` ruby class Mapper < Remap::Base define do map :people do map first, :name, to: :name end end end
Mapper.call({ people: [{ name: “John” }] }) # => { name: “John” } ```
Callbacks
Selected values can easily be processed before being returned using call-backs.
See
Remap::Rule::Map
for more information
``` ruby 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
``` ruby 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
Fixed & semi-fixed values
A mapper can require options using the option
method.
An option can be referenced from within callbacks and via set
.
``` ruby 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.
ruby
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
``` ruby class Mapper < Remap::Base define do set :api_key, to: value(“ABC-123”) end end
Mapper.call(input) # => { api_key: “ABC-123” } ```
Type casting
wrap
allows output values to be type casts into an array.
``` ruby class Mapper < Remap::Base define do to :names do wrap(:array) do map :name end end end end
Mapper.call({ name: “John” }) # ={ names: [“John”] } ```
Operators
Mappers can be composed using the |
(or), &
(and) and ^
(xor) operators.
Composed mappers can then be embedded into other mappers using embed
.
``` ruby 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” } }] ```
Error handling
TODO
Constructors
TODO
Schemas & rules
TODO