Skip to content

Commit ccbd9ed

Browse files
committed
Make a StreamingStacClient to make G is independent from F; finish search pagination
1 parent 5a4fbe1 commit ccbd9ed

File tree

11 files changed

+45
-34
lines changed

11 files changed

+45
-34
lines changed

build.sbt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ lazy val client = crossProject(JSPlatform, JVMPlatform)
209209
.settings(publishSettings)
210210
.settings(
211211
libraryDependencies ++= Seq(
212+
"com.github.julien-truffaut" %%% "monocle-core" % Versions.Monocle,
213+
"com.github.julien-truffaut" %%% "monocle-macro" % Versions.Monocle,
212214
"io.circe" %%% "circe-core" % Versions.Circe,
213215
"io.circe" %%% "circe-generic" % Versions.Circe,
214216
"io.circe" %%% "circe-refined" % Versions.Circe,

modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import com.azavea.stac4s.jsTypes.TemporalExtent
88
import eu.timepit.refined.types.numeric.NonNegInt
99
import io.circe._
1010
import io.circe.refined._
11+
import monocle.Lens
12+
import monocle.macros.GenLens
1113

1214
case class SearchFilters(
1315
bbox: Option[Bbox] = None,
@@ -21,6 +23,7 @@ case class SearchFilters(
2123
)
2224

2325
object SearchFilters extends ClientCodecs {
26+
implicit val paginationTokenLens: Lens[SearchFilters, Option[PaginationToken]] = GenLens[SearchFilters](_.next)
2427

2528
implicit val searchFiltersDecoder: Decoder[SearchFilters] = { c =>
2629
for {

modules/client/js/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala

Lines changed: 0 additions & 4 deletions
This file was deleted.
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.azavea.stac4s.api
22

33
package object client {
4-
type SttpStacClient[F[_]] = SttpStacClientF[F, SearchFilters]
4+
type SttpStacClient[F[_]] = SttpStacClientF[F, SearchFilters]
5+
type StacClient[F[_]] = StacClientF[F, SearchFilters]
6+
type StreamingStacClient[F[_], G[_]] = StreamingStacClientF[F, G, SearchFilters]
57
}

modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import eu.timepit.refined.types.numeric.NonNegInt
88
import geotrellis.vector.{io => _, _}
99
import io.circe._
1010
import io.circe.refined._
11+
import monocle.Lens
12+
import monocle.macros.GenLens
1113

1214
case class SearchFilters(
1315
bbox: Option[Bbox] = None,
@@ -21,6 +23,7 @@ case class SearchFilters(
2123
)
2224

2325
object SearchFilters extends ClientCodecs {
26+
implicit val paginationTokenLens: Lens[SearchFilters, Option[PaginationToken]] = GenLens[SearchFilters](_.next)
2427

2528
implicit val searchFiltersDecoder: Decoder[SearchFilters] = { c =>
2629
for {

modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala

Lines changed: 0 additions & 4 deletions
This file was deleted.

modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/client.scala

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.azavea.stac4s.api
2+
3+
package object client {
4+
type SttpStacClient[F[_]] = SttpStacClientF[F, SearchFilters]
5+
type StacClient[F[_]] = StacClientF[F, SearchFilters]
6+
type StreamingStacClient[F[_], G[_]] = StreamingStacClientF[F, G, SearchFilters]
7+
}

modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/SttpStacClientF.scala

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,25 @@ import cats.MonadThrow
66
import cats.syntax.apply._
77
import cats.syntax.either._
88
import cats.syntax.flatMap._
9+
import cats.syntax.functor._
910
import cats.syntax.option._
1011
import eu.timepit.refined.types.string.NonEmptyString
1112
import fs2.Stream
12-
import io.circe
1313
import io.circe.syntax._
14-
import io.circe.{Encoder, Json}
14+
import io.circe.{Encoder, Error, Json, JsonObject}
15+
import monocle.Lens
1516
import sttp.client3.circe.asJson
1617
import sttp.client3.{ResponseException, SttpBackend, UriContext, basicRequest}
1718
import sttp.model.Uri
1819

19-
case class SttpStacClientF[F[_]: MonadThrow, S: Encoder](
20+
case class SttpStacClientF[F[_]: MonadThrow, S: Lens[*, Option[PaginationToken]]: Encoder](
2021
client: SttpBackend[F, Any],
2122
baseUri: Uri
2223
) extends StreamingStacClientF[F, Stream[F, *], S] {
24+
private val paginationTokenLens = implicitly[Lens[S, Option[PaginationToken]]]
2325

24-
/** Get the next page [[Uri]] from the received [[Json]] body. */
25-
private def getNextLink(body: Either[ResponseException[String, circe.Error], Json]): F[Option[Uri]] =
26+
/** Get the next page [[Uri]] from the retrieved [[Json]] body. */
27+
private def getNextLink(body: Either[ResponseException[String, Error], Json]): F[Option[Uri]] =
2628
body
2729
.flatMap {
2830
_.hcursor
@@ -34,22 +36,27 @@ case class SttpStacClientF[F[_]: MonadThrow, S: Encoder](
3436

3537
def search: Stream[F, StacItem] = search(None)
3638

37-
def search(filter: S): Stream[F, StacItem] = search(filter.asJson.some)
39+
def search(filter: S): Stream[F, StacItem] = search(filter.some)
3840

39-
private def search(filter: Option[Json]): Stream[F, StacItem] =
41+
private def search(filter: Option[S]): Stream[F, StacItem] = {
42+
val emptyJson = JsonObject.empty.asJson
43+
// the initial filter may contain the paginationToken that is used for the initial query
44+
val initialBody = filter.map(_.asJson).getOrElse(emptyJson)
45+
// the same filter would be used as a body for all pagination requests
46+
val noPaginationBody = filter.map(paginationTokenLens.set(None)(_).asJson).getOrElse(emptyJson)
4047
Stream
41-
.unfoldLoopEval(baseUri.withPath("search")) { link =>
48+
.unfoldLoopEval((baseUri.withPath("search"), initialBody)) { case (link, request) =>
4249
client
43-
.send(filter.fold(basicRequest)(f => basicRequest.body(f.asJson.noSpaces)).post(link).response(asJson[Json]))
50+
.send(basicRequest.body(request.noSpaces).post(link).response(asJson[Json]))
4451
.flatMap { response =>
45-
val body = response.body
46-
val items = body.flatMap(_.hcursor.downField("features").as[List[StacItem]]).liftTo[F]
47-
val nextLink = getNextLink(body)
48-
49-
(items, nextLink).tupled
52+
val body = response.body
53+
val items = body.flatMap(_.hcursor.downField("features").as[List[StacItem]]).liftTo[F]
54+
val next = getNextLink(body).map(_.map(_ -> noPaginationBody))
55+
(items, next).tupled
5056
}
5157
}
5258
.flatMap(Stream.emits)
59+
}
5360

5461
def collections: Stream[F, StacCollection] =
5562
Stream
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package com.azavea.stac4s
22

3+
import com.azavea.stac4s.testing.TestInstances
4+
5+
import cats.kernel.laws.discipline.SemigroupTests
36
import org.scalatest.funsuite.AnyFunSuite
47
import org.scalatest.matchers.must.Matchers
58
import org.scalatestplus.scalacheck.Checkers
69
import org.typelevel.discipline.scalatest.FunSuiteDiscipline
710

8-
import cats.kernel.laws.discipline.SemigroupTests
9-
import com.azavea.stac4s.testing.TestInstances
10-
1111
class JsFPLawsSpec extends AnyFunSuite with FunSuiteDiscipline with Checkers with Matchers with TestInstances {
1212
checkAll("Semigroup.Bbox", SemigroupTests[Bbox].semigroup)
1313
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package com.azavea.stac4s
22

3+
import com.azavea.stac4s.testing.TestInstances
4+
5+
import cats.kernel.laws.discipline.SemigroupTests
36
import org.scalatest.funsuite.AnyFunSuite
47
import org.scalatest.matchers.must.Matchers
58
import org.scalatestplus.scalacheck.Checkers
69
import org.typelevel.discipline.scalatest.FunSuiteDiscipline
710

8-
import cats.kernel.laws.discipline.SemigroupTests
9-
import com.azavea.stac4s.testing.TestInstances
10-
1111
class JvmFPLawsSpec extends AnyFunSuite with FunSuiteDiscipline with Checkers with Matchers with TestInstances {
1212
checkAll("Semigroup.Bbox", SemigroupTests[Bbox].semigroup)
1313
}

0 commit comments

Comments
 (0)