SkillAgentSearch skills...

Pipez

Generate type mappers for your own type class

Install / Use

/learn @kubuszok/Pipez
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Pipez

Pipez JVM Pipez JS Pipez Native

Scaladoc Scaladoc CI build License

Scala library for type-safe data-transformations, which allows you to build-in Chimney-like abilities to your own type classes and effects.

Pipez is a result of research about possible ways of migrating Chimney to Scala 3. It focuses on a certain deprecated type class from Chimney- TransformerF - and while it attempts to replicate as much features as possible it is not intended to replace Chimney nor reimplement all of its features.

<!-- START doctoc generated TOC please keep comment here to allow auto update --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

Table of Contents

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

Installation

Core derivation library:

// Add to sbt if you use only JVM Scala
libraryDependencies += "com.kubuszok" %% "pipez" % "<version>"
// Add to sbt it you use Scala.js or Scala Native
libraryDependencies += "com.kubuszok" %%% "pipez" % "<version>"

Data transformation DSL:

// Add to sbt if you use only JVM Scala
libraryDependencies += "com.kubuszok" %% "pipez-dsl" % "<version>"
// Add to sbt it you use Scala.js or Scala Native
libraryDependencies += "com.kubuszok" %%% "pipez-dsl" % "<version>"

Motivating example

Your user send a request to you and, while handling it, you obtained the domain data:

enum UserType:
  case Normal
  case Admin

final case class User(
  id:       String,
  name:     String,
  password: Array[Byte],
  userType: UserType
)

which you have to manually rewrite into format used by your API endpoints:

enum UType:
  case Normal
  case Admin

final case class ApiUser(
  id:       String,
  name:     String,
  userType: UType
)

These types are almost identical, so rewriting it would be pretty dumb - we should be able to convert one into the other just by matching the corresponding fields (or subtypes) by name! That's what Convert[From, To] type class from pipez DSL does:

// given such User
val user: User = User(
  id       = "user-1",
  name     = "User #1",
  password = "some-hash".getBytes,
  userType = UserType.Normal,
)

import pipez.dsl.* // provides convertInto

// we can generate the conversion into ApiUser!
val apiUser = user.convertInto[ApiUser]

apiUser == ApiUser(
  id       = "user-1",
  name     = "User #1",
  userType = UType.NORMAL
)

Under the hood there is a type class:

// Converts From value into To value, without failure
trait Converter[From, To]:
  def convert(from: From): To

and user.convertInto[ApiUser] creates an instance of Converter[User, ApiUser] and calls .convert(user) on it. It basically writes for us:

// This is (more or less) what
//   user.convertInto[ApiUser]
// generates:
new Converter[User, ApiUser] {

  def convert(from: User): ApiUser = ApiUser(
    // Here we map each User field to corresponding to ApiUser
    // field by their name:
    id       = from.id,
    name     = from.name,
    // and when we need to map UserType to UType
    // we map them by their corresponding subtype names:
    userType = from.userName match {
      case UserType.Normal => UType.Normal
      case UserType.Admin  => UType.Admin
    }
  )
}
// Finally, we apply User value to created Converter
.convert(user)

letting us forget about writing all this dumb, error-prone boilerplate code ourselves!

Converter is a demonstration how Pipez can be used to implement something similar to Chimney's Transformer.

Great, but what if needed to convert things the other way? We would receive the password and we would have to hash it with a function:

// Left describes the parsing error
def hashPassword(password: String): Either[String, Array[Byte]]

and we receive:

final case class ApiUserWithPassword(
  id:       String,
  name:     String,
  password: String, // different type than User.password
  userType: UType
)

This would require some ability to fail conversion with an error. We still have a way of parsing that automatically!

import pipez.dsl.* // provides parseFastInto and Parser

val apiUserWithPassword: ApiUserWithPassword = ...

// We are turning the function into Parser instance...
implicit val passwordParser: Parser[String, Array[Byte]] =
  Parser.instance(hashPassword)

// ...because it will be picked up when looking how to perform conversion
//   ApiUserWithPassword.password => User.password
// which might produce errors:
val userResult = apiUserWithPassword.parseFastInto[User]

// assuming correct password:
userResult == Right(User(
  id       = apiUserWithPassword.id,
  name     = apiUserWithPassword.name,
  password = hashPassword(apiUserWithPassword.password).right.get,
  userType = apiUserWithPassword.userType match {
    case UType.Normal => UserType.Normal,
    case UType.Admin  => UserType.Admin
  }
))

Parsing allows you to calculate all possible errors or give up upon the first one - for that you have parseFullInto and parseFastInto methods. Similarly to Converter there is Parser type class:

// Converts From value into To value, but allows conversion to fail, report path
// to the failed value, and chose between fail fast and full error reporting.
trait Parser[From, To]:

  def parse(
    from:     From, // parsed input
    path:     Parser.Path, // Vector of fields/subtype matches leading to the value
    failFast: Parser.ShouldFailFast // =:= Boolean, should we fail fast or continue
  ): Parser.ParsingResult[To] // =:= Either[Errors, To], accumulates parsing errors

  final def parseFast(from: From): Parser.ParsingResult[To] =
    parse(from, Vector.empty, failFast = true)
  final def parseFull(from: From): Parser.ParsingResult[To] =
    parse(from, Vector.empty, failFast = false)

When we called apiUserWithPassword.parseFastInto[User] we created Parser[ApiUserWithPassword, User] instance and called .parseFast(apiUserWithPassword.parseFastInto) on it.

// This is (more or less!) what
//   apiUserWithPassword.parseFastInto[User]
// generates:
new Parser[ApiUserWithPassword, User] {

  def parse(
    from:     ApiUserWithPassword,
    path:     Parser.Path,
    failFast: Parser.ShouldFailFast
  ): Parser.ParsingResult[User] =
    // We start by calling Parsers for types we cannot just
    // copy paste:
    passwordParser.parse(
      from.password,
      // giving them some extra informaton how we got the value
      path :+ PathSegment.AtField("password"),
      failFast
    ).map { password =>
      // once all "parsable" fields are parsed, we just rewrite
      // the rest matching fields and subtypes by their name
      User(
        id       = from.id,
        name     = from.name,
        password = password,
        userType = from.userType match {
          case UType.Normal => UserType.Normal,
          case UType.Admin  => UserType.Admin
        }
      )
    }
}
// Finally, we pass ApiUserWithPassword to try to convert it to User
.parseFast(apiUserWithPassword)

`Pars

Related Skills

View on GitHub
GitHub Stars13
CategoryContent
Updated15d ago
Forks1

Languages

Scala

Security Score

95/100

Audited on Mar 21, 2026

No findings