Tim Gautier
Tim Gautier

Reputation: 30142

Is this possible with less code repetition

We're using case classes to represent the JSON objects transferred between the client and server. It's been working great except for one sticking point we've been living with for quite a while now and I wonder if anyone has a clever way around it.

Let's say I have a user object that has id, first name, last name and email address. Once a user has been saved to the database, he has an id (Int) assigned to him, so for all communication between the client and server dealing with existing users, the id is a required field. In fact, there is only one case when the id field is not required and that's when the user is first being saved. The way we currently deal with this is with a case class that looks like this:

case class User(id: Option[Int], firstName: String, lastName: String, email:String)

In all cases except the initial save, that id is Some and for the initial save id is always None so we find ourselves using id.getOrElse(0) quite often. (Sometimes we'll do a .get but it feels dirty.)

What I would love to have is an object with an id: Int field for existing users and an object with no id field at all for new users, but without declaring all the other fields twice in two separate case classes. However, I'm not seeing a way to do that conveniently. I'm also not fond of using a 'magic' number for the id field of new users.

Does anyone have a better solution to this issue?

Upvotes: 1

Views: 113

Answers (3)

Tim Gautier
Tim Gautier

Reputation: 30142

This is what we ended up going with for now.

trait Resource[T <: Option[Int]] {
  def idOpt: T
}

object Resource {
  type IsSome = Some[Int]
  implicit class SomeOps[R <: Resource[IsSome]](val resource: R) {
    def id: Int = resource.idOpt.get
  }
}

This allows us to use it like this:

case class User[T <: Option[Int]](idOpt:T, firstName:String, lastName:String, email:String) extends Resource[T]
case class Company[T <: Option[Int]](idOpt:T, companyName: String) extends Resource[T]

val u1 = User(None, "Bubba", "Blue", "[email protected]")
val u2 = User(Some(1), "Forrest", "Gump", "[email protected]")
u1.id // <-- won't compile
u2.id // <-- compiles

Upvotes: 1

Sleiman Jneidi
Sleiman Jneidi

Reputation: 23339

Having a magic number is not a terrible idea if you hide it from the user. in fact it is a common pattern, Slick uses it for example. You can just ignore the id value for the objects to be inserted.

So you can start by making the the constructor package private

 case class User private[db](id: Int, firstName: String, lastName: String, email:String)

And then provide a companion object for users to create it without id

 object User{
   def apply(firstName: String, lastName: String, email: String): User = User(-1, firstName, lastName, email)
}

And now you can construct it as if id wasn't required

val user = User("first","last","email")

Upvotes: 0

PH88
PH88

Reputation: 1806

case class User[+IdOpt <: Option[Int]](idOpt: IdOpt, firstName: String, lastName: String, email:String)
object User {
  // Type aliases for convenience and code readability
  type New = User[None.type]
  type Saved = User[Some[Int]]
  type Value = User[Option[Int]] // New or Saved

  implicit class SavedOps(val user: Saved) extends AnyVal {
    def id: Int = user.idOpt.get
  }
}

Tests:

scala> val billNew = User(None, "Bill", "Gate", "[email protected]")
billNew: User[None.type] = User(None,Bill,Gate,[email protected])

scala> billNew.id 
<console>:17: error: value id is not a member of User[None.type]
       billNew.id
               ^

scala> val billSaved = billNew.copy(idOpt = Some(1))
billSaved: User[Some[Int]] = User(Some(1),Bill,Gate,[email protected])

scala> billSaved.id
res1: Int = 1

Upvotes: 1

Related Questions