scalaakkaakka-streamakka-http

Scala Akka HTTP Actor Class protocol Type Mismatch Error


I'm building an Akka HTTP application using scala 3.3 and Akka actor.

When a star, planet or moon is created in the StarRegistry object, I would like it's value to be temporarily stored in the registry function's map variable map: Map[String, Set[HeavenlyBody]].

The map should store the values like below, each parameter with its own type as described:


"stars" -> Set[CreateStar], 
"planets" -> Set[CreatePlanet], 
"moons" -> Set[CreateMoon], 

In the starRoutes script are the APIs.

The stars API route is used to create a new star and also to GET all stars stored in the storage map above.

When the stars GET API route is fetched, I want the api to fetch all the stars stored in the stars field from the registry function's map above.

The description of the star functionalities are similar to that of the moon and planet functionalities.

The current code is giving the error:


Type Mismatch Error: D:\Studio\WORK\akka\AKKA-HTTP\scala-akka-http-interstellar-IoT\src\main\scala\com\example\StarRegistry.scala:
replyTo ! Stars(map.get("stars").toSeq)
Found:    Seq[Set[com.example.HeavenlyBody]]
Required: Seq[com.example.CreateStar]

Type Mismatch Error: D:\Studio\WORK\akka\AKKA-HTTP\scala-akka-http-interstellar-IoT\src\main\scala\com\example\StarRegistry.scala:199:45 
replyTo ! Planets(map.get("planets").toSeq)
Found:    Seq[Set[com.example.HeavenlyBody]]
Required: Seq[com.example.CreatePlanet]

Type Mismatch Error: D:\Studio\WORK\akka\AKKA-HTTP\scala-akka-http-interstellar-IoT\src\main\scala\com\example\StarRegistry.scala:213:41 
replyTo ! Moons(map.get("moons").toSeq)
Found:    Seq[com.example.HeavenlyBody]
Required: Seq[com.example.CreateMoon]

My current code looks like below.

Thanks in advance for your help!

StarRegistry.scala:


sealed trait HeavenlyBody

final case class CreateStar(id: String, name: String, location: String, size: Option[Size]) extends HeavenlyBody
final case class CreatePlanet(id: String, name: String, location: String, size: Option[Size]) extends HeavenlyBody
final case class CreateMoon(id: String, name: String, location: String, size: Option[Size]) extends HeavenlyBody

final case class Stars(stars: immutable.Seq[CreateStar])
final case class Planets(planets: immutable.Seq[CreatePlanet]) 
final case class Moons(moons: immutable.Seq[CreateMoon]) 

final case class Size(radius: Long)

object StarRegistry {

  // actor protocol
  sealed trait Command
  final case class GetStars(replyTo: ActorRef[Stars]) extends Command
  final case class NewStar(star: CreateStar, replyTo: ActorRef[ActionPerformed]) extends Command

  final case class GetPlanets(replyTo: ActorRef[Planets]) extends Command
  final case class NewPlanet(planet: CreatePlanet, replyTo: ActorRef[ActionPerformed]) extends Command

  final case class GetMoons(replyTo: ActorRef[Moons]) extends Command
  final case class NewMoon(moon: CreateMoon, replyTo: ActorRef[ActionPerformed]) extends Command

  final case class ActionPerformed(description: String)

  def apply(): Behavior[Command] = registry(Map("stars" -> Set.empty, "planets" -> Set.empty, "moons" -> Set.empty))

  private def registry(map: Map[String, Set[HeavenlyBody]]): Behavior[Command] =
    Behaviors.receiveMessage {

      case GetStars(replyTo) =>
        replyTo ! Stars(map.get("stars").toSeq)
        Behaviors.same
      case NewStar(star, replyTo) =>
        replyTo ! ActionPerformed(s"Star '${star.name}' created!")
        registry(map("stars") += star)

      case GetPlanets(replyTo) =>
        replyTo ! Planets(map.get("planets").toSeq)
        Behaviors.same
      case NewPlanet(planet, replyTo) =>
        replyTo ! ActionPerformed(s"Planet '${planet.name}' created!")
        registry(map("planets") += planet)

      case GetMoons(replyTo) =>
        replyTo ! Moons(map.get("moons").toSeq)
        Behaviors.same
      case NewMoon(moon, replyTo) =>
        replyTo ! ActionPerformed(s"Moon '${moon.name}' created!")
        registry(map("moons") += moon)

    }
}

jsonFormats.scala:


...
...

object JsonFormats  {
  import DefaultJsonProtocol._

  implicit val sizeJsonFormat: RootJsonFormat[Size] = jsonFormat1(Size.apply)
  implicit val createStarJsonFormat: RootJsonFormat[CreateStar] = jsonFormat4(CreateStar.apply)
  implicit val createPlanetJsonFormat: RootJsonFormat[CreatePlanet] = jsonFormat4(CreatePlanet.apply)
  implicit val createMoonJsonFormat: RootJsonFormat[CreateMoon] = jsonFormat4(CreateMoon.apply)
  implicit val starsJsonFormat: RootJsonFormat[Stars] = jsonFormat1(Stars.apply)
  implicit val planetsJsonFormat: RootJsonFormat[Planets] = jsonFormat1(Planets.apply)
  implicit val moonsJsonFormat: RootJsonFormat[Moons] = jsonFormat1(Moons.apply) 
  implicit val actionPerformedJsonFormat: RootJsonFormat[ActionPerformed]  = jsonFormat1(ActionPerformed.apply)
}

starRoutes.scala:


class StarRoutes(starRegistry: ActorRef[StarRegistry.Command])(implicit val system: ActorSystem[_]) {

  import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
  import JsonFormats._ //#import-json-formats

  def getStars(): Future[Stars] =
    starRegistry.ask(GetStars.apply)
  def createStar(star: CreateStar): Future[ActionPerformed] =
    starRegistry.ask(NewStar(star, _))

  def getPlanets(): Future[Planets] =
    starRegistry.ask(GetPlanets.apply)
  def createPlanet(planet: CreatePlanet): Future[ActionPerformed] =
    starRegistry.ask(NewPlanet(planet, _))

  def getMoons(): Future[Moons] =
    starRegistry.ask(GetMoons.apply)
  def getMoon(name: String): Future[GetMoonResponse] =
    starRegistry.ask(GetMoon(name, _))

  val starRoutes: Route =
    ...
    pathPrefix("stars") {
      concat(
        pathEnd {
          concat(
            get {
              complete(getStars())
            },
            post {
              entity(as[CreateStar]) { star =>
                onSuccess(createStar(star)) { performed =>
                  complete((StatusCodes.Created, performed))
                }
              }
            })
        },
    }
    ...
    pathPrefix("planets") {
      concat(
        pathEnd {
          concat(
            get {
              complete(getPlanets())
            },
            post {
              entity(as[CreatePlanet]) { planet =>
                onSuccess(createPlanet(planet)) { performed =>
                  complete((StatusCodes.Created, performed))
                }
              }
            })
    },
    ...
    pathPrefix("moons") {
      concat(
        pathEnd {
          concat(
            get {
              complete(getMoons())
            },
            post {
              entity(as[CreateMoon]) { moon =>
                onSuccess(createMoon(moon)) { performed =>
                  complete((StatusCodes.Created, performed))
                }
              }
            })
    }


Solution

  • private def registry(map: Map[String, Set[HeavenlyBody]]): Behavior[Command] =
      Behaviors.receiveMessage {
        case GetStars(replyTo) =>
          replyTo ! Stars(map.get("stars").toSeq)
    

    You get the error because map.get(...).toSeq returns Seq[HeavenlyBody] and you want Seq[CreateStar]. You may think that all the elements of the Seq are CreateStar but the compiler can't tell.

    The solution is to use collect to constrain the type:

    replyTo ! Stars(map.get("stars").toSeq.collect{case x: CreateStar => x})
    

    This will discard any elements that are not CreateStar and return Seq[CreateStar] as required.

    You also might want to consider using enum HeavenlyBody rather than a sealed trait.