All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.gu.facia.api.contentapi.ContentApi.scala Maven / Gradle / Ivy

The newest version!
package com.gu.facia.api.contentapi

import java.net.URI

import com.gu.contentapi.client.ContentApiClient
import com.gu.contentapi.client.model.v1.{Content, ItemResponse, SearchResponse}
import com.gu.contentapi.client.model.{ItemQuery, SearchQuery}
import com.gu.facia.api.models.BrandingByEdition
import com.gu.facia.api.utils.ContentApiUtils._
import com.gu.facia.api.{CapiError, Response, UrlConstructError}
import com.typesafe.scalalogging.StrictLogging

import scala.concurrent.{ExecutionContext, Future}
import scala.util.control.NonFatal
import scala.util.{Success, Try, Failure}

case class LatestSnapsRequest(snaps: Map[String, String]) {
  def join(other: LatestSnapsRequest): LatestSnapsRequest = this.copy(snaps = this.snaps ++ other.snaps)
}

case class LinkSnapsRequest(snaps: Map[String, String])

object ContentApi extends StrictLogging {
  type AdjustSearchQuery = SearchQuery => SearchQuery
  type AdjustItemQuery = ItemQuery => ItemQuery

  def buildHydrateQueries(client: ContentApiClient, ids: List[String], adjustSearchQuery: AdjustSearchQuery = identity, itemQueries: ItemQueries = ItemQueries): Response[Seq[SearchQuery]] = {
    Try(IdsSearchQueries.makeBatches(ids)) match {
        case Success(Some(batches)) =>
          Response.Right(batches.map(adjustSearchQuery compose itemQueries.queryForIds))
        case _ =>
          Response.Left(UrlConstructError("Unable to construct url for ids search query (the constructed URL for a " +
            s"single ID must be too long!): ${ids.mkString(", ")}"))}
  }

  def getHydrateResponse(client: ContentApiClient, searchQueries: Seq[SearchQuery])(implicit ec: ExecutionContext): Response[Seq[SearchResponse]] = {
    Response.Async.Right(Future.traverse(searchQueries)(client.getResponse(_))) mapError { err =>
      CapiError(s"Failed to hydrate content ${err.message}", err.cause)
    }
  }

  def itemsFromSearchResponses(searchResponses: Seq[SearchResponse]): Set[Content] =
    searchResponses.flatMap(_.results).toSet

  def buildBackfillQuery(apiQuery: String): Either[ItemQuery, SearchQuery] = {
    val uri = new URI(apiQuery.replace("|", "%7C").replace(" ", "%20"))
    val path = uri.getPath.stripPrefix("/")
    val rawParams = Option(uri.getQuery).map(parseQueryString).getOrElse(Nil).map {
      // wrap backfill tags in parentheses in case the editors wrote a raw OR query
      // makes it possible to safely append additional tags
      case (k, v) if k == "tag" => (k, s"($v)")
      case (k, v) if k == "show-fields" => (k, s"$v,internalPageCode")
      case param => param
    }
    val paramsWithFields =
      if (rawParams.exists {
        case ("show-fields", _) => true
        case _ => false
      }) rawParams else rawParams :+ ("show-fields" -> "internalPageCode")

    val paramsWithEditorsPicks = paramsWithFields :+ ("show-editors-picks" -> "false")

    if (path == "search") {
      val searchQuery = SearchQuery()
      val queryWithParams = searchQuery.withParameters(paramsWithEditorsPicks.map { case (k, v) => k -> searchQuery.StringParameter(k, Some(v)) }.toMap)
      Right(queryWithParams)
    } else {
      val itemQuery = ItemQuery(path)
      val queryWithParams = itemQuery.withParameters(paramsWithEditorsPicks.map { case (k, v) => k -> itemQuery.StringParameter(k, Some(v)) }.toMap)
      Left(queryWithParams)
    }
  }

  def getBackfillResponse(client: ContentApiClient, query: Either[ItemQuery, SearchQuery])
                         (implicit ec: ExecutionContext): Either[Response[ItemResponse], Response[SearchResponse]] = {
    query.map { itemQuery =>
      Response.Async.Right(client.getResponse(itemQuery)) mapError { err =>
        CapiError(s"Failed to get backfill response ${err.message}", err.cause)
      }
    }.left.map { searchQuery =>
      Response.Async.Right(client.getResponse(searchQuery)) mapError { err =>
        CapiError(s"Failed to get backfill response ${err.message}", err.cause)
      }
    }
  }

  def backfillContentFromResponse(response: Either[Response[ItemResponse], Response[SearchResponse]])
                                 (implicit ec: ExecutionContext): Response[List[Content]] = {
    response.fold(
      _.map { itemResponse =>
        (itemResponse.mostViewed.getOrElse(Nil) ++ itemResponse.results.getOrElse(Nil)).toList
      },
      _.map { searchResponse =>
        searchResponse.results.toList
      }
    )
  }

  def parseQueryString(queryString: String): Seq[(String, String)] = {
    val KeyValuePair = """([^=]+)=(.*)""".r

    queryString.split("&").toSeq.collect {
      case KeyValuePair(key, value) => (key, value)
    }
  }

  def latestContentFromLatestSnaps(capiClient: ContentApiClient, latestSnapsRequest: LatestSnapsRequest, adjustItemQuery: AdjustItemQuery, itemQueries: ItemQueries = ItemQueries)
                                  (implicit ec: ExecutionContext): Response[Map[String, Option[Content]]] = {
    Response.Async.Right(
      Future.traverse(latestSnapsRequest.snaps.toSeq) { case (id, uri) =>
        capiClient.getResponse(adjustItemQuery(itemQueries.latestContentQueryFromSnapUri(uri)))
          .map(_.results.getOrElse(Nil).headOption).map(id -> _)
      }.map(_.toMap))
  }

  def linkSnapBrandingsByEdition(capiClient: ContentApiClient, linkSnapsRequest: LinkSnapsRequest, itemQueries: ItemQueries = ItemQueries)
    (implicit ec: ExecutionContext): Response[Map[String, BrandingByEdition]] = {

    def toIdAndUri(snap: (String, String)): Option[(String, URI)] = Try { new URI(snap._2) } match {
      case Success(uri) => Some(snap._1 -> uri)
      case Failure(exception) =>
        // Swallow the error at this point rather than failing fast, and log.
        logger.error(s"Failed to parse URI ${snap._2} of snap ${snap._1}", exception)
        None
    }

    def isPossibleSectionFrontOrTagPage(snap: (String, URI)): Boolean = {
      val uri = snap._2
      !uri.isAbsolute && !uri.getPath.contains('+')
    }

    def brandingsFromResponse(response: ItemResponse): BrandingByEdition =
      response.section.map(_.brandingByEdition) orElse response.tag.map(_.brandingByEdition) getOrElse Map.empty

    Response.Async.Right {
      Future.traverse(linkSnapsRequest.snaps.toSeq.flatMap(toIdAndUri).filter(isPossibleSectionFrontOrTagPage)) {
        case (id, uri) =>
          val query = itemQueries.brandingQueryFromSnapUri(uri)
          val response = capiClient.getResponse(query)
          response.failed.foreach { case NonFatal(e) =>
            logger.warn(s"Failed to get response for link snap query '$query'", e)
          }
          response.map(brandingsFromResponse).map(id -> _)
      }.map(_.toMap)
      .recoverWith { case NonFatal(e) =>
        Future.successful(Map.empty[String, BrandingByEdition])
      }
    }
  }
}

trait ItemQueries {
  def queryForIds(ids: Seq[String]): SearchQuery

  def latestContentQueryFromSnapUri(uri: String): ItemQuery

  def brandingQueryFromSnapUri(uri: URI): ItemQuery
}

object ItemQueries extends ItemQueries {
  def queryForIds(ids: Seq[String]) = ContentApiClient.search
    .ids(ids mkString ",")
    .pageSize(ids.size)
    .showFields("internalPageCode")

  def latestContentQueryFromSnapUri(uri: String): ItemQuery =
    ContentApiClient.item(uri).pageSize(1).showFields("internalPageCode")

  def brandingQueryFromSnapUri(uri: URI): ItemQuery = {
    def cleaned(path: String) = path.stripPrefix("/").stripSuffix("/all").stripSuffix("/latest")
    ContentApiClient.item(cleaned(uri.getPath)).pageSize(1)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy