Skip to content

Repository files navigation

CaseComplete

A Scala 3 library that provides compile-time guarantees for complete case class field handling. CaseComplete ensures that all fields of a case class are processed by your transformation logic, preventing runtime errors from forgotten fields.

CaseComplete Demo - Compile-time field validation

Features

  • Compile-time Safety: Ensures all case class fields have corresponding handlers
  • Optional Field Support: Built-in support for Option fields with usingNonEmpty
  • Builder Pattern: Fluent API for building handlers
  • Macro-based: Leverages Scala 3 macros for compile-time validation

Installation

Maven Central

build.sbt:

libraryDependencies +="io.github.stivens"%%"casecomplete"%"0.3.0"

scala-cli:

//>usinglib"io.github.stivens::casecomplete:0.3.0"

scala-cli REPL:

scala-cli repl --dep io.github.stivens::casecomplete:0.3.0

Quick Start

importio.github.stivens.casecomplete.CaseCompleteimportdoobie.*importjava.time.YearcaseclassMovieFilter(
title_like: Option[String] =None,
director_eq: Option[String] =None,
releaseYear_eq: Option[Year] =None,
rating_gte: Option[Double] =None
)
// Create a handler that transforms MovieFilter to SQL conditionsvalmovieFilterHandler=CaseComplete.build[MovieFilter, Option[Fragment]]
.usingNonEmpty(_.title_like)(title =>fr"title ILIKE $title")
.usingNonEmpty(_.director_eq)(director =>fr"director = $director")
.usingNonEmpty(_.releaseYear_eq)(year =>fr"release_year = $year")
.usingNonEmpty(_.rating_gte)(rating =>fr"rating >= $rating")
.compile
// Use the handlervalfilter=MovieFilter(
releaseYear_eq =Some(Year.of(1999)),
rating_gte =Some(7.0)
)
valconditions= movieFilterHandler.eval(filter).toSet.flatten
// Returns: Set(fr"release_year = ${1999}", fr"rating >= ${7.0}")

How It Works

CaseComplete uses Scala 3's macro system to:

  1. Track Handled Fields: The builder tracks which fields have been handled through type parameters
  2. Compile-time Validation: When you call .compile(), it verifies all case class fields have handlers
  3. Field Name Extraction: Extracts field names from selectors like _.fieldName at compile time

CaseComplete vs Pattern Matching

While pattern matching on case classes is a powerful Scala feature, it has limitations when it comes to ensuring complete field handling. CaseComplete provides dual-purpose functionality: it not only allows you to implement transformations that are validated at compile-time, but also provides an interface that guarantees every implementation will have these properties.

The Problem with Pattern Matching

Pattern matching on case classes is just a specific implementation of A => B functions. You cannot enforce the use of pattern matching at the interface level - the interface only specifies the function signature, not how it should be implemented. This means there's no compile-time guarantee that all fields will be handled.

abstractclassAbstractRepository[ENTITY_TYPE, FILTER_TYPE, UPDATE_TYPE](
tableName: String,
evalFilter: FILTER_TYPE=>Set[Fragment],
evalUpdate: UPDATE_TYPE=>Set[Fragment]
) {
// some methods etc
}
caseclassMovieFilter(
title_like: Option[String] =None,
director_eq: Option[String] =None,
releaseYear_eq: Option[Year] =None,
rating_gte: Option[Double] =None
)
caseclassMovieUpdate(
title: Option[String],
rating: Option[Double],
cast: Option[List[Person]]
)
objectMovieRepositoryextendsAbstractRepository[Movie, MovieFilter, MovieUpdate] (
tableName ="movies",
evalFilter = {
caseMovieFilter(director_eq, title_like, releaseYear_eq, rating_gte) =>List(
title_like.map(title =>fr"title ILIKE $title"),
director_eq.map(director =>fr"director = $director"),
releaseYear_eq.map(year =>fr"release_year = $year"),
rating_gte.map(rating =>fr"rating >= $rating")
).flatten.toSet
}, // This looks good at first glance, but notice the order mismatch:// - Pattern has: director_eq, title_like, releaseYear_eq, rating_gte// - Usage has: title_like, director_eq, releaseYear_eq, rating_gte// This will cause runtime bugs: fr"title ILIKE 'some director name'" and fr"director = 'some movie title'"
evalUpdate = update => {
List(
update.title.map(title =>fr"title = $title"),
update.cast.map(cast =>fr"cast = $cast")
).flatten.toSet
} // Whoops - the `rating` field is not handled, but it still compiles!
)

The CaseComplete Solution

With CaseComplete, you can define the AbstractRepository to enforce complete field handling:

abstractclassAbstractRepository[ENTITY_TYPE, FILTER_TYPE, UPDATE_TYPE](
tableName: String,
evalFilter: CaseComplete[FILTER_TYPE, Option[Fragment]],
evalUpdate: CaseComplete[UPDATE_TYPE, Option[Fragment]]
) {
// some methods etc
}

Now, providing an implementation that isn't validated at compile-time is impossible. All classes that inherit from AbstractRepository must provide implementations that handle every field:

objectMovieRepositoryextendsAbstractRepository[Movie, MovieFilter, MovieUpdate] (
tableName ="movies",
evalFilter =CaseComplete.build[MovieFilter, Option[Fragment]]
.usingNonEmpty(_.title_like)(title =>fr"title ILIKE $title")
.usingNonEmpty(_.director_eq)(director =>fr"director = $director")
.usingNonEmpty(_.releaseYear_eq)(year =>fr"release_year = $year")
.usingNonEmpty(_.rating_gte)(rating =>fr"rating >= $rating")
.compile,
evalUpdate =CaseComplete.build[MovieUpdate, Option[Fragment]]
.usingNonEmpty(_.title)(title =>fr"title = $title")
.usingNonEmpty(_.rating)(rating =>fr"rating = $rating")
.usingNonEmpty(_.cast)(cast =>fr"cast = $cast")
.compile
)

Pro tip: Make interfaces more expressive with type aliases

typeAsFragments[A<:Product] =CaseComplete[A, Option[Fragment]]
deftoFragments[A<:Product]:CaseCompleteBuilder[A, Option[Fragment], EmptyTuple] =CaseComplete.build[A, Option[Fragment]]
abstractclassAbstractRepository[ENTITY_TYPE, FILTER_TYPE, UPDATE_TYPE](
tableName: String,
evalFilter: AsFragments[FILTER_TYPE],
evalUpdate: AsFragments[UPDATE_TYPE]
)
objectMovieRepositoryextendsAbstractRepository[Movie, MovieFilter, MovieUpdate] (
tableName ="movies",
evalFilter = toFragments[MovieFilter]
.usingNonEmpty(_.title_like)(title =>fr"title ILIKE $title")
.usingNonEmpty(_.director_eq)(director =>fr"director = $director")
.usingNonEmpty(_.releaseYear_eq)(year =>fr"release_year = $year")
.usingNonEmpty(_.rating_gte)(rating =>fr"rating >= $rating")
.compile,
evalUpdate = toFragments[MovieUpdate]
.usingNonEmpty(_.title)(title =>fr"title = $title")
.usingNonEmpty(_.rating)(rating =>fr"rating = $rating")
.usingNonEmpty(_.cast)(cast =>fr"cast = $cast")
.compile
)

API Reference

CaseComplete.build

Creates a new builder instance:

objectCaseComplete {
defbuild[SOURCE_TYPE<:Product, TARGET_TYPE]:CaseCompleteBuilder[SOURCE_TYPE, TARGET_TYPE, EmptyTuple]

Builder Methods

using(_.field)(handler)

Registers a handler for a specific field:

builder.using(_.fieldName)(value => transformedValue)

usingNonEmpty(_.field)(handler) (for Option fields)

Registers a handler for optional fields, automatically handling None:

builder.usingNonEmpty(_.optionalField)(value => transformedValue)
// equivalant to builder.using(_.optionalField)((_: Option[F]).map((value: F) => transformedValue))

ignoring(_.field)

Explicitly marks a field as ignored during processing. This is useful when you want to intentionally skip a field (e.g., deprecated fields) while ensuring compile-time validation that you didn't forget to handle it:

builder.ignoring(_.deprecatedField)

Why use ignoring? When you have fields that you intentionally don't want to process (like deprecated fields, internal fields, or fields that don't apply to your use case), ignoring provides a clear, explicit way to indicate this intention. It guarantees that you made a conscious decision to ignore the field rather than accidentally forgetting to handle it.

compile

Compiles the handler and validates all fields are handled:

valbuilder:CaseCompleteBuilder[A, B, _] =???valhandler:CaseComplete[A, B] = builder.compile

Handler Usage

valresult= handler.eval(sourceInstance)
// Returns: List[TargetType]

Requirements

  • Scala >= 3.3

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

Compile-time guaranteed case class field handling for Scala 3

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages