com.gu.contentapi.json.CirceDecoders.scala Maven / Gradle / Ivy
package com.gu.contentapi.json
import io.circe._
import cats.data.Xor
import com.gu.contentatom.thrift.{Atom, AtomData, AtomType, ContentChangeDetails}
import com.gu.contentatom.thrift.atom.media.MediaAtom
import com.gu.contentatom.thrift.atom.quiz.QuizAtom
import com.gu.contentatom.thrift.atom.explainer.ExplainerAtom
import com.gu.contentatom.thrift.atom.cta.CTAAtom
import com.gu.contentatom.thrift.atom.interactive.InteractiveAtom
import com.gu.contentapi.circe.CirceScroogeMacros._
import com.gu.contentapi.client.model.v1._
import org.joda.time.format.ISODateTimeFormat
object CirceDecoders {
/**
* We override Circe's provided behaviour so we can emulate json4s's
* "silently convert a Long to a String" behaviour.
*/
implicit final val decodeString: Decoder[String] = new Decoder[String] {
final def apply(c: HCursor): Decoder.Result[String] = {
val focus = c.focus
val fromStringOrLong = focus.asString.orElse(focus.asNumber.flatMap(_.toLong.map(_.toString)))
Xor.fromOption(fromStringOrLong, ifNone = DecodingFailure("String", c.history))
}
}
implicit val dateTimeDecoder = Decoder[String].map { dateTimeString =>
val dateTime = ISODateTimeFormat.dateOptionalTimeParser().withOffsetParsed().parseDateTime(dateTimeString)
CapiDateTime.apply(dateTime.getMillis, dateTime.toString(ISODateTimeFormat.dateTime()))
}
/**
* We override Circe's provided behaviour so we can decode the JSON strings "true" and "false"
* into their corresponding booleans.
*/
implicit final val decodeBoolean: Decoder[Boolean] = new Decoder[Boolean] {
final def apply(c: HCursor): Decoder.Result[Boolean] = {
val focus = c.focus
val fromBooleanOrString = focus.asBoolean.orElse(focus.asString.flatMap {
case "true" => Some(true)
case "false" => Some(false)
case _ => None
})
Xor.fromOption(fromBooleanOrString, ifNone = DecodingFailure("Boolean", c.history))
}
}
implicit val atomDecoder: Decoder[Atom] = Decoder.instance(AtomDecoder.getAtom)
implicit val atomsDecoder: Decoder[Atoms] = Decoder.instance(AtomDecoder.getAtoms)
// The following implicits technically shouldn't be necessary
// but stuff doesn't compile without them
implicit val contentFieldsDecoder = Decoder[ContentFields]
implicit val editionDecoder = Decoder[Edition]
implicit val sponsorshipDecoder = Decoder[Sponsorship]
implicit val tagDecoder = Decoder[Tag]
implicit val assetDecoder = Decoder[Asset]
implicit val elementDecoder = Decoder[Element]
implicit val referenceDecoder = Decoder[Reference]
implicit val blockDecoder = Decoder[Block]
implicit val blocksDecoder = genBlocksDecoder
implicit val rightsDecoder = Decoder[Rights]
implicit val crosswordEntryDecoder = genCrosswordEntryDecoder
implicit val crosswordDecoder = Decoder[Crossword]
implicit val contentStatsDecoder = Decoder[ContentStats]
implicit val sectionDecoder = Decoder[Section]
implicit val debugDecoder = Decoder[Debug]
implicit val contentDecoder = Decoder[Content]
implicit val mostViewedVideoDecoder = Decoder[MostViewedVideo]
implicit val networkFrontDecoder = Decoder[NetworkFront]
implicit val packageDecoder = Decoder[Package]
implicit val itemResponseDecoder = Decoder[ItemResponse]
implicit val searchResponseDecoder = Decoder[SearchResponse]
implicit val editionsResponseDecoder = Decoder[EditionsResponse]
implicit val tagsResponseDecoder = Decoder[TagsResponse]
implicit val sectionsResponseDecoder = Decoder[SectionsResponse]
implicit val atomsResponseDecoder = Decoder[AtomsResponse]
implicit val packagesResponseDecoder = Decoder[PackagesResponse]
implicit val errorResponseDecoder = Decoder[ErrorResponse]
implicit val videoStatsResponseDecoder = Decoder[VideoStatsResponse]
implicit val atomsUsageResponseDecoder = Decoder[AtomUsageResponse]
implicit val removedContentResponseDecoder = Decoder[RemovedContentResponse]
// These two need to be written manually. I think the `Map[K, V]` type having 2 type params causes implicit divergence,
// although shapeless's Lazy is supposed to work around that.
def genBlocksDecoder(implicit blockDecoder: Decoder[Block]): Decoder[Blocks] = Decoder.instance[Blocks] { cursor =>
for {
main <- cursor.get[Option[Block]]("main")
body <- cursor.get[Option[Seq[Block]]]("body")
totalBodyBlocks <- cursor.get[Option[Int]]("totalBodyBlocks")
requestedBodyBlocks <- cursor.get[Option[Map[String, Seq[Block]]]]("requestedBodyBlocks")
} yield Blocks(main, body, totalBodyBlocks, requestedBodyBlocks)
}
def genCrosswordEntryDecoder(implicit dec: Decoder[Option[Map[String,Seq[Int]]]]): Decoder[CrosswordEntry] = Decoder.instance[CrosswordEntry] { cursor =>
for {
id <- cursor.get[String]("id")
number <- cursor.get[Option[Int]]("number")
humanNumber <- cursor.get[Option[String]]("humanNumber")
direction <- cursor.get[Option[String]]("direction")
position <- cursor.get[Option[CrosswordPosition]]("position")
separatorLocations <- cursor.get[Option[Map[String, Seq[Int]]]]("separatorLocations")
length <- cursor.get[Option[Int]]("length")
clue <- cursor.get[Option[String]]("clue")
group <- cursor.get[Option[Seq[String]]]("group")
solution <- cursor.get[Option[String]]("solution")
format <- cursor.get[Option[String]]("format")
} yield CrosswordEntry(
id,
number,
humanNumber,
direction,
position,
separatorLocations,
length,
clue,
group,
solution,
format
)
}
object AtomDecoder {
implicit val decodeUnknownOpt: Decoder[AtomData.UnknownUnionField] =
Decoder.instance(c =>
Xor.left(DecodingFailure("AtomData.UnknownUnionField", c.history))
)
def getAtom(c: HCursor): Decoder.Result[Atom] = {
for {
atomType <- c.get[AtomType]("atomType")
atomData <- getAtomData(c, atomType)
atom <- getAtom(c, atomData)
} yield atom
}
def getAtoms(c: HCursor): Decoder.Result[Atoms] = {
for {
quizzes <- getAtoms(c, AtomType.Quiz)
media <- getAtoms(c, AtomType.Media)
explainers <- getAtoms(c, AtomType.Explainer)
cta <- getAtoms(c, AtomType.Cta)
interactives <- getAtoms(c, AtomType.Interactive)
} yield {
Atoms(quizzes = quizzes, media = media, explainers = explainers, cta = cta, interactives = interactives)
}
}
private def getAtom(c: HCursor, atomData: AtomData): Decoder.Result[Atom] = {
for {
id <- c.get[String]("id")
atomType <- c.get[AtomType]("atomType")
labels <- c.get[Seq[String]]("labels")
defaultHtml <- c.get[String]("defaultHtml")
change <- c.get[ContentChangeDetails]("contentChangeDetails")
} yield {
Atom(id, atomType, labels, defaultHtml, atomData, change)
}
}
private def getAtomData(c: HCursor, atomType: AtomType): Decoder.Result[AtomData] = {
atomType match {
case AtomType.Quiz => c.downField("data").get[QuizAtom]("quiz").map(atom => AtomData.Quiz(atom))
case AtomType.Media => c.downField("data").get[MediaAtom]("media").map(atom => AtomData.Media(atom))
case AtomType.Explainer => c.downField("data").get[ExplainerAtom]("explainer").map(atom => AtomData.Explainer(atom))
case AtomType.Cta => c.downField("data").get[CTAAtom]("cta").map(atom => AtomData.Cta(atom))
case AtomType.Interactive => c.downField("data").get[InteractiveAtom]("interactive").map(json => AtomData.Interactive(json))
case _ => Xor.left(DecodingFailure("AtomData", c.history))
}
}
private def getAtomTypeFieldName(atomType: AtomType): Option[String] = {
atomType match {
case AtomType.Quiz => Some("quizzes")
case AtomType.Media => Some("media")
case AtomType.Explainer => Some("explainers")
case AtomType.Cta => Some("cta")
case AtomType.Interactive => Some("interactives")
case _ => None
}
}
private def getAtoms(c: HCursor, atomType: AtomType): Decoder.Result[Option[Seq[Atom]]] = {
getAtomTypeFieldName(atomType) match {
case Some(fieldName) => c.get[Option[Seq[Atom]]](fieldName)
case None => Xor.right(None) //ignore unrecognised atom types
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy