I'm trying to create a type safe builder of a case class , where its params can be of following types:
I figured how to get required and optional cases, but cannot get case 3 and 4. Any ideas how to proceed.
These checks can be done at runtime, but I want these set of rules being implemented at compile time
case class Person(name: String, /*required*/
address: String, /*optional*/
city: String, /* reqd exclusive*/
county: String, /* reqd exclusive*/
state: String /* reqd exclusive*/,
ssn: String, /* optional exclusive*/
insurance: String, /* opt exclusive*/
passport: String /* opt exclusive*/)
// where (city) and (county, state) are required but are mutually exclusive
// (ssn) and (insurance, passport) are optional but are mutually exclusive.
// If I set passport, I've to set insurance
sealed trait PersonInfo
object PersonInfo {
sealed trait Empty extends PersonInfo
sealed trait Name extends PersonInfo
sealed trait Address extends PersonInfo
type Required = Empty with Name with Address
}
case class PersonBuilder[T <: PersonInfo]
(name: String = "", address: String = "", city: String = "", county: String = "",
state: String = "", ssn: String = "", insurance: String = "",passport: String ="") {
def withName(name: String): PersonBuilder[T with PersonInfo.Name] =
this.copy(name = name)
def withTask(address: String): PersonBuilder[T with PersonInfo.Address ] =
this.copy(address = address)
def withCity(city: String): PersonBuilder[T] =
this.copy(city = city)
def withCountry(county: String): PersonBuilder[T] =
this.copy(county = county)
def withState(state: String): PersonBuilder[T] =
this.copy(state = state)
def withSsn(ssn: String): PersonBuilder[T] =
this.copy(ssn = ssn)
def withInsurance(insurance: String): PersonBuilder[T] =
this.copy(insurance = insurance)
def withPassport(passport: String): PersonBuilder[T] =
this.copy(passport = passport)
def build(implicit ev: T =:= PersonInfo.Required): Person =
Person(name, address, city, county, state, ssn, insurance, passport)
}
here's the build
val testPerson = PersonBuilder[PersonInfo.Empty]()
.withName("foo")
.withSsn("bar")
As mentioned in a comment, if creating a builder is not a hard requirement, a viable choice could be to make those requirements explicit in the types, using sum types for exclusive choices and Option
s for optional ones, as in the following example:
sealed abstract class Location extends Product with Serializable {
def value: String
}
object Location {
final case class City(value: String) extends Location
final case class County(value: String) extends Location
final case class State(value: String) extends Location
}
sealed abstract class Identity extends Product with Serializable {
def value: String
}
object Identity {
final case class Ssn(value: String) extends Identity
final case class Insurance(value: String) extends Identity
final case class Passport(value: String) extends Identity
}
final case class Person(
name: String,
address: Option[String],
location: Location,
identity: Option[Identity],
)
Scala 3 further introduced enum
s which makes the definition more compact and readable:
enum Location(value: String) {
case City(value: String) extends Location(value)
case County(value: String) extends Location(value)
case State(value: String) extends Location(value)
}
enum Identity(value: String) {
case Ssn(value: String) extends Identity(value)
case Insurance(value: String) extends Identity(value)
case Passport(value: String) extends Identity(value)
}
final case class Person(
name: String,
address: Option[String],
location: Location,
identity: Option[Identity],
)
And making Option
s default to None
you get a very similar experience to custom-made builders without any additional code:
final case class Person(
name: String,
location: Location,
address: Option[String] = None,
identity: Option[Identity] = None,
)
Person("Alice", Location.City("New York"))
.copy(identity = Some(Identity.Ssn("123456")))
Which you can further refine very easily:
final case class Person(
name: String,
location: Location,
address: Option[String] = None,
identity: Option[Identity] = None
) {
def withAddress(address: String): Person =
this.copy(address = Some(address))
def withIdentity(identity: Identity): Person =
this.copy(identity = Some(identity))
}
Person("Alice", Location.City("New York")).withIdentity(Identity.Ssn("123456"))
You can play around with this code here on Scastie.