io.cequence.wsclient.JsonUtil.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ws-client-core_2.12 Show documentation
Show all versions of ws-client-core_2.12 Show documentation
Generic WebServices library currently only with Play WS impl./backend
package io.cequence.wsclient
import io.cequence.wsclient.domain.CequenceWSException
import play.api.libs.json.JsonNaming.SnakeCase
import play.api.libs.json._
import scala.collection.mutable.{Seq => MutableSeq}
import java.util.Date
import java.{util => ju}
object JsonUtil {
implicit class JsonOps(val json: JsValue) {
def asSafe[T](
implicit fjs: Reads[T]
): T =
try {
json.validate[T] match {
case JsSuccess(value, _) => value
case JsError(errors) =>
val errorString = errors.map { case (path, pathErrors) =>
s"JSON at path '${path}' contains the following errors: ${pathErrors.map(_.message).mkString(";")}"
}.mkString("\n")
throw new CequenceWSException(
s"Unexpected JSON:\n'${Json.prettyPrint(json)}'. Cannot be parsed due to: $errorString"
)
}
} catch {
case e: Exception =>
throw new CequenceWSException(
s"Error thrown while processing a JSON '${Json.prettyPrint(json)}'. Cause: ${e.getMessage}"
)
}
def asSafeArray[T](
implicit fjs: Reads[T]
): Seq[T] =
json.asSafe[JsArray].value.map(_.asSafe[T]).toSeq
}
object SecDateFormat extends Format[ju.Date] {
override def reads(json: JsValue): JsResult[Date] = {
json match {
case JsString(s) =>
try {
val millis = s.toLong * 1000
JsSuccess(new ju.Date(millis))
} catch {
case _: NumberFormatException => JsError(s"$s is not a number.")
}
case JsNumber(n) =>
val millis = (n * 1000).toLong
JsSuccess(new ju.Date(millis))
case _ => JsError(s"String or number expected but got '$json'.")
}
}
override def writes(o: Date): JsValue =
JsNumber(Math.round(o.getTime.toDouble / 1000))
}
def toJson(value: Any): JsValue =
if (value == null)
JsNull
else
value match {
case x: JsValue => x // nothing to do
case x: String => JsString(x)
case x: BigDecimal => JsNumber(x)
case x: Integer => JsNumber(BigDecimal.valueOf(x.toLong))
case x: Long => JsNumber(BigDecimal.valueOf(x))
case x: Double => JsNumber(BigDecimal.valueOf(x))
case x: Float => JsNumber(BigDecimal.valueOf(x.toDouble))
case x: Boolean => JsBoolean(x)
case x: ju.Date => Json.toJson(x)
case x: Option[_] => x.map(toJson).getOrElse(JsNull)
case x: Array[_] => JsArray(x.map(toJson))
case x: Seq[_] => JsArray(x.map(toJson))
case x: MutableSeq[_] => JsArray(x.map(toJson))
case x: Map[String, _] =>
val jsonValues = x.map { case (fieldName, value) =>
(fieldName, toJson(value))
}
JsObject(jsonValues)
case _ =>
throw new IllegalArgumentException(
s"No JSON formatter found for the class ${value.getClass.getName}."
)
}
def toValue(jsValue: JsValue): Option[Any] =
jsValue match {
case JsNull => None
case JsString(value) => Some(value)
case JsNumber(value) => Some(value)
case JsBoolean(value) => Some(value)
case JsArray(value) =>
val seq = value.toSeq.map(toValue)
Some(seq)
case jsObject: JsObject =>
Some(toValueMap(jsObject))
}
def toValueMap(jsObject: JsObject): Map[String, Option[Any]] =
jsObject.value.map { case (fieldName, jsValue) =>
(fieldName, toValue(jsValue))
}.toMap
object StringDoubleMapFormat extends Format[Map[String, Double]] {
override def reads(json: JsValue): JsResult[Map[String, Double]] = {
val resultJsons =
json.asSafe[JsObject].fields.map { case (fieldName, jsValue) =>
(fieldName, jsValue.as[Double])
}
JsSuccess(resultJsons.toMap)
}
override def writes(o: Map[String, Double]): JsValue = {
val fields = o.map { case (fieldName, value) =>
(fieldName, JsNumber(value))
}
JsObject(fields)
}
}
object StringStringMapFormat extends Format[Map[String, String]] {
override def reads(json: JsValue): JsResult[Map[String, String]] = {
val resultJsons =
json.asSafe[JsObject].fields.map { case (fieldName, jsValue) =>
(fieldName, jsValue.as[String])
}
JsSuccess(resultJsons.toMap)
}
override def writes(o: Map[String, String]): JsValue = {
val fields = o.map { case (fieldName, value) =>
(fieldName, JsString(value))
}
JsObject(fields)
}
}
object StringAnyMapFormat extends Format[Map[String, Any]] {
override def reads(json: JsValue): JsResult[Map[String, Any]] = {
val resultJsons =
json.asSafe[JsObject].fields.map { case (fieldName, jsValue) =>
val stringValue = jsValue match {
case JsString(s) => s
case _ => jsValue.toString()
}
(fieldName, stringValue)
}
JsSuccess(resultJsons.toMap)
}
override def writes(o: Map[String, Any]): JsValue = {
val fields = o.map { case (fieldName, value) =>
(fieldName, toJson(value))
}
JsObject(fields)
}
}
private class EitherFormat[L, R](
leftFormat: Format[L],
rightFormat: Format[R]
) extends Format[Either[L, R]] {
override def reads(json: JsValue): JsResult[Either[L, R]] = {
val left = leftFormat.reads(json)
val right = rightFormat.reads(json)
if (left.isSuccess) {
left.map(Left(_))
} else if (right.isSuccess) {
right.map(Right(_))
} else {
JsError(s"Unable to read Either type from JSON $json")
}
}
override def writes(o: Either[L, R]): JsValue =
o match {
case Left(value) => leftFormat.writes(value)
case Right(value) => rightFormat.writes(value)
}
}
def eitherFormat[L: Format, R: Format]: Format[Either[L, R]] = {
val leftFormat = implicitly[Format[L]]
val rightFormat = implicitly[Format[R]]
new EitherFormat[L, R](leftFormat, rightFormat)
}
private def enumFormatAux[T](values: T*)(mapper: T => String): Format[T] = {
val valueMap = values.map(v => mapper(v) -> v).toMap
val reads: Reads[T] = Reads {
case JsString(value) =>
valueMap.get(value) match {
case Some(v) => JsSuccess(v)
case None => JsError(s"$value is not a valid enum value.")
}
case _ => JsError("String value expected")
}
val writes: Writes[T] = Writes((v: T) => JsString(mapper(v)))
Format(reads, writes)
}
def enumFormat[T](values: T*): Format[T] =
enumFormatAux(values: _*)(_.toString)
def snakeEnumFormat[T](values: T*): Format[T] =
enumFormatAux(values: _*)(v => SnakeCase(v.toString))
}