Skip to content

Commit be07703

Browse files
committed
Add ClientJS
1 parent c732bae commit be07703

File tree

11 files changed

+576
-109
lines changed

11 files changed

+576
-109
lines changed

build.sbt

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -145,14 +145,8 @@ lazy val core = crossProject(JSPlatform, JVMPlatform)
145145
"org.typelevel" %%% "cats-kernel" % Versions.Cats
146146
)
147147
})
148-
.jvmSettings(
149-
libraryDependencies ++= coreDependenciesJVM
150-
)
151-
.jsSettings(
152-
libraryDependencies ++= Seq(
153-
"io.github.cquiroz" %% "scala-java-time" % "2.1.0"
154-
)
155-
)
148+
.jvmSettings(libraryDependencies ++= coreDependenciesJVM)
149+
.jsSettings(libraryDependencies +"io.github.cquiroz" %%% "scala-java-time" % "2.1.0")
156150

157151
lazy val coreJVM = core.jvm
158152
lazy val coreJS = core.js
@@ -209,26 +203,28 @@ lazy val client = crossProject(JSPlatform, JVMPlatform)
209203
.settings(publishSettings)
210204
.settings(
211205
libraryDependencies ++= Seq(
212-
"io.circe" %% "circe-core" % Versions.Circe,
213-
"io.circe" %% "circe-generic" % Versions.Circe,
214-
"io.circe" %% "circe-refined" % Versions.Circe,
215-
"com.chuusai" %% "shapeless" % Versions.Shapeless,
216-
"eu.timepit" %% "refined" % Versions.Refined,
217-
"org.locationtech.geotrellis" %% "geotrellis-vector" % Versions.GeoTrellis,
218-
"org.locationtech.jts" % "jts-core" % Versions.Jts,
219-
"org.typelevel" %% "cats-core" % Versions.Cats,
220-
"com.softwaremill.sttp.client3" %% "core" % Versions.Sttp,
221-
"com.softwaremill.sttp.client3" %% "circe" % Versions.Sttp,
222-
"com.softwaremill.sttp.client3" %% "json-common" % Versions.Sttp,
223-
"com.softwaremill.sttp.model" %% "core" % Versions.SttpModel,
224-
"com.softwaremill.sttp.shared" %% "core" % Versions.SttpShared,
225-
"com.softwaremill.sttp.client3" %% "http4s-backend" % Versions.Sttp % Test,
226-
"com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % Versions.Sttp % Test,
227-
"org.scalatest" %%% "scalatest" % Versions.Scalatest % Test,
228-
"io.chrisdavenport" %% "log4cats-core" % Versions.Log4Cats,
229-
"io.chrisdavenport" %% "log4cats-slf4j" % Versions.Log4Cats % Test
206+
"io.circe" %%% "circe-core" % Versions.Circe,
207+
"io.circe" %%% "circe-generic" % Versions.Circe,
208+
"io.circe" %%% "circe-refined" % Versions.Circe,
209+
"com.chuusai" %%% "shapeless" % Versions.Shapeless,
210+
"eu.timepit" %%% "refined" % Versions.Refined,
211+
"org.typelevel" %%% "cats-core" % Versions.Cats,
212+
"com.softwaremill.sttp.client3" %%% "core" % Versions.Sttp,
213+
"com.softwaremill.sttp.client3" %%% "circe" % Versions.Sttp,
214+
"com.softwaremill.sttp.client3" %%% "json-common" % Versions.Sttp,
215+
"com.softwaremill.sttp.model" %%% "core" % Versions.SttpModel,
216+
"com.softwaremill.sttp.shared" %%% "core" % Versions.SttpShared,
217+
"org.scalatest" %%% "scalatest" % Versions.Scalatest % Test,
230218
)
231219
)
220+
.jsSettings(libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % "2.1.0")
221+
.jvmSettings(libraryDependencies ++= coreDependenciesJVM)
222+
.jvmSettings(libraryDependencies ++= Seq(
223+
"io.chrisdavenport" %%% "log4cats-core" % Versions.Log4Cats,
224+
"io.chrisdavenport" %%% "log4cats-slf4j" % Versions.Log4Cats % Test,
225+
"com.softwaremill.sttp.client3" %% "http4s-backend" % Versions.Sttp % Test,
226+
"com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % Versions.Sttp % Test
227+
))
232228

233229
lazy val clientJVM = client.jvm
234230
lazy val clientJS = client.js
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package com.azavea.stac4s.api.client
2+
3+
import com.azavea.stac4s.Bbox
4+
import com.azavea.stac4s.geometry.Geometry
5+
import com.azavea.stac4s.types.TemporalExtent
6+
7+
import cats.instances.either._
8+
import cats.syntax.apply._
9+
import cats.syntax.either._
10+
import eu.timepit.refined.types.numeric.NonNegInt
11+
import io.circe._
12+
import io.circe.generic.semiauto._
13+
import io.circe.refined._
14+
15+
import java.time.Instant
16+
17+
case class SearchFilters(
18+
bbox: Option[Bbox] = None,
19+
datetime: Option[TemporalExtent] = None,
20+
intersects: Option[Geometry] = None,
21+
collections: List[String] = Nil,
22+
items: List[String] = Nil,
23+
limit: Option[NonNegInt] = None,
24+
query: Map[String, List[Query]] = Map.empty,
25+
next: Option[PaginationToken] = None
26+
)
27+
28+
object SearchFilters {
29+
30+
// TemporalExtent STAC API compatible serialization
31+
// Ported from https://github.com/azavea/franklin/
32+
private def stringToInstant(s: String): Either[Throwable, Instant] =
33+
Either.catchNonFatal(Instant.parse(s))
34+
35+
private def temporalExtentToString(te: TemporalExtent): String =
36+
te.value match {
37+
case Some(start) :: Some(end) :: _ if start != end => s"${start.toString}/${end.toString}"
38+
case Some(start) :: Some(end) :: _ if start == end => s"${start.toString}"
39+
case Some(start) :: None :: _ => s"${start.toString}/.."
40+
case None :: Some(end) :: _ => s"../${end.toString}"
41+
}
42+
43+
private def temporalExtentFromString(str: String): Either[String, TemporalExtent] = {
44+
str.split("/").toList match {
45+
case ".." :: endString :: _ =>
46+
val parsedEnd = stringToInstant(endString)
47+
parsedEnd match {
48+
case Left(_) => s"Could not decode instant: $str".asLeft
49+
case Right(end: Instant) => TemporalExtent(None, end).asRight
50+
}
51+
case startString :: ".." :: _ =>
52+
val parsedStart = stringToInstant(startString)
53+
parsedStart match {
54+
case Left(_) => s"Could not decode instant: $str".asLeft
55+
case Right(start: Instant) => TemporalExtent(start, None).asRight
56+
}
57+
case startString :: endString :: _ =>
58+
val parsedStart = stringToInstant(startString)
59+
val parsedEnd = stringToInstant(endString)
60+
(parsedStart, parsedEnd).tupled match {
61+
case Left(_) => s"Could not decode instant: $str".asLeft
62+
case Right((start: Instant, end: Instant)) => TemporalExtent(start, end).asRight
63+
}
64+
case _ =>
65+
Either.catchNonFatal(Instant.parse(str)) match {
66+
case Left(_) => s"Could not decode instant: $str".asLeft
67+
case Right(t: Instant) => TemporalExtent(t, t).asRight
68+
}
69+
}
70+
}
71+
72+
implicit val encoderTemporalExtent: Encoder[TemporalExtent] =
73+
Encoder.encodeString.contramap[TemporalExtent](temporalExtentToString)
74+
75+
implicit val decoderTemporalExtent: Decoder[TemporalExtent] =
76+
Decoder.decodeString.emap(temporalExtentFromString)
77+
78+
implicit val searchFilterDecoder: Decoder[SearchFilters] = { c =>
79+
for {
80+
bbox <- c.downField("bbox").as[Option[Bbox]]
81+
datetime <- c.downField("datetime").as[Option[TemporalExtent]]
82+
intersects <- c.downField("intersects").as[Option[Geometry]]
83+
collectionsOption <- c.downField("collections").as[Option[List[String]]]
84+
itemsOption <- c.downField("items").as[Option[List[String]]]
85+
limit <- c.downField("limit").as[Option[NonNegInt]]
86+
query <- c.get[Option[Map[String, List[Query]]]]("query")
87+
paginationToken <- c.get[Option[PaginationToken]]("next")
88+
} yield {
89+
SearchFilters(
90+
bbox,
91+
datetime,
92+
intersects,
93+
collectionsOption.getOrElse(Nil),
94+
itemsOption.getOrElse(Nil),
95+
limit,
96+
query getOrElse Map.empty,
97+
paginationToken
98+
)
99+
}
100+
}
101+
102+
implicit val searchFilterEncoder: Encoder[SearchFilters] = deriveEncoder
103+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.azavea.stac4s.api.client
2+
3+
import com.azavea.stac4s.{StacCollection, StacItem}
4+
5+
import cats.MonadError
6+
import cats.syntax.flatMap._
7+
import cats.syntax.functor._
8+
import eu.timepit.refined.types.string.NonEmptyString
9+
import io.circe.Json
10+
import io.circe.syntax._
11+
import sttp.client3._
12+
import sttp.client3.circe.asJson
13+
import sttp.model.Uri
14+
15+
case class SttpStacClient[F[_]: MonadError[*[_], Throwable]](
16+
client: SttpBackend[F, Any],
17+
baseUri: Uri
18+
) extends StacClient[F] {
19+
20+
def search(filter: SearchFilters = SearchFilters()): F[List[StacItem]] =
21+
client
22+
.send(basicRequest.post(baseUri.withPath("search")).body(filter.asJson.noSpaces).response(asJson[Json]))
23+
.map(_.body.flatMap(_.hcursor.downField("features").as[List[StacItem]]))
24+
.flatMap(MonadError[F, Throwable].fromEither)
25+
26+
def collections: F[List[StacCollection]] =
27+
client
28+
.send(basicRequest.get(baseUri.withPath("collections")).response(asJson[Json]))
29+
.map(_.body.flatMap(_.hcursor.downField("collections").as[List[StacCollection]]))
30+
.flatMap(MonadError[F, Throwable].fromEither)
31+
32+
def collection(collectionId: NonEmptyString): F[Option[StacCollection]] =
33+
client
34+
.send(
35+
basicRequest
36+
.get(baseUri.withPath("collections", collectionId.value))
37+
.response(asJson[Option[StacCollection]])
38+
)
39+
.map(_.body)
40+
.flatMap(MonadError[F, Throwable].fromEither)
41+
42+
def items(collectionId: NonEmptyString): F[List[StacItem]] =
43+
client
44+
.send(basicRequest.get(baseUri.withPath("collections", collectionId.value, "items")).response(asJson[Json]))
45+
.map(_.body.flatMap(_.hcursor.downField("features").as[List[StacItem]]))
46+
.flatMap(MonadError[F, Throwable].fromEither)
47+
48+
def item(collectionId: NonEmptyString, itemId: NonEmptyString): F[Option[StacItem]] =
49+
client
50+
.send(
51+
basicRequest
52+
.get(baseUri.withPath("collections", collectionId.value, "items", itemId.value))
53+
.response(asJson[Option[StacItem]])
54+
)
55+
.map(_.body)
56+
.flatMap(MonadError[F, Throwable].fromEither)
57+
58+
def itemCreate(collectionId: NonEmptyString, item: StacItem): F[StacItem] =
59+
client
60+
.send(
61+
basicRequest
62+
.post(baseUri.withPath("collections", collectionId.value, "items"))
63+
.body(item.asJson.noSpaces)
64+
.response(asJson[StacItem])
65+
)
66+
.map(_.body)
67+
.flatMap(MonadError[F, Throwable].fromEither)
68+
69+
def collectionCreate(collection: StacCollection): F[StacCollection] =
70+
client
71+
.send(
72+
basicRequest
73+
.post(baseUri.withPath("collections"))
74+
.body(collection.asJson.noSpaces)
75+
.response(asJson[StacCollection])
76+
)
77+
.map(_.body)
78+
.flatMap(MonadError[F, Throwable].fromEither)
79+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<configuration>
2+
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
3+
<encoder>
4+
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
5+
</encoder>
6+
</appender>
7+
8+
<root level="info">
9+
<appender-ref ref="STDOUT"/>
10+
</root>
11+
</configuration>

0 commit comments

Comments
 (0)