Transform Ruby objects in functional style
Ruby
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.

README.md

Transproc

Gem Version Build Status Dependency Status Code Climate Test Coverage Inline docs

Transproc is a small library that allows you to compose procs into a functional pipeline using left-to-right function composition.

The approach came from Functional Programming, where simple functions are composed into more complex functions in order to transform some data. It works like |> in Elixir or >> in F#.

transproc provides a mechanism to define and compose transformations, along with a number of built-in transformations.

It's currently used as the data mapping backend in Ruby Object Mapper.

Installation

Add this line to your application's Gemfile:

gem 'transproc'

And then execute:

$ bundle

Or install it yourself as:

$ gem install transproc

Basics

Simple transformations are defined as easy as:

increment = Transproc::Function.new(-> (data) { data + 1 })
increment[1] # => 2

It's easy to compose transformations:

to_string = Transproc::Function.new(:to_s.to_proc)
(increment >> to_string)[1] => '2'

It's easy to pass additional arguments to transformations:

append = Transproc::Function.new(-> (value, suffix) { value + suffix })
append_bar = append.with('_bar')
append_bar['foo'] # => foo_bar

Or even accept another transformation as an argument:

map_array = Transproc::Function.new(-> (array, fn) { array.map(&fn) })
map_array.with(to_string).call([1, 2, 3]) # => ['1', '2', '3']

To improve this low-level definition, you can use class methods with Transproc::Registry:

M = Module.new do
  extend Transproc::Registry

  def self.to_string(value)
    value.to_s
  end

  def self.map_array(array, fn)
    array.map(&fn)
  end
end
M[:map_array, M[:to_string]].([1, 2, 3]) # => ['1', '2', '3']

Built-in transformations

transproc comes with a lot of built-in functions. They come in the form of modules with class methods, which you can import into a registry:

You can import everything with:

module T
  extend Transproc::Registry

  import Transproc::Coercions
  import Transproc::ArrayTransformations
  import Transproc::HashTransformations
  import Transproc::ClassTransformations
  import Transproc::ProcTransformations
  import Transproc::Conditional
  import Transproc::Recursion
end
T[:to_string].call(:abc) # => 'abc'

Or import selectively with:

module T
  extend Transproc::Registry

  import :to_string, from: Transproc::Coercions, as: :stringify
end
T[:stringify].call(:abc) # => 'abc'
T[:to_string].call(:abc)
# => Transproc::FunctionNotFoundError: No registered function T[:to_string]

Transformer

Transformer is a class-level DSL for composing transformation pipelines, for example:

T = Class.new(Transproc::Transformer) do
  map_array do
    symbolize_keys
    rename_keys user_name: :name
    nest :address, [:city, :street, :zipcode]
  end
end

T.new.call(
  [
    { 'user_name' => 'Jane',
      'city' => 'NYC',
      'street' => 'Street 1',
      'zipcode' => '123'
    }
  ]
)
# => [{:name=>"Jane", :address=>{:city=>"NYC", :street=>"Street 1", :zipcode=>"123"}}]

It converts every method call to its corresponding transformation, and joins these transformations into a transformation pipeline (a transproc).

Transproc Example Usage

require 'json'
require 'transproc/all'

# create your own local registry for transformation functions
module Functions
  extend Transproc::Registry
end

# import necessary functions from other transprocs...
module Functions
  # import all singleton methods from a module/class
  import Transproc::HashTransformations
  import Transproc::ArrayTransformations
end

# ...or from any external library
require 'inflecto'
module Functions
  # import only necessary singleton methods from a module/class
  # and rename them locally
  import :camelize, from: Inflecto, as: :camel_case
end

def t(*args)
  Functions[*args]
end

# use imported transformation
transformation = t(:camel_case)

transformation.call 'i_am_a_camel'
# => "IAmACamel"

transformation = t(:map_array, (
  t(:symbolize_keys).>> t(:rename_keys, user_name: :user)
  )).>> t(:wrap, :address, [:city, :street, :zipcode])

transformation.call(
  [
    { 'user_name' => 'Jane',
      'city' => 'NYC',
      'street' => 'Street 1',
      'zipcode' => '123' }
  ]
)
# => [{:user=>"Jane", :address=>{:city=>"NYC", :street=>"Street 1", :zipcode=>"123"}}]

# define your own composable transformation easily
transformation = t(-> v { JSON.dump(v) })

transformation.call(name: 'Jane')
# => "{\"name\":\"Jane\"}"

# ...or add it to registered functions via singleton method of the registry
module Functions
  # ...

  def self.load_json(v)
    JSON.load(v)
  end
end

# ...or add it to registered functions via .register method
Functions.register(:load_json) { |v| JSON.load(v) }

transformation = t(:load_json) >> t(:map_array, t(:symbolize_keys))

transformation.call('[{"name":"Jane"}]')
# => [{ :name => "Jane" }]

Credits

This project is inspired by the work of following people:

Contributing

  1. Fork it ( https://github.com/solnic/transproc/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request