common/app/model/Formats.scala (351 lines of code) (raw):
package model
import com.gu.contentapi.client.utils.DesignType
import com.gu.facia.api.models.{GroupConfig, GroupsConfig}
import com.gu.facia.api.utils.BoostLevel
import common.Pagination
import json.ObjectDeduplication.deduplicate
import model.content._
import model.facia.PressedCollection
import model.pressed._
import play.api
import play.api.libs
import play.api.libs.json
import play.api.libs.json._
import play.api.libs.json.JodaReads._
import scala.concurrent.duration.DurationInt
object GenericThriftAtomFormat extends Format[com.gu.contentatom.thrift.Atom] {
def reads(json: JsValue): JsError = JsError("Converting from Json is not supported by intent!")
def writes(atom: com.gu.contentatom.thrift.Atom): JsObject = JsObject(Seq.empty)
}
object ReviewThriftAtomFormat extends Format[com.gu.contentatom.thrift.atom.review.ReviewAtom] {
def reads(json: JsValue): JsError = JsError("Converting from Json is not supported by intent!")
def writes(review: com.gu.contentatom.thrift.atom.review.ReviewAtom): JsObject = JsObject(Seq.empty)
}
object ExplainerThriftAtomFormat extends Format[com.gu.contentatom.thrift.atom.explainer.ExplainerAtom] {
def reads(json: JsValue): JsError = JsError("Converting from Json is not supported by intent!")
def writes(explainer: com.gu.contentatom.thrift.atom.explainer.ExplainerAtom): JsObject = JsObject(Seq.empty)
}
object QandasThriftAtomFormat extends Format[com.gu.contentatom.thrift.atom.qanda.QAndAAtom] {
def reads(json: JsValue): JsError = JsError("Converting from Json is not supported by intent!")
def writes(qanda: com.gu.contentatom.thrift.atom.qanda.QAndAAtom): JsObject = JsObject(Seq.empty)
}
object GuidesThriftAtomFormat extends Format[com.gu.contentatom.thrift.atom.guide.GuideAtom] {
def reads(json: JsValue): JsError = JsError("Converting from Json is not supported by intent!")
def writes(guide: com.gu.contentatom.thrift.atom.guide.GuideAtom): JsObject = JsObject(Seq.empty)
}
object ProfilesThriftAtomFormat extends Format[com.gu.contentatom.thrift.atom.profile.ProfileAtom] {
def reads(json: JsValue): JsError = JsError("Converting from Json is not supported by intent!")
def writes(profile: com.gu.contentatom.thrift.atom.profile.ProfileAtom): JsObject = JsObject(Seq.empty)
}
object TimelinesThriftAtomFormat extends Format[com.gu.contentatom.thrift.atom.timeline.TimelineAtom] {
def reads(json: JsValue): JsError = JsError("Converting from Json is not supported by intent!")
def writes(timeline: com.gu.contentatom.thrift.atom.timeline.TimelineAtom): JsObject = JsObject(Seq.empty)
}
object BoostLevelFormat extends Format[BoostLevel] {
def reads(json: JsValue): JsResult[BoostLevel] = {
json match {
case JsString("default") => JsSuccess(BoostLevel.Default)
case JsString("boost") => JsSuccess(BoostLevel.Boost)
case JsString("megaboost") => JsSuccess(BoostLevel.MegaBoost)
case JsString("gigaboost") => JsSuccess(BoostLevel.GigaBoost)
case _ => JsError("Could not convert BoostLevel")
}
}
def writes(boostLevel: BoostLevel): JsValue = {
boostLevel match {
case BoostLevel.Default => JsString("default")
case BoostLevel.Boost => JsString("boost")
case BoostLevel.MegaBoost => JsString("megaboost")
case BoostLevel.GigaBoost => JsString("gigaboost")
}
}
}
object CardStyleFormat extends Format[CardStyle] {
def reads(json: JsValue): JsResult[CardStyle] = {
(json \ "type").transform[JsString](Reads.JsStringReads) match {
case JsSuccess(JsString("SpecialReport"), _) => JsSuccess(SpecialReport)
case JsSuccess(JsString("SpecialReportAlt"), _) => JsSuccess(SpecialReportAlt)
case JsSuccess(JsString("LiveBlog"), _) => JsSuccess(LiveBlog)
case JsSuccess(JsString("DeadBlog"), _) => JsSuccess(DeadBlog)
case JsSuccess(JsString("Feature"), _) => JsSuccess(Feature)
case JsSuccess(JsString("Editorial"), _) => JsSuccess(Editorial)
case JsSuccess(JsString("Comment"), _) => JsSuccess(Comment)
case JsSuccess(JsString("Media"), _) => JsSuccess(Media)
case JsSuccess(JsString("Analysis"), _) => JsSuccess(Analysis)
case JsSuccess(JsString("Review"), _) => JsSuccess(Review)
case JsSuccess(JsString("Letters"), _) => JsSuccess(Letters)
case JsSuccess(JsString("ExternalLink"), _) => JsSuccess(ExternalLink)
case JsSuccess(JsString("DefaultCardstyle"), _) => JsSuccess(DefaultCardstyle)
case _ => JsError("Could not convert CardStyle")
}
}
def writes(cardStyle: CardStyle): JsObject =
cardStyle match {
case SpecialReport => JsObject(Seq("type" -> JsString("SpecialReport")))
case SpecialReportAlt => JsObject(Seq("type" -> JsString("SpecialReportAlt")))
case LiveBlog => JsObject(Seq("type" -> JsString("LiveBlog")))
case DeadBlog => JsObject(Seq("type" -> JsString("DeadBlog")))
case Feature => JsObject(Seq("type" -> JsString("Feature")))
case Editorial => JsObject(Seq("type" -> JsString("Editorial")))
case Comment => JsObject(Seq("type" -> JsString("Comment")))
case Media => JsObject(Seq("type" -> JsString("Media")))
case Analysis => JsObject(Seq("type" -> JsString("Analysis")))
case Review => JsObject(Seq("type" -> JsString("Review")))
case Letters => JsObject(Seq("type" -> JsString("Letters")))
case ExternalLink => JsObject(Seq("type" -> JsString("ExternalLink")))
case DefaultCardstyle => JsObject(Seq("type" -> JsString("DefaultCardstyle")))
}
}
object MediaTypeFormat extends Format[MediaType] {
def reads(json: JsValue): JsResult[MediaType] = {
(json \ "type").transform[JsString](Reads.JsStringReads) match {
case JsSuccess(JsString("Video"), _) => JsSuccess(pressed.Video)
case JsSuccess(JsString("Gallery"), _) => JsSuccess(pressed.Gallery)
case JsSuccess(JsString("Audio"), _) => JsSuccess(pressed.Audio)
case _ => JsError("Could not convert MediaType")
}
}
def writes(mediaType: MediaType): JsObject =
mediaType match {
case pressed.Video => JsObject(Seq("type" -> JsString("Video")))
case pressed.Gallery => JsObject(Seq("type" -> JsString("Gallery")))
case pressed.Audio => JsObject(Seq("type" -> JsString("Audio")))
}
}
object PressedContentFormat {
// This format is implicit because CuratedContent is recursively defined, so it needs a format object in scope.
implicit object format extends Format[PressedContent] {
def reads(json: JsValue): JsResult[PressedContent] = {
(json \ "type").transform[JsString](Reads.JsStringReads) match {
case JsSuccess(JsString("LinkSnap"), _) => {
// we know a Link Snap will never have Format but this is a required field in DCR so we add a default here.
val snapLinkJsonWithFormat = json.as[JsObject] + ("format" -> Json.toJson(ContentFormat.defaultContentFormat))
JsSuccess(snapLinkJsonWithFormat.as[LinkSnap](linkSnapFormat))
}
case JsSuccess(JsString("LatestSnap"), _) => JsSuccess(json.as[LatestSnap](latestSnapFormat))
case JsSuccess(JsString("CuratedContent"), _) => JsSuccess(json.as[CuratedContent](curatedContentFormat))
case JsSuccess(JsString("SupportingCuratedContent"), _) =>
JsSuccess(json.as[SupportingCuratedContent](supportingCuratedContentFormat))
case _ => JsError("Could not convert PressedContent")
}
}
def writes(faciaContent: PressedContent): JsValue =
faciaContent match {
case linkSnap: LinkSnap =>
Json
.toJson(linkSnap)(linkSnapFormat)
.transform[JsObject](Reads.JsObjectReads) match {
case JsSuccess(l, _) =>
l ++ Json.obj("type" -> "LinkSnap")
case JsError(_) => JsNull
}
case latestSnap: LatestSnap =>
Json
.toJson(latestSnap)(latestSnapFormat)
.transform[JsObject](Reads.JsObjectReads) match {
case JsSuccess(l, _) =>
l ++ Json.obj("type" -> "LatestSnap")
case JsError(_) => JsNull
}
case content: CuratedContent =>
Json
.toJson(content)(curatedContentFormat)
.transform[JsObject](Reads.JsObjectReads) match {
case JsSuccess(l, _) =>
l ++ Json.obj("type" -> "CuratedContent")
case JsError(_) => JsNull
}
case supporting: SupportingCuratedContent =>
Json
.toJson(supporting)(supportingCuratedContentFormat)
.transform[JsObject](Reads.JsObjectReads) match {
case JsSuccess(l, _) =>
l ++ Json.obj("type" -> "SupportingCuratedContent")
case JsError(_) => JsNull
}
case _ => JsNull
}
}
implicit val designTypeFormat: Format[DesignType] = new Format[DesignType] {
override def reads(json: JsValue): JsResult[DesignType] =
json match {
case JsString("Article") => JsSuccess(com.gu.contentapi.client.utils.Article)
case JsString("Immersive") => JsSuccess(com.gu.contentapi.client.utils.Immersive)
case JsString("Media") => JsSuccess(com.gu.contentapi.client.utils.Media)
case JsString("Review") => JsSuccess(com.gu.contentapi.client.utils.Review)
case JsString("Analysis") => JsSuccess(com.gu.contentapi.client.utils.Analysis)
case JsString("Comment") => JsSuccess(com.gu.contentapi.client.utils.Comment)
case JsString("Feature") => JsSuccess(com.gu.contentapi.client.utils.Feature)
case JsString("Live") => JsSuccess(com.gu.contentapi.client.utils.Live)
case JsString("SpecialReport") => JsSuccess(com.gu.contentapi.client.utils.SpecialReport)
case JsString("Recipe") => JsSuccess(com.gu.contentapi.client.utils.Recipe)
case JsString("MatchReport") => JsSuccess(com.gu.contentapi.client.utils.MatchReport)
case JsString("Interview") => JsSuccess(com.gu.contentapi.client.utils.Interview)
case JsString("GuardianView") => JsSuccess(com.gu.contentapi.client.utils.GuardianView)
case JsString("Quiz") => JsSuccess(com.gu.contentapi.client.utils.Quiz)
case JsString("GuardianLabs") => JsSuccess(com.gu.contentapi.client.utils.GuardianLabs)
case JsString("AdvertisementFeature") => JsSuccess(com.gu.contentapi.client.utils.AdvertisementFeature)
case JsString("Newsletter") => JsSuccess(com.gu.contentapi.client.utils.Newsletter)
case JsString("Profile") => JsSuccess(com.gu.contentapi.client.utils.Profile)
case JsString("Timeline") => JsSuccess(com.gu.contentapi.client.utils.Timeline)
case _ => JsError(s"Unknown design type: '$json'")
}
override def writes(dt: DesignType): JsValue =
dt match {
case com.gu.contentapi.client.utils.Article => JsString("Article")
case com.gu.contentapi.client.utils.Immersive => JsString("Immersive")
case com.gu.contentapi.client.utils.Media => JsString("Media")
case com.gu.contentapi.client.utils.Review => JsString("Review")
case com.gu.contentapi.client.utils.Analysis => JsString("Analysis")
case com.gu.contentapi.client.utils.Comment => JsString("Comment")
case com.gu.contentapi.client.utils.Feature => JsString("Feature")
case com.gu.contentapi.client.utils.Live => JsString("Live")
case com.gu.contentapi.client.utils.SpecialReport => JsString("SpecialReport")
case com.gu.contentapi.client.utils.Recipe => JsString("Recipe")
case com.gu.contentapi.client.utils.MatchReport => JsString("MatchReport")
case com.gu.contentapi.client.utils.Interview => JsString("Interview")
case com.gu.contentapi.client.utils.GuardianView => JsString("GuardianView")
case com.gu.contentapi.client.utils.Quiz => JsString("Quiz")
case com.gu.contentapi.client.utils.GuardianLabs => JsString("GuardianLabs")
case com.gu.contentapi.client.utils.AdvertisementFeature => JsString("AdvertisementFeature")
case com.gu.contentapi.client.utils.Newsletter => JsString("Newsletter")
case com.gu.contentapi.client.utils.Profile => JsString("Profile")
case com.gu.contentapi.client.utils.Timeline => JsString("Timeline")
}
}
implicit val pillarFormat: OFormat[Pillar] = Json.format[Pillar]
implicit val dateToTimestampWrites: json.JodaWrites.JodaDateTimeNumberWrites.type =
play.api.libs.json.JodaWrites.JodaDateTimeNumberWrites
implicit val paginationFormat: OFormat[Pagination] = Json.format[Pagination]
implicit val podcastFormat: OFormat[Podcast] = Json.format[Podcast]
implicit val referenceFormat: OFormat[Reference] = Json.format[Reference]
implicit val tagPropertiesFormat: OFormat[TagProperties] = Json.format[TagProperties]
implicit val tagFormat: Format[Tag] =
deduplicate(
Json.format[Tag],
_.id,
_.maximumSize(20000) // average Tag retains ~8KB in memory, so 20000 cached Tags retain only 160MB
.expireAfterAccess(1.hour),
)
implicit val tagsFormat: OFormat[Tags] = Json.format[Tags]
implicit val elementPropertiesFormat: OFormat[ElementProperties] = Json.format[ElementProperties]
implicit val imageAssetFormat: OFormat[ImageAsset] = Json.format[ImageAsset]
implicit val videoAssetFormat: OFormat[VideoAsset] = Json.format[VideoAsset]
implicit val imageMediaFormat: OFormat[ImageMedia] = Json.format[ImageMedia]
implicit val videoMediaFormat: OFormat[VideoMedia] = Json.format[VideoMedia]
implicit val videoElementFormat: OFormat[VideoElement] = Json.format[VideoElement]
implicit val mediaAssetFormat: OFormat[MediaAsset] = Json.format[MediaAsset]
implicit val mediaAtomFormat: OFormat[MediaAtom] = Json.format[MediaAtom]
implicit val mediaTypeFormat: MediaTypeFormat.type = MediaTypeFormat
implicit val cardStyleFormat: CardStyleFormat.type = CardStyleFormat
implicit val faciaImageFormat: FaciaImageFormat.format.type = FaciaImageFormat.format
implicit val itemKickerFormat: ItemKickerFormat.format.type = ItemKickerFormat.format
implicit val tagKickerFormat: OFormat[TagKicker] = ItemKickerFormat.tagKickerFormat
implicit val pressedCardHeader: OFormat[PressedCardHeader] = Json.format[PressedCardHeader]
implicit val boostLevel: BoostLevelFormat.type = BoostLevelFormat
implicit val pressedDisplaySettings: OFormat[PressedDisplaySettings] = Json.format[PressedDisplaySettings]
implicit val pressedDiscussionSettings: OFormat[PressedDiscussionSettings] = Json.format[PressedDiscussionSettings]
implicit val pressedCard: OFormat[PressedCard] = Json.format[PressedCard]
implicit val pressedFields: OFormat[PressedFields] = Json.format[PressedFields]
implicit val pressedTrail: OFormat[PressedTrail] = Json.format[PressedTrail]
implicit val pressedMetadata: OFormat[PressedMetadata] = Json.format[PressedMetadata]
implicit val pressedElements: OFormat[PressedElements] = Json.format[PressedElements]
implicit val pressedStory: OFormat[PressedStory] = Json.format[PressedStory]
implicit val pressedPropertiesFormat: OFormat[PressedProperties] = Json.format[PressedProperties]
implicit val enrichedContentFormat: OFormat[EnrichedContent] = Json.format[EnrichedContent]
val latestSnapFormat = Json.format[LatestSnap]
val linkSnapFormat = Json.format[LinkSnap]
val curatedContentFormat = Json.format[CuratedContent]
val supportingCuratedContentFormat = Json.format[SupportingCuratedContent]
}
object ItemKickerFormat {
implicit val kickerPropertiesFormat: OFormat[KickerProperties] = Json.format[KickerProperties]
implicit val seriesFormat: OFormat[Series] = Json.format[Series]
val tagKickerFormat: OFormat[TagKicker] = Json.format[TagKicker]
private val podcastKickerFormat = Json.format[PodcastKicker]
private val sectionKickerFormat = Json.format[SectionKicker]
private val freeHtmlKickerFormat = Json.format[FreeHtmlKicker]
private val freeHtmlKickerWithLinkFormat = Json.format[FreeHtmlKickerWithLink]
object format extends Format[ItemKicker] {
def reads(json: JsValue): JsResult[ItemKicker] = {
(json \ "type").transform[JsString](Reads.JsStringReads) match {
case JsSuccess(JsString("BreakingNewsKicker"), _) => JsSuccess(BreakingNewsKicker)
case JsSuccess(JsString("LiveKicker"), _) => JsSuccess(LiveKicker)
case JsSuccess(JsString("AnalysisKicker"), _) => JsSuccess(AnalysisKicker)
case JsSuccess(JsString("ReviewKicker"), _) => JsSuccess(ReviewKicker)
case JsSuccess(JsString("CartoonKicker"), _) => JsSuccess(CartoonKicker)
case JsSuccess(JsString("PodcastKicker"), _) => (json \ "series").validate[PodcastKicker](podcastKickerFormat)
case JsSuccess(JsString("TagKicker"), _) => (json \ "item").validate[TagKicker](tagKickerFormat)
case JsSuccess(JsString("SectionKicker"), _) => (json \ "item").validate[SectionKicker](sectionKickerFormat)
case JsSuccess(JsString("FreeHtmlKicker"), _) => (json \ "item").validate[FreeHtmlKicker](freeHtmlKickerFormat)
case JsSuccess(JsString("FreeHtmlKickerWithLink"), _) =>
(json \ "item").validate[FreeHtmlKickerWithLink](freeHtmlKickerWithLinkFormat)
case _ => JsError("Could not convert ItemKicker")
}
}
def writes(itemKicker: ItemKicker): JsObject =
itemKicker match {
case BreakingNewsKicker => JsObject(Seq("type" -> JsString("BreakingNewsKicker")))
case LiveKicker => JsObject(Seq("type" -> JsString("LiveKicker")))
case AnalysisKicker => JsObject(Seq("type" -> JsString("AnalysisKicker")))
case ReviewKicker => JsObject(Seq("type" -> JsString("ReviewKicker")))
case CartoonKicker => JsObject(Seq("type" -> JsString("CartoonKicker")))
case podcastKicker: PodcastKicker =>
JsObject(
Seq("type" -> JsString("PodcastKicker"), "series" -> Json.toJson(podcastKicker)(podcastKickerFormat)),
)
case tagKicker: TagKicker =>
JsObject(Seq("type" -> JsString("TagKicker"), "item" -> Json.toJson(tagKicker)(tagKickerFormat)))
case sectionKicker: SectionKicker =>
JsObject(Seq("type" -> JsString("SectionKicker"), "item" -> Json.toJson(sectionKicker)(sectionKickerFormat)))
case freeHtmlKicker: FreeHtmlKicker =>
JsObject(
Seq("type" -> JsString("FreeHtmlKicker"), "item" -> Json.toJson(freeHtmlKicker)(freeHtmlKickerFormat)),
)
case freeHtmlKickerWithLink: FreeHtmlKickerWithLink =>
JsObject(
Seq(
"type" -> JsString("FreeHtmlKickerWithLink"),
"item" -> Json.toJson(freeHtmlKickerWithLink)(freeHtmlKickerWithLinkFormat),
),
)
}
}
}
object FaciaImageFormat {
implicit val cutoutFormat: OFormat[Cutout] = Json.format[Cutout]
implicit val replaceFormat: OFormat[Replace] = Json.format[Replace]
implicit val slideshowFormat: OFormat[ImageSlideshow] = Json.format[ImageSlideshow]
object format extends Format[Image] {
def reads(json: JsValue): JsResult[Image] = {
(json \ "type").transform[JsString](Reads.JsStringReads) match {
case JsSuccess(JsString("Cutout"), _) => (json \ "item").validate[Cutout](cutoutFormat)
case JsSuccess(JsString("Replace"), _) => (json \ "item").validate[Replace](replaceFormat)
case JsSuccess(JsString("ImageSlideshow"), _) => (json \ "item").validate[ImageSlideshow](slideshowFormat)
case _ => JsError("Could not convert ItemKicker")
}
}
def writes(faciaImage: Image): JsObject =
faciaImage match {
case cutout: Cutout => JsObject(Seq("type" -> JsString("Cutout"), "item" -> Json.toJson(cutout)(cutoutFormat)))
case replace: Replace =>
JsObject(Seq("type" -> JsString("Replace"), "item" -> Json.toJson(replace)(replaceFormat)))
case imageSlideshow: ImageSlideshow =>
JsObject(Seq("type" -> JsString("ImageSlideshow"), "item" -> Json.toJson(imageSlideshow)(slideshowFormat)))
}
}
}
object PressedCollectionFormat {
implicit val dateToTimestampWrites: libs.json.JodaWrites.JodaDateTimeNumberWrites.type =
play.api.libs.json.JodaWrites.JodaDateTimeNumberWrites
implicit val displayHintsFormat: OFormat[DisplayHints] = Json.format[DisplayHints]
implicit val groupConfigFormat: OFormat[GroupConfig] = Json.format[GroupConfig]
implicit val groupsConfigFormat: OFormat[GroupsConfig] = Json.format[GroupsConfig]
implicit val collectionConfigFormat: OFormat[CollectionConfig] = Json.format[CollectionConfig]
implicit val pressedContentFormat: PressedContentFormat.format.type = PressedContentFormat.format
val format: OFormat[PressedCollection] = Json.format[PressedCollection]
}
object PressedPageFormat {
implicit val dateToTimestampWrites: api.libs.json.JodaWrites.JodaDateTimeNumberWrites.type =
play.api.libs.json.JodaWrites.JodaDateTimeNumberWrites
implicit val pressedCollection: OFormat[PressedCollection] = PressedCollectionFormat.format
val format: OFormat[PressedPage] = Json.format[PressedPage]
}