Конечная точка, получающая список объектов из иерархии

Конечная точка в Akka Http выглядит так:

pathPrefix("somePath" / Segment) { someData =>
  post {
    entity(as[SMS]) { sms =>
      // some code here ...
      complete(StatusCodes.OK)
    }
  }
}

А СМС определяется как:

sealed trait Message
case class SMS(numFrom: String, message:String) extends Message
case class Email(emailFrom: String, message: String) extends Message

Если я хочу получить список SMS, я могу сделать следующее:

type SMSList = List[SMS]
...

pathPrefix("somePath" / Segment) { someData =>
  post {
    entity(as[SMSList]) { listOfSMSs =>
      // some code here ...
      complete(StatusCodes.OK)
    }
  }
}

Что делать, если я хочу получать список SMS и электронных писем одновременно? Я пробовал это, и это не сработало:

type MessageList = List[Message]

pathPrefix("somePath" / Segment) { someData =>
  post {
    entity(as[MessageList]) { listOfMessages =>
      // some code here ...
      complete(StatusCodes.OK)
    }
  }
}

Можно ли получить список объектов, принадлежащих к одной иерархии?

Библиотеки:

circe = 0.13.0
heikoseeberger = 1.35.3
akka http = 10.2.3

Json:

[ 
  {"numForm": "123 456", "message": "sms message"},
  {"emailFrom": "[email protected]", "email message"}
]

person M.G.    schedule 08.02.2021    source источник
comment
К сожалению, я забыл упомянуть библиотеки: circe (0.13.0) и heikoseeberger (1.35.3), akka http (10.2.3)   -  person M.G.    schedule 08.02.2021
comment
Вам это помогает? stackoverflow.com/q/50457466/2359227   -  person Tomer Shetah    schedule 08.02.2021
comment
Спасибо, Томер, но я думаю, что это другая проблема. В вашем коде вы видите { \n Something: ... }. Это другой ввод, у меня нет строки Something. Я обновил текст с помощью json вызова. Поправьте меня если я ошибаюсь   -  person M.G.    schedule 09.02.2021


Ответы (2)


Предполагая, что вы используете одну библиотеку сериализации json по умолчанию в akka-http — spray-json — вы весьма ограничены в объединении нескольких программ чтения json (согласно официальная страница и исходный код). Лучшее, что вы можете сделать, это написать вручную какой-нибудь форматтер (или просто ридер) для Message.

import spray.json.DefaultJsonProtocol._
import spray.json._

implicit val smsFormat: JsonFormat[SMS] = jsonFormat2(SMS)
implicit val emailFormat: JsonFormat[Email] = jsonFormat2(Email)

implicit val messageFormat: JsonFormat[Message] = new JsonFormat[Message] {
  override def read(json: JsValue): Message = json match {
    case sms@JsObject(_) if sms.fields.contains("numFrom") => smsFormat.read(sms)
    case email@JsObject(_) if email.fields.contains("emailFrom") => emailFormat.read(email)
    case _ => deserializationError("object expected")
  }

  override def write(obj: Message): JsValue = obj match {
    case sms: SMS => sms.toJson
    case email: Email => email.toJson
    case _ => throw new RuntimeException("Houston, we have a problem")
  }
}

Я бы также посоветовал вам взглянуть на библиотеку circe, чей код гораздо более компонуемый. Его также легко интегрировать с akka-http.


Обновление 1: (после указания точной библиотеки):

Есть несколько вариантов:

  1. объединить несколько декодеров

      import io.circe.generic.auto._
      import io.circe.{Decoder, HCursor}
    
      class CirceExample extends App {
    
        sealed trait Message
    
        case class SMS(numFrom: String, message: String) extends Message
    
        case class Email(emailFrom: String, message: String) extends Message
    
        val smsDecoder = implicitly[Decoder[SMS]]
        val emailDecoder = implicitly[Decoder[Email]]
    
        val messageDecoder: Decoder[Message] = (c: HCursor) => smsDecoder(c).orElse(emailDecoder(c))
      }
    

    Этого легко добиться, потому что результатом декодирования Decoder будет Either.

  2. Создайте собственный декодер, который явно проверяет наличие необходимых полей — документация. Этот подход немного похож на предыдущий пример с spray-json.


обновление 2:

import cats.syntax.functor._
import io.circe.Decoder
import io.circe.generic.auto._
import io.circe.parser._

object CirceExample extends App {

  sealed trait Message

  case class SMS(numFrom: String, message: String) extends Message

  case class Email(emailFrom: String, message: String) extends Message

  implicit val messageDecoder: Decoder[Message] = List[Decoder[Message]](Decoder[SMS].widen, Decoder[Email].widen).reduceLeft(_ or _)
  // or without list..
  //implicit val messageDecoder: Decoder[Message] = Decoder[SMS].widen or Decoder[Email].widen

  val payload = """[{"emailFrom":"a","message":"b"}]"""
  val result = decode[List[Message]](payload)
  println(result)
}
person Serhii Shynkarenko    schedule 08.02.2021
comment
Я забыл упомянуть, какие библиотеки я использую, и на самом деле я использую circe. - person M.G.; 08.02.2021
comment
Я не мог заставить это работать. Код адаптировал под свой пример, создаю объект Message с энкодерами внутри, импортировал, но не заработало... - person M.G.; 09.02.2021
comment
Это должно работать, потому что декодеры в circe компонуемые. То есть, если вы объявите List[Message], circe возьмет существующий предопределенный декодер для списка и будет искать неявный декодер для Message (в качестве альтернативы вы можете явно указать все декодеры). Вы должны проверить, каково разрешение декодеров. В Intellij Idea IDE это можно сделать через меню Вид -> Показать неявные подсказки. Было бы полезно, если бы вы разместили ошибку здесь. - person Serhii Shynkarenko; 09.02.2021
comment
@ М.Г. Хорошо, вы правы, это не работает. Подробности об этом (и на самом деле ваш случай здесь stackoverflow.com/questions/42165460/). Добавлено обновление о том, как это должно выглядеть. Будьте осторожны, чтобы явно указать, какие декодеры вы будете использовать. - person Serhii Shynkarenko; 09.02.2021
comment
О, второе обновление сработало как шарм!! Большое спасибо !! - person M.G.; 09.02.2021

Здесь есть несколько шагов, поэтому я постараюсь не усложнять.

Во-первых, нам нужно определить кодировщики и декодеры для Email и n SMS:

sealed trait Message
@JsonCodec case class SMS(numFrom: String, message:String) extends Message
@JsonCodec case class Email(emailFrom: String, message: String) extends Message

object Message {
  implicit val decodeA: Decoder[Message] = Decoder[SMS].map[Message](identity).or(Decoder[Email].map[Message](identity))
  implicit val encodeA: Encoder[Message] = Encoder.instance {
    case b @ SMS(_, _) => b.asJson
    case c @ Email(_, _) => c.asJson
  }
}

Я нашел это в этой ссылке, после всех предыдущих попыток с этой нить не работала.

Продолжим. Мы должны найти это в отдельном модуле. В противном случае получаем enable macro paradise to expand macro annotations ошибку. После прочтения этого поста, в котором в основном объясняется, что макросы нельзя объявлять и использовать в одном и том же модуле, я переместил указанное выше код в другой модуль, который заставил код компилироваться.

Имея отдельный модуль с приведенным выше кодом, нам нужно определить новый модуль, чтобы он зависел от первого.

Теперь в последнем модуле нам нужно сначала импортировать:

import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport.unmarshaller

то мы можем создать маршрут:

val routes = pathPrefix("somePath" / Segment) { someData =>
  post {
    entity(as[List[Message]]) { listOfMessages =>
      // some code here ...
      complete(StatusCodes.OK)
    }
  }
}
person Tomer Shetah    schedule 08.02.2021
comment
Я попробовал ваш код, но он не скомпилировался, мне нужно определить неявные кодеки для классов случаев SMS и электронной почты. Тогда он работал нормально, но принимал только один объект, он не работает с сущностью (как [Список [Сообщение]]) { .... Кроме того, это хороший код - person M.G.; 12.02.2021