1 /*
2  *  Make.org Core API
3  *  Copyright (C) 2020 Make.org
4  *
5  * This program is free software: you can redistribute it and/or modify
6  *  it under the terms of the GNU Affero General Public License as
7  *  published by the Free Software Foundation, either version 3 of the
8  *  License, or (at your option) any later version.
9  *
10  *  This program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *  GNU Affero General Public License for more details.
14  *
15  *  You should have received a copy of the GNU Affero General Public License
16  *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
17  *
18  */
19 
20 package org.make.api.post
21 
22 import akka.stream.scaladsl.Sink
23 import cats.data.Validated.{Invalid, Valid}
24 import cats.data.{Validated, ValidatedNel}
25 import cats.syntax.apply._
26 import eu.timepit.refined.auto._
27 import grizzled.slf4j.Logging
28 import org.make.api.technical.webflow.WebflowPost.{WebflowImageRef, WebflowPostWithCountry}
29 import org.make.api.technical.webflow.{WebflowClientComponent, WebflowConfigurationComponent}
30 import org.make.api.technical.{ActorSystemComponent, StreamUtils}
31 import org.make.core.post.indexed.{PostSearchQuery, PostSearchResult}
32 import org.make.core.post.{Post, PostId}
33 import org.make.core.reference.Country
34 
35 import java.net.URI
36 import java.time.ZonedDateTime
37 import scala.concurrent.ExecutionContext.Implicits.global
38 import scala.concurrent.Future
39 import scala.util.{Failure, Success, Try}
40 
41 trait DefaultPostServiceComponent extends PostServiceComponent with Logging {
42   this: PostSearchEngineComponent
43     with WebflowClientComponent
44     with ActorSystemComponent
45     with WebflowConfigurationComponent =>
46 
47   override lazy val postService: PostService = new DefaultPostService
48 
49   class DefaultPostService extends PostService {
50 
51     override def search(searchQuery: PostSearchQuery): Future[PostSearchResult] = {
52       elasticsearchPostAPI.searchPosts(searchQuery)
53     }
54 
55     override def fetchPostsForHome(): Future[Seq[Post]] = {
56       def toPost(webflowPost: WebflowPostWithCountry, postDate: ZonedDateTime): ValidatedNel[String, Post] = {
57         def parseImageUrl(imageUrl: Option[WebflowImageRef]): ValidatedNel[String, URI] = {
58           imageUrl match {
59             case Some(WebflowImageRef(url, _)) =>
60               Try(new URI(url)) match {
61                 case Success(validUrl) => Validated.valid(validUrl)
62                 case Failure(_)        => Validated.invalidNel(s"invalid thumbnailImage: $url")
63               }
64             case None => Validated.invalidNel("empty thumbnailImage")
65           }
66         }
67         def parseSummary(summary: Option[String]): ValidatedNel[String, String] = {
68           summary match {
69             case Some(desc) if desc.nonEmpty => Valid(desc)
70             case _                           => Validated.invalidNel("empty postSummary")
71           }
72         }
73         (parseImageUrl(webflowPost.thumbnailImage), parseSummary(webflowPost.summary))
74           .mapN[Post] { (url: URI, summary: String) =>
75             Post(
76               postId = PostId(webflowPost.id),
77               name = webflowPost.name,
78               slug = webflowPost.slug,
79               displayHome = webflowPost.displayHome.contains(true),
80               postDate = postDate,
81               thumbnailUrl = url,
82               thumbnailAlt = webflowPost.thumbnailImage.flatMap(_.alt),
83               sourceUrl = new URI(s"${webflowConfiguration.blogUrl}/${webflowPost.collectionSlug}/${webflowPost.slug}"),
84               summary = summary,
85               country = Country(webflowPost.country)
86             )
87           }
88       }
89 
90       StreamUtils
91         .asyncPageToPageSource(offset => webflowClient.getAllPosts(100, offset))
92         .mapConcat(identity)
93         .collect {
94           case post @ WebflowPostWithCountry(id, false, false, _, _, Some(true), Some(postDate), _, _, _, _) =>
95             (id, toPost(post, postDate))
96         }
97         .wireTap {
98           case (postId, Invalid(errors)) =>
99             logger.error(
100               s"Ignoring failed post $postId due to invalid required fields: ${errors.toList.mkString(", ")}"
101             )
102           case _ => ()
103         }
104         .collect {
105           case (_, Valid(post)) => post
106         }
107         .runWith(Sink.seq)
108     }
109   }
110 }
Line Stmt Id Pos Tree Symbol Tests Code
52 24958 1996 - 2041 Apply org.make.api.post.PostSearchEngine.searchPosts org.make.api.post.postservicetest DefaultPostServiceComponent.this.elasticsearchPostAPI.searchPosts(searchQuery)
60 26753 2403 - 2420 Apply scala.util.Try.apply scala.util.Try.apply[java.net.URI](new java.net.URI(url))
60 22922 2407 - 2419 Apply java.net.URI.<init> new java.net.URI(url)
61 25466 2471 - 2496 Apply cats.data.ValidatedFunctions.valid cats.data.Validated.valid[Nothing, java.net.URI](validUrl)
62 23389 2539 - 2592 Apply cats.data.ValidatedFunctions.invalidNel cats.data.Validated.invalidNel[String, Nothing](("invalid thumbnailImage: ".+(url): String))
64 27063 2634 - 2678 Apply cats.data.ValidatedFunctions.invalidNel cats.data.Validated.invalidNel[String, Nothing]("empty thumbnailImage")
69 24796 2842 - 2855 Select scala.collection.StringOps.nonEmpty scala.Predef.augmentString(desc).nonEmpty
69 22621 2859 - 2870 Apply cats.data.Validated.Valid.apply cats.data.Validated.Valid.apply[String](desc)
70 27339 2919 - 2960 Apply cats.data.ValidatedFunctions.invalidNel cats.data.Validated.invalidNel[String, Nothing]("empty postSummary")
73 23227 2991 - 3069 Apply scala.Tuple2.apply scala.Tuple2.apply[cats.data.ValidatedNel[String,java.net.URI], cats.data.ValidatedNel[String,String]](parseImageUrl(webflowPost.thumbnailImage), parseSummary(webflowPost.summary))
73 22931 2992 - 3033 Apply org.make.api.post.DefaultPostServiceComponent.DefaultPostService.parseImageUrl parseImageUrl(webflowPost.thumbnailImage)
73 25275 3006 - 3032 Select org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry.thumbnailImage webflowPost.thumbnailImage
73 25400 3035 - 3068 Apply org.make.api.post.DefaultPostServiceComponent.DefaultPostService.parseSummary parseSummary(webflowPost.summary)
73 26764 3048 - 3067 Select org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry.summary webflowPost.summary
74 22701 3092 - 3092 ApplyToImplicitArgs cats.data.ValidatedInstances.catsDataApplicativeErrorForValidated data.this.Validated.catsDataApplicativeErrorForValidated[cats.data.NonEmptyList[String]](data.this.NonEmptyList.catsDataSemigroupForNonEmptyList[String])
74 25261 3092 - 3092 TypeApply cats.data.NonEmptyListInstances.catsDataSemigroupForNonEmptyList data.this.NonEmptyList.catsDataSemigroupForNonEmptyList[String]
74 26545 2991 - 3709 ApplyToImplicitArgs cats.syntax.Tuple2SemigroupalOps.mapN cats.syntax.`package`.apply.catsSyntaxTuple2Semigroupal[[+A]cats.data.ValidatedNel[String,A], java.net.URI, String](scala.Tuple2.apply[cats.data.ValidatedNel[String,java.net.URI], cats.data.ValidatedNel[String,String]](parseImageUrl(webflowPost.thumbnailImage), parseSummary(webflowPost.summary))).mapN[org.make.core.post.Post](((url: java.net.URI, summary: String) => org.make.core.post.Post.apply(org.make.core.post.PostId.apply(webflowPost.id), webflowPost.name, webflowPost.slug, webflowPost.displayHome.contains[Boolean](true), postDate, url, webflowPost.thumbnailImage.flatMap[String](((x$1: org.make.api.technical.webflow.WebflowPost.WebflowImageRef) => x$1.alt)), new java.net.URI(("".+(DefaultPostServiceComponent.this.webflowConfiguration.blogUrl).+("/").+(webflowPost.collectionSlug).+("/").+(webflowPost.slug): String)), summary, org.make.core.reference.Country.apply(webflowPost.country))))(data.this.Validated.catsDataApplicativeErrorForValidated[cats.data.NonEmptyList[String]](data.this.NonEmptyList.catsDataSemigroupForNonEmptyList[String]), data.this.Validated.catsDataApplicativeErrorForValidated[cats.data.NonEmptyList[String]](data.this.NonEmptyList.catsDataSemigroupForNonEmptyList[String]))
74 22388 3092 - 3092 TypeApply cats.data.NonEmptyListInstances.catsDataSemigroupForNonEmptyList data.this.NonEmptyList.catsDataSemigroupForNonEmptyList[String]
74 27280 3092 - 3092 ApplyToImplicitArgs cats.data.ValidatedInstances.catsDataApplicativeErrorForValidated data.this.Validated.catsDataApplicativeErrorForValidated[cats.data.NonEmptyList[String]](data.this.NonEmptyList.catsDataSemigroupForNonEmptyList[String])
75 24820 3137 - 3697 Apply org.make.core.post.Post.apply org.make.core.post.Post.apply(org.make.core.post.PostId.apply(webflowPost.id), webflowPost.name, webflowPost.slug, webflowPost.displayHome.contains[Boolean](true), postDate, url, webflowPost.thumbnailImage.flatMap[String](((x$1: org.make.api.technical.webflow.WebflowPost.WebflowImageRef) => x$1.alt)), new java.net.URI(("".+(DefaultPostServiceComponent.this.webflowConfiguration.blogUrl).+("/").+(webflowPost.collectionSlug).+("/").+(webflowPost.slug): String)), summary, org.make.core.reference.Country.apply(webflowPost.country))
76 24807 3166 - 3188 Apply org.make.core.post.PostId.apply org.make.core.post.PostId.apply(webflowPost.id)
76 26996 3173 - 3187 Select org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry.id webflowPost.id
77 22456 3211 - 3227 Select org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry.name webflowPost.name
78 27270 3250 - 3266 Select org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry.slug webflowPost.slug
79 24944 3296 - 3334 Apply scala.Option.contains webflowPost.displayHome.contains[Boolean](true)
82 22944 3469 - 3474 Select org.make.api.technical.webflow.WebflowPost.WebflowImageRef.alt x$1.alt
82 26688 3434 - 3475 Apply scala.Option.flatMap webflowPost.thumbnailImage.flatMap[String](((x$1: org.make.api.technical.webflow.WebflowPost.WebflowImageRef) => x$1.alt))
83 24350 3503 - 3596 Apply java.net.URI.<init> new java.net.URI(("".+(DefaultPostServiceComponent.this.webflowConfiguration.blogUrl).+("/").+(webflowPost.collectionSlug).+("/").+(webflowPost.slug): String))
85 23148 3663 - 3682 Select org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry.country webflowPost.country
85 26829 3655 - 3683 Apply org.make.core.reference.Country.apply org.make.core.reference.Country.apply(webflowPost.country)
91 23158 3767 - 3767 Select scala.concurrent.ExecutionContext.Implicits.global org.make.api.post.postservicetest scala.concurrent.ExecutionContext.Implicits.global
91 24273 3778 - 3816 Apply org.make.api.technical.webflow.WebflowClient.getAllPosts DefaultPostServiceComponent.this.webflowClient.getAllPosts((api.this.RefType.refinedRefType.unsafeWrap[Int, org.make.api.technical.webflow.WebflowClient.UpToOneHundredRefinement](100): eu.timepit.refined.api.Refined[Int,org.make.api.technical.webflow.WebflowClient.UpToOneHundredRefinement]), offset)
92 26836 3837 - 3845 Apply scala.Predef.identity scala.Predef.identity[Seq[org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry]](x)
93 27216 3864 - 3864 Apply org.make.api.post.DefaultPostServiceComponent.DefaultPostService.$anonfun.<init> org.make.api.post.postservicetest new $anonfun()
95 22399 3990 - 4018 Apply scala.Tuple2.apply scala.Tuple2.apply[String, cats.data.ValidatedNel[String,org.make.core.post.Post]](id, toPost(post, postDate))
95 24581 3995 - 4017 Apply org.make.api.post.DefaultPostServiceComponent.DefaultPostService.toPost toPost(post, postDate)
99 25271 4104 - 4241 Apply grizzled.slf4j.Logger.error DefaultPostServiceComponent.this.logger.error(("Ignoring failed post ".+(postId).+(" due to invalid required fields: ").+(errors.toList.mkString(", ")): String))
102 22709 4262 - 4264 Literal <nosymbol> ()
104 26674 4292 - 4292 Apply org.make.api.post.DefaultPostServiceComponent.DefaultPostService.$anonfun.<init> org.make.api.post.postservicetest new $anonfun()
107 27139 4360 - 4360 ApplyToImplicitArgs akka.stream.Materializer.matFromSystem org.make.api.post.postservicetest stream.this.Materializer.matFromSystem(DefaultPostServiceComponent.this.actorSystem)
107 24285 4361 - 4369 TypeApply akka.stream.scaladsl.Sink.seq org.make.api.post.postservicetest akka.stream.scaladsl.Sink.seq[org.make.core.post.Post]
107 23019 4360 - 4360 Select org.make.api.technical.ActorSystemComponent.actorSystem org.make.api.post.postservicetest DefaultPostServiceComponent.this.actorSystem
107 24801 3725 - 4370 ApplyToImplicitArgs akka.stream.scaladsl.Source.runWith org.make.api.post.postservicetest org.make.api.technical.StreamUtils.asyncPageToPageSource[org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry](((offset: Int) => DefaultPostServiceComponent.this.webflowClient.getAllPosts((api.this.RefType.refinedRefType.unsafeWrap[Int, org.make.api.technical.webflow.WebflowClient.UpToOneHundredRefinement](100): eu.timepit.refined.api.Refined[Int,org.make.api.technical.webflow.WebflowClient.UpToOneHundredRefinement]), offset)))(scala.concurrent.ExecutionContext.Implicits.global).mapConcat[org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry](((x: Seq[org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry]) => scala.Predef.identity[Seq[org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry]](x))).collect[(String, cats.data.ValidatedNel[String,org.make.core.post.Post])](({ @SerialVersionUID(value = 0) final <synthetic> class $anonfun extends scala.runtime.AbstractPartialFunction[org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry,(String, cats.data.ValidatedNel[String,org.make.core.post.Post])] with java.io.Serializable { def <init>(): <$anon: org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry => (String, cats.data.ValidatedNel[String,org.make.core.post.Post])> = { $anonfun.super.<init>(); () }; final override def applyOrElse[A1 <: org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry, B1 >: (String, cats.data.ValidatedNel[String,org.make.core.post.Post])](x1: A1, default: A1 => B1): B1 = ((x1.asInstanceOf[org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry]: org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry): org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry @unchecked) match { case (post @ (id: String, archived: Boolean, draft: Boolean, name: String, slug: String, displayHome: Option[Boolean], postDate: Option[java.time.ZonedDateTime], thumbnailImage: Option[org.make.api.technical.webflow.WebflowPost.WebflowImageRef], summary: Option[String], country: String, collectionSlug: String): org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry((id @ _), false, false, _, _, (value: Boolean): Some[Boolean](true), (value: java.time.ZonedDateTime): Some[java.time.ZonedDateTime]((postDate @ _)), _, _, _, _)) => scala.Tuple2.apply[String, cats.data.ValidatedNel[String,org.make.core.post.Post]](id, toPost(post, postDate)) case (defaultCase$ @ _) => default.apply(x1) }; final def isDefinedAt(x1: org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry): Boolean = ((x1.asInstanceOf[org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry]: org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry): org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry @unchecked) match { case (post @ (id: String, archived: Boolean, draft: Boolean, name: String, slug: String, displayHome: Option[Boolean], postDate: Option[java.time.ZonedDateTime], thumbnailImage: Option[org.make.api.technical.webflow.WebflowPost.WebflowImageRef], summary: Option[String], country: String, collectionSlug: String): org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry((id @ _), false, false, _, _, (value: Boolean): Some[Boolean](true), (value: java.time.ZonedDateTime): Some[java.time.ZonedDateTime]((postDate @ _)), _, _, _, _)) => true case (defaultCase$ @ _) => false } }; new $anonfun() }: PartialFunction[org.make.api.technical.webflow.WebflowPost.WebflowPostWithCountry,(String, cats.data.ValidatedNel[String,org.make.core.post.Post])])).wireTap(((x0$1: (String, cats.data.Validated[cats.data.NonEmptyList[String],org.make.core.post.Post])) => x0$1 match { case (_1: String, _2: cats.data.Validated[cats.data.NonEmptyList[String],org.make.core.post.Post]): (String, cats.data.Validated[cats.data.NonEmptyList[String],org.make.core.post.Post])((postId @ _), (e: cats.data.NonEmptyList[String]): cats.data.Validated.Invalid[cats.data.NonEmptyList[String]]((errors @ _))) => DefaultPostServiceComponent.this.logger.error(("Ignoring failed post ".+(postId).+(" due to invalid required fields: ").+(errors.toList.mkString(", ")): String)) case _ => () })).collect[org.make.core.post.Post](({ @SerialVersionUID(value = 0) final <synthetic> class $anonfun extends scala.runtime.AbstractPartialFunction[(String, cats.data.Validated[cats.data.NonEmptyList[String],org.make.core.post.Post]),org.make.core.post.Post] with java.io.Serializable { def <init>(): <$anon: ((String, cats.data.Validated[cats.data.NonEmptyList[String],org.make.core.post.Post])) => org.make.core.post.Post> = { $anonfun.super.<init>(); () }; final override def applyOrElse[A1 <: (String, cats.data.Validated[cats.data.NonEmptyList[String],org.make.core.post.Post]), B1 >: org.make.core.post.Post](x2: A1, default: A1 => B1): B1 = ((x2.asInstanceOf[(String, cats.data.Validated[cats.data.NonEmptyList[String],org.make.core.post.Post])]: (String, cats.data.Validated[cats.data.NonEmptyList[String],org.make.core.post.Post])): (String, cats.data.Validated[cats.data.NonEmptyList[String],org.make.core.post.Post]) @unchecked) match { case (_1: String, _2: cats.data.Validated[cats.data.NonEmptyList[String],org.make.core.post.Post]): (String, cats.data.Validated[cats.data.NonEmptyList[String],org.make.core.post.Post])(_, (a: org.make.core.post.Post): cats.data.Validated.Valid[org.make.core.post.Post]((post @ _))) => post case (defaultCase$ @ _) => default.apply(x2) }; final def isDefinedAt(x2: (String, cats.data.Validated[cats.data.NonEmptyList[String],org.make.core.post.Post])): Boolean = ((x2.asInstanceOf[(String, cats.data.Validated[cats.data.NonEmptyList[String],org.make.core.post.Post])]: (String, cats.data.Validated[cats.data.NonEmptyList[String],org.make.core.post.Post])): (String, cats.data.Validated[cats.data.NonEmptyList[String],org.make.core.post.Post]) @unchecked) match { case (_1: String, _2: cats.data.Validated[cats.data.NonEmptyList[String],org.make.core.post.Post]): (String, cats.data.Validated[cats.data.NonEmptyList[String],org.make.core.post.Post])(_, (a: org.make.core.post.Post): cats.data.Validated.Valid[org.make.core.post.Post]((post @ _))) => true case (defaultCase$ @ _) => false } }; new $anonfun() }: PartialFunction[(String, cats.data.Validated[cats.data.NonEmptyList[String],org.make.core.post.Post]),org.make.core.post.Post])).runWith[scala.concurrent.Future[Seq[org.make.core.post.Post]]](akka.stream.scaladsl.Sink.seq[org.make.core.post.Post])(stream.this.Materializer.matFromSystem(DefaultPostServiceComponent.this.actorSystem))