Pipez
Generate type mappers for your own type class
Install / Use
/learn @kubuszok/PipezREADME
Pipez
Scala library for type-safe data-transformations, which allows you to build-in Chimney-like abilities to your own type classes and effects.
<!-- START doctoc generated TOC please keep comment here to allow auto update --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->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.
Table of Contents
- Installation
- Motivating example
- Custom parsers
- Supported features and configuration options
- How to define
PipeDerivation - Pipez and Chimney
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!
Converteris a demonstration how Pipez can be used to implement something similar to Chimney'sTransformer.
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
qqbot-channel
349.2kQQ 频道管理技能。查询频道列表、子频道、成员、发帖、公告、日程等操作。使用 qqbot_channel_api 工具代理 QQ 开放平台 HTTP 接口,自动处理 Token 鉴权。当用户需要查看频道、管理子频道、查询成员、发布帖子/公告/日程时使用。
docs-writer
100.3k`docs-writer` skill instructions As an expert technical writer and editor for the Gemini CLI project, you produce accurate, clear, and consistent documentation. When asked to write, edit, or revie
model-usage
349.2kUse CodexBar CLI local cost usage to summarize per-model usage for Codex or Claude, including the current (most recent) model or a full model breakdown. Trigger when asked for model-level usage/cost data from codexbar, or when you need a scriptable per-model summary from codexbar cost JSON.
arscontexta
3.0kClaude Code plugin that generates individualized knowledge systems from conversation. You describe how you think and work, have a conversation and get a complete second brain as markdown files you own.
