From 7d1c22b988b6d146fb96447500b6b869a9d9162d Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Thu, 20 Aug 2020 18:17:03 -0400 Subject: [PATCH 01/27] Add a client module --- build.sbt | 20 ++++ .../stac4s/api/client/Http4sStacClient.scala | 68 ++++++++++++ .../stac4s/api/client/PaginationToken.scala | 15 +++ .../stac4s/api/client/SearchFilters.scala | 100 ++++++++++++++++++ .../azavea/stac4s/api/client/StacClient.scala | 28 +++++ .../scala/com/azavea/stac4s/TestObject.scala | 1 + .../scala/com/azavea/stac4s/TestObject.scala | 1 + 7 files changed, 233 insertions(+) create mode 100644 modules/client/src/main/scala/com/azavea/stac4s/api/client/Http4sStacClient.scala create mode 100644 modules/client/src/main/scala/com/azavea/stac4s/api/client/PaginationToken.scala create mode 100644 modules/client/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala create mode 100644 modules/client/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala create mode 100644 modules/core/js/src/main/scala/com/azavea/stac4s/TestObject.scala create mode 100644 modules/core/jvm/src/main/scala/com/azavea/stac4s/TestObject.scala diff --git a/build.sbt b/build.sbt index f0b069fe..10a829b4 100644 --- a/build.sbt +++ b/build.sbt @@ -200,3 +200,23 @@ lazy val coreTest = crossProject(JSPlatform, JVMPlatform) lazy val coreTestJVM = coreTest.jvm lazy val coreTestJS = coreTest.js +lazy val coreTestRef = LocalProject("modules/core-test") + +lazy val client = (project in file("modules/client")) + .dependsOn(core) + .settings(commonSettings) + .settings(publishSettings) + .settings(libraryDependencies ++= coreDependencies) + .settings( + libraryDependencies ++= Seq( + "co.fs2" %% "fs2-core" % "2.4.2", + "org.http4s" %% "http4s-blaze-client" % "0.21.7", + "org.http4s" %% "http4s-circe" % "0.21.7", + "org.http4s" %% "http4s-client" % "0.21.7", + "org.http4s" %% "http4s-core" % "0.21.7", + "org.typelevel" %% "cats-effect" % "2.1.4", + "org.typelevel" %% "cats-effect" % "2.1.4", + "io.chrisdavenport" %% "vault" % "2.0.0", + "io.chrisdavenport" %% "log4cats-core" % "1.1.1" + ) + ) diff --git a/modules/client/src/main/scala/com/azavea/stac4s/api/client/Http4sStacClient.scala b/modules/client/src/main/scala/com/azavea/stac4s/api/client/Http4sStacClient.scala new file mode 100644 index 00000000..bda0c5fc --- /dev/null +++ b/modules/client/src/main/scala/com/azavea/stac4s/api/client/Http4sStacClient.scala @@ -0,0 +1,68 @@ +package com.azavea.stac4s.api.client + +import com.azavea.stac4s.{StacCollection, StacItem} +import org.http4s.Method.{GET, POST} +import org.http4s.{Request, Uri} +import org.http4s.client.Client +import io.circe.syntax._ +import org.http4s.circe._ +import cats.syntax.functor._ +import cats.syntax.either._ +import cats.syntax.apply._ +import cats.effect.{ConcurrentEffect, Resource, Sync} +import eu.timepit.refined.types.string.NonEmptyString +import org.http4s.client.blaze.BlazeClientBuilder +import io.chrisdavenport.log4cats.Logger + +import scala.concurrent.ExecutionContext + +case class Http4sStacClient[F[_]: Sync: Logger]( + client: Client[F], + baseUri: Uri +) extends StacClient[F] { + private lazy val logger = Logger[F] + private def postRequest = Request[F]().withMethod(POST) + private def getRequest = Request[F]().withMethod(GET) + + def search(filter: SearchFilters = SearchFilters()): F[List[StacItem]] = + logger.trace(s"search: ${filter.asJson.spaces4}") *> + client + .expect(postRequest.withUri(baseUri.withPath("/search")).withEntity(filter.asJson.noSpaces)) + .map(_.hcursor.downField("features").as[List[StacItem]].bimap(_ => Nil, identity).merge) + + def collections: F[List[StacCollection]] = + logger.trace("collections") *> + client + .expect(getRequest.withUri(baseUri.withPath("/collections"))) + .map(_.hcursor.downField("collections").as[List[StacCollection]].bimap(_ => Nil, identity).merge) + + def collection(collectionId: NonEmptyString): F[Option[StacCollection]] = + logger.trace(s"collection: $collectionId") *> + client + .expect(getRequest.withUri(baseUri.withPath(s"/collections/$collectionId"))) + .map(_.as[Option[StacCollection]].bimap(_ => None, identity).merge) + + def items(collectionId: NonEmptyString): F[List[StacItem]] = + logger.trace(s"items: $collectionId") *> + client + .expect(getRequest.withUri(baseUri.withPath(s"/collections/$collectionId/items"))) + .map(_.hcursor.downField("features").as[List[StacItem]].bimap(_ => Nil, identity).merge) + + def item(collectionId: NonEmptyString, itemId: NonEmptyString): F[Option[StacItem]] = + logger.trace(s"items: $collectionId, $itemId") *> + client + .expect(getRequest.withUri(baseUri.withPath(s"/collections/$collectionId/items/$itemId"))) + .map(_.as[Option[StacItem]].bimap(_ => None, identity).merge) +} + +object Http4sStacClient { + + def apply[F[_]: ConcurrentEffect: Logger](baseUri: Uri)(implicit ec: ExecutionContext): F[Http4sStacClient[F]] = { + BlazeClientBuilder[F](ec).resource.use { client => ConcurrentEffect[F].delay(Http4sStacClient[F](client, baseUri)) } + } + + def resource[F[_]: ConcurrentEffect: Logger]( + baseUri: Uri + )(implicit ec: ExecutionContext): Resource[F, Http4sStacClient[F]] = + BlazeClientBuilder[F](ec).resource.map(Http4sStacClient[F](_, baseUri)) +} diff --git a/modules/client/src/main/scala/com/azavea/stac4s/api/client/PaginationToken.scala b/modules/client/src/main/scala/com/azavea/stac4s/api/client/PaginationToken.scala new file mode 100644 index 00000000..714087ea --- /dev/null +++ b/modules/client/src/main/scala/com/azavea/stac4s/api/client/PaginationToken.scala @@ -0,0 +1,15 @@ +package com.azavea.stac4s.api.client + +import eu.timepit.refined.types.numeric.PosInt +import io.circe.generic.semiauto._ +import io.circe.refined._ +import io.circe.{Decoder, Encoder} + +import java.time.Instant + +final case class PaginationToken(timestampAtLeast: Instant, serialIdGreaterThan: PosInt) + +object PaginationToken { + implicit val dec: Decoder[PaginationToken] = deriveDecoder + implicit val enc: Encoder[PaginationToken] = deriveEncoder +} diff --git a/modules/client/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala b/modules/client/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala new file mode 100644 index 00000000..3457d614 --- /dev/null +++ b/modules/client/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala @@ -0,0 +1,100 @@ +package com.azavea.stac4s.api.client + +import com.azavea.stac4s.{Bbox, TemporalExtent} +import io.circe._ +import io.circe.generic.semiauto._ +import io.circe.refined._ +import geotrellis.vector._ +import cats.syntax.either._ +import cats.syntax.apply._ +import cats.instances.either._ +import eu.timepit.refined.types.numeric.NonNegInt + +import java.time.Instant + +case class SearchFilters( + bbox: Option[Bbox] = None, + datetime: Option[TemporalExtent] = None, + intersects: Option[Geometry] = None, + collections: List[String] = Nil, + items: List[String] = Nil, + limit: Option[NonNegInt] = None, + query: JsonObject = JsonObject.empty, + next: Option[PaginationToken] = None +) + +object SearchFilters { + + // TemporalExtent STAC API compatible serialization + private def stringToInstant(s: String): Either[Throwable, Instant] = + Either.catchNonFatal(Instant.parse(s)) + + private def temporalExtentToString(te: TemporalExtent): String = + te.value match { + case Some(start) :: Some(end) :: _ if start != end => s"${start.toString}/${end.toString}" + case Some(start) :: Some(end) :: _ if start == end => s"${start.toString}" + case Some(start) :: None :: _ => s"${start.toString}/.." + case None :: Some(end) :: _ => s"../${end.toString}" + } + + private def temporalExtentFromString(str: String): Either[String, TemporalExtent] = { + str.split("/").toList match { + case ".." :: endString :: _ => + val parsedEnd = stringToInstant(endString) + parsedEnd match { + case Left(_) => s"Could not decode instant: $str".asLeft + case Right(end: Instant) => TemporalExtent(None, end).asRight + } + case startString :: ".." :: _ => + val parsedStart = stringToInstant(startString) + parsedStart match { + case Left(_) => s"Could not decode instant: $str".asLeft + case Right(start: Instant) => TemporalExtent(start, None).asRight + } + case startString :: endString :: _ => + val parsedStart = stringToInstant(startString) + val parsedEnd = stringToInstant(endString) + (parsedStart, parsedEnd).tupled match { + case Left(_) => s"Could not decode instant: $str".asLeft + case Right((start: Instant, end: Instant)) => TemporalExtent(start, end).asRight + } + case _ => + Either.catchNonFatal(Instant.parse(str)) match { + case Left(_) => s"Could not decode instant: $str".asLeft + case Right(t: Instant) => TemporalExtent(t, t).asRight + } + } + } + + implicit val encoderTemporalExtent: Encoder[TemporalExtent] = + Encoder.encodeString.contramap[TemporalExtent](temporalExtentToString) + + implicit val decoderTemporalExtent: Decoder[TemporalExtent] = + Decoder.decodeString.emap(temporalExtentFromString) + + implicit val searchFilterDecoder: Decoder[SearchFilters] = { c => + for { + bbox <- c.downField("bbox").as[Option[Bbox]] + datetime <- c.downField("datetime").as[Option[TemporalExtent]] + intersects <- c.downField("intersects").as[Option[Geometry]] + collectionsOption <- c.downField("collections").as[Option[List[String]]] + itemsOption <- c.downField("items").as[Option[List[String]]] + limit <- c.downField("limit").as[Option[NonNegInt]] + query <- c.get[Option[JsonObject]]("query") + paginationToken <- c.get[Option[PaginationToken]]("next") + } yield { + SearchFilters( + bbox, + datetime, + intersects, + collectionsOption.getOrElse(Nil), + itemsOption.getOrElse(Nil), + limit, + query.getOrElse(JsonObject.empty), + paginationToken + ) + } + } + + implicit val searchFilterEncoder: Encoder[SearchFilters] = deriveEncoder +} diff --git a/modules/client/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala b/modules/client/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala new file mode 100644 index 00000000..67c69d3a --- /dev/null +++ b/modules/client/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala @@ -0,0 +1,28 @@ +/* + * Copyright 2020 Azavea + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.azavea.stac4s.api.client + +import com.azavea.stac4s._ +import eu.timepit.refined.types.string.NonEmptyString + +trait StacClient[F[_]] { + def search(filter: SearchFilters = SearchFilters()): F[List[StacItem]] + def collections: F[List[StacCollection]] + def collection(collectionId: NonEmptyString): F[Option[StacCollection]] + def items(collectionId: NonEmptyString): F[List[StacItem]] + def item(collectionId: NonEmptyString, itemId: NonEmptyString): F[Option[StacItem]] +} diff --git a/modules/core/js/src/main/scala/com/azavea/stac4s/TestObject.scala b/modules/core/js/src/main/scala/com/azavea/stac4s/TestObject.scala new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/modules/core/js/src/main/scala/com/azavea/stac4s/TestObject.scala @@ -0,0 +1 @@ + diff --git a/modules/core/jvm/src/main/scala/com/azavea/stac4s/TestObject.scala b/modules/core/jvm/src/main/scala/com/azavea/stac4s/TestObject.scala new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/modules/core/jvm/src/main/scala/com/azavea/stac4s/TestObject.scala @@ -0,0 +1 @@ + From c6562afdb1950af8efed143c005da87e77b13e7f Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Thu, 20 Aug 2020 18:39:07 -0400 Subject: [PATCH 02/27] Add Query from Franklin --- build.sbt | 26 +++-- .../com/azavea/stac4s/api/client/Query.scala | 107 ++++++++++++++++++ .../stac4s/api/client/SearchFilters.scala | 6 +- 3 files changed, 126 insertions(+), 13 deletions(-) create mode 100644 modules/client/src/main/scala/com/azavea/stac4s/api/client/Query.scala diff --git a/build.sbt b/build.sbt index 10a829b4..2656b365 100644 --- a/build.sbt +++ b/build.sbt @@ -206,17 +206,23 @@ lazy val client = (project in file("modules/client")) .dependsOn(core) .settings(commonSettings) .settings(publishSettings) - .settings(libraryDependencies ++= coreDependencies) .settings( libraryDependencies ++= Seq( - "co.fs2" %% "fs2-core" % "2.4.2", - "org.http4s" %% "http4s-blaze-client" % "0.21.7", - "org.http4s" %% "http4s-circe" % "0.21.7", - "org.http4s" %% "http4s-client" % "0.21.7", - "org.http4s" %% "http4s-core" % "0.21.7", - "org.typelevel" %% "cats-effect" % "2.1.4", - "org.typelevel" %% "cats-effect" % "2.1.4", - "io.chrisdavenport" %% "vault" % "2.0.0", - "io.chrisdavenport" %% "log4cats-core" % "1.1.1" + "io.circe" %% "circe-core" % Versions.CirceVersion, + "io.circe" %% "circe-generic" % Versions.CirceVersion, + "io.circe" %% "circe-refined" % Versions.CirceVersion, + "com.chuusai" %% "shapeless" % Versions.ShapelessVersion, + "eu.timepit" %% "refined" % Versions.RefinedVersion, + "org.locationtech.geotrellis" %% "geotrellis-vector" % Versions.GeoTrellisVersion, + "org.locationtech.jts" % "jts-core" % Versions.jts, + "org.typelevel" %% "cats-core" % Versions.CatsVersion, + "co.fs2" %% "fs2-core" % "2.4.2", + "org.http4s" %% "http4s-blaze-client" % "0.21.7", + "org.http4s" %% "http4s-circe" % "0.21.7", + "org.http4s" %% "http4s-client" % "0.21.7", + "org.http4s" %% "http4s-core" % "0.21.7", + "org.typelevel" %% "cats-effect" % "2.1.4", + "io.chrisdavenport" %% "vault" % "2.0.0", + "io.chrisdavenport" %% "log4cats-core" % "1.1.1" ) ) diff --git a/modules/client/src/main/scala/com/azavea/stac4s/api/client/Query.scala b/modules/client/src/main/scala/com/azavea/stac4s/api/client/Query.scala new file mode 100644 index 00000000..e89e43b8 --- /dev/null +++ b/modules/client/src/main/scala/com/azavea/stac4s/api/client/Query.scala @@ -0,0 +1,107 @@ +package com.azavea.stac4s.api.client + +import cats.data.NonEmptyVector +import cats.implicits._ +import eu.timepit.refined.types.string.NonEmptyString +import io.circe._ +import io.circe.syntax._ +import io.circe.refined._ + +sealed abstract class Query + +case class Equals(value: Json) extends Query +case class NotEqualTo(value: Json) extends Query +case class GreaterThan(floor: Json) extends Query +case class GreaterThanEqual(floor: Json) extends Query +case class LessThan(ceiling: Json) extends Query +case class LessThanEqual(ceiling: Json) extends Query +case class StartsWith(prefix: NonEmptyString) extends Query +case class EndsWith(postfix: NonEmptyString) extends Query +case class Contains(substring: NonEmptyString) extends Query +case class In(values: NonEmptyVector[Json]) extends Query +case class Superset(values: NonEmptyVector[Json]) extends Query + +object Query { + + private def fromString(s: String, f: NonEmptyString => Query): Option[Query] = + NonEmptyString.from(s).toOption.map(f) + + private def fromStringOrNum(js: Json, f: Json => Query): Option[Query] = + js.asNumber map { _ => + f(js) + } orElse { js.asString map { _ => f(js) } } + + private def errMessage(operator: String, json: Json): String = + s"Cannot construct `$operator` query with $json" + + def queriesFromMap(unparsed: Map[String, Json]): Either[String, List[Query]] = + (unparsed.toList traverse { + case (op @ "eq", json) => Right(Equals(json)) + case (op @ "neq", json) => Right(NotEqualTo(json)) + case (op @ "lt", json) => + Either.fromOption(fromStringOrNum(json, LessThan.apply), errMessage(op, json)) + case (op @ "lte", json) => + Either.fromOption( + fromStringOrNum(json, LessThanEqual.apply), + errMessage(op, json) + ) + case (op @ "gt", json) => + Either.fromOption( + fromStringOrNum(json, GreaterThan.apply), + errMessage(op, json) + ) + case (op @ "gte", json) => + Either.fromOption( + fromStringOrNum(json, GreaterThanEqual.apply), + errMessage(op, json) + ) + case (op @ "startsWith", json) => + Either.fromOption( + json.asString flatMap { fromString(_, StartsWith.apply) }, + errMessage(op, json) + ) + case (op @ "endsWith", json) => + Either.fromOption( + json.asString flatMap { fromString(_, EndsWith.apply) }, + errMessage(op, json) + ) + case (op @ "contains", json) => + Either.fromOption( + json.asString flatMap { fromString(_, Contains.apply) }, + errMessage(op, json) + ) + case (op @ "in", json) => + Either.fromOption( + json.asArray flatMap { _.toNev } map { vec => In(vec) }, + errMessage(op, json) + ) + case (op @ "superset", json) => + Either.fromOption( + json.asArray flatMap { _.toNev } map { vec => Superset(vec) }, + errMessage(op, json) + ) + case (k, _) => Left(s"$k is not a valid operator") + }) + + implicit val encQuery: Encoder[List[Query]] = new Encoder[List[Query]] { + + def apply(queries: List[Query]): Json = + Map( + (queries map { + case Equals(value) => "eq" -> value.asJson + case NotEqualTo(value) => "neq" -> value.asJson + case GreaterThan(floor) => "gt" -> floor.asJson + case GreaterThanEqual(floor) => "gte" -> floor.asJson + case LessThan(ceiling) => "lt" -> ceiling.asJson + case LessThanEqual(ceiling) => "lte" -> ceiling.asJson + case StartsWith(prefix) => "startsWith" -> prefix.asJson + case EndsWith(postfix) => "endsWith" -> postfix.asJson + case Contains(substring) => "contains" -> substring.asJson + case In(values) => "in" -> values.asJson + case Superset(values) => "superset" -> values.asJson + }): _* + ).asJson + } + + implicit val decQueries: Decoder[List[Query]] = Decoder[JsonObject].emap { jsonObj => queriesFromMap(jsonObj.toMap) } +} diff --git a/modules/client/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala b/modules/client/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala index 3457d614..3cc5ad08 100644 --- a/modules/client/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala +++ b/modules/client/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala @@ -19,7 +19,7 @@ case class SearchFilters( collections: List[String] = Nil, items: List[String] = Nil, limit: Option[NonNegInt] = None, - query: JsonObject = JsonObject.empty, + query: Map[String, List[Query]] = Map.empty, next: Option[PaginationToken] = None ) @@ -80,7 +80,7 @@ object SearchFilters { collectionsOption <- c.downField("collections").as[Option[List[String]]] itemsOption <- c.downField("items").as[Option[List[String]]] limit <- c.downField("limit").as[Option[NonNegInt]] - query <- c.get[Option[JsonObject]]("query") + query <- c.get[Option[Map[String, List[Query]]]]("query") paginationToken <- c.get[Option[PaginationToken]]("next") } yield { SearchFilters( @@ -90,7 +90,7 @@ object SearchFilters { collectionsOption.getOrElse(Nil), itemsOption.getOrElse(Nil), limit, - query.getOrElse(JsonObject.empty), + query getOrElse Map.empty, paginationToken ) } From 76304125290c630ed3935dd2f2805fff79146141 Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Wed, 23 Dec 2020 12:00:44 -0500 Subject: [PATCH 03/27] Upd up to the main branch --- build.sbt | 12 ++++-- .../stac4s/api/client/Http4sStacClient.scala | 19 +++++----- .../stac4s/api/client/PaginationToken.scala | 0 .../com/azavea/stac4s/api/client/Query.scala | 37 +++++++++---------- .../stac4s/api/client/SearchFilters.scala | 15 +++++--- .../azavea/stac4s/api/client/StacClient.scala | 1 + 6 files changed, 46 insertions(+), 38 deletions(-) rename modules/client/{ => shared}/src/main/scala/com/azavea/stac4s/api/client/Http4sStacClient.scala (99%) rename modules/client/{ => shared}/src/main/scala/com/azavea/stac4s/api/client/PaginationToken.scala (100%) rename modules/client/{ => shared}/src/main/scala/com/azavea/stac4s/api/client/Query.scala (76%) rename modules/client/{ => shared}/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala (95%) rename modules/client/{ => shared}/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala (99%) diff --git a/build.sbt b/build.sbt index 2656b365..9ff49494 100644 --- a/build.sbt +++ b/build.sbt @@ -125,7 +125,7 @@ lazy val root = project .settings(commonSettings) .settings(publishSettings) .settings(noPublishSettings) - .aggregate(coreJS, coreJVM, testingJS, testingJVM, coreTestJS, coreTestJVM) + .aggregate(coreJS, coreJVM, testingJS, testingJVM, coreTestJS, coreTestJVM, clientJS, clientJVM) lazy val core = crossProject(JSPlatform, JVMPlatform) .in(file("modules/core")) @@ -157,7 +157,7 @@ lazy val core = crossProject(JSPlatform, JVMPlatform) lazy val coreJVM = core.jvm lazy val coreJS = core.js -lazy val testing = (crossProject(JSPlatform, JVMPlatform)) +lazy val testing = crossProject(JSPlatform, JVMPlatform) .in(file("modules/testing")) .dependsOn(core) .settings(commonSettings) @@ -202,7 +202,8 @@ lazy val coreTestJVM = coreTest.jvm lazy val coreTestJS = coreTest.js lazy val coreTestRef = LocalProject("modules/core-test") -lazy val client = (project in file("modules/client")) +lazy val client = crossProject(JSPlatform, JVMPlatform) + .in(file("modules/client")) .dependsOn(core) .settings(commonSettings) .settings(publishSettings) @@ -214,7 +215,7 @@ lazy val client = (project in file("modules/client")) "com.chuusai" %% "shapeless" % Versions.ShapelessVersion, "eu.timepit" %% "refined" % Versions.RefinedVersion, "org.locationtech.geotrellis" %% "geotrellis-vector" % Versions.GeoTrellisVersion, - "org.locationtech.jts" % "jts-core" % Versions.jts, + "org.locationtech.jts" % "jts-core" % Versions.Jts, "org.typelevel" %% "cats-core" % Versions.CatsVersion, "co.fs2" %% "fs2-core" % "2.4.2", "org.http4s" %% "http4s-blaze-client" % "0.21.7", @@ -226,3 +227,6 @@ lazy val client = (project in file("modules/client")) "io.chrisdavenport" %% "log4cats-core" % "1.1.1" ) ) + +lazy val clientJVM = client.jvm +lazy val clientJS = client.js diff --git a/modules/client/src/main/scala/com/azavea/stac4s/api/client/Http4sStacClient.scala b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/Http4sStacClient.scala similarity index 99% rename from modules/client/src/main/scala/com/azavea/stac4s/api/client/Http4sStacClient.scala rename to modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/Http4sStacClient.scala index bda0c5fc..f82d1ad6 100644 --- a/modules/client/src/main/scala/com/azavea/stac4s/api/client/Http4sStacClient.scala +++ b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/Http4sStacClient.scala @@ -1,18 +1,19 @@ package com.azavea.stac4s.api.client import com.azavea.stac4s.{StacCollection, StacItem} -import org.http4s.Method.{GET, POST} -import org.http4s.{Request, Uri} -import org.http4s.client.Client -import io.circe.syntax._ -import org.http4s.circe._ -import cats.syntax.functor._ -import cats.syntax.either._ -import cats.syntax.apply._ + import cats.effect.{ConcurrentEffect, Resource, Sync} +import cats.syntax.apply._ +import cats.syntax.either._ +import cats.syntax.functor._ import eu.timepit.refined.types.string.NonEmptyString -import org.http4s.client.blaze.BlazeClientBuilder import io.chrisdavenport.log4cats.Logger +import io.circe.syntax._ +import org.http4s.Method.{GET, POST} +import org.http4s.circe._ +import org.http4s.client.Client +import org.http4s.client.blaze.BlazeClientBuilder +import org.http4s.{Request, Uri} import scala.concurrent.ExecutionContext diff --git a/modules/client/src/main/scala/com/azavea/stac4s/api/client/PaginationToken.scala b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/PaginationToken.scala similarity index 100% rename from modules/client/src/main/scala/com/azavea/stac4s/api/client/PaginationToken.scala rename to modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/PaginationToken.scala diff --git a/modules/client/src/main/scala/com/azavea/stac4s/api/client/Query.scala b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/Query.scala similarity index 76% rename from modules/client/src/main/scala/com/azavea/stac4s/api/client/Query.scala rename to modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/Query.scala index e89e43b8..58ce4ea6 100644 --- a/modules/client/src/main/scala/com/azavea/stac4s/api/client/Query.scala +++ b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/Query.scala @@ -4,9 +4,10 @@ import cats.data.NonEmptyVector import cats.implicits._ import eu.timepit.refined.types.string.NonEmptyString import io.circe._ -import io.circe.syntax._ import io.circe.refined._ +import io.circe.syntax._ +/** https://github.com/azavea/franklin/blob/286c5c755585cf743eae5bd176609d8c125ad2b9/application/src/main/scala/com/azavea/franklin/datamodel/Query.scala */ sealed abstract class Query case class Equals(value: Json) extends Query @@ -83,24 +84,22 @@ object Query { case (k, _) => Left(s"$k is not a valid operator") }) - implicit val encQuery: Encoder[List[Query]] = new Encoder[List[Query]] { - - def apply(queries: List[Query]): Json = - Map( - (queries map { - case Equals(value) => "eq" -> value.asJson - case NotEqualTo(value) => "neq" -> value.asJson - case GreaterThan(floor) => "gt" -> floor.asJson - case GreaterThanEqual(floor) => "gte" -> floor.asJson - case LessThan(ceiling) => "lt" -> ceiling.asJson - case LessThanEqual(ceiling) => "lte" -> ceiling.asJson - case StartsWith(prefix) => "startsWith" -> prefix.asJson - case EndsWith(postfix) => "endsWith" -> postfix.asJson - case Contains(substring) => "contains" -> substring.asJson - case In(values) => "in" -> values.asJson - case Superset(values) => "superset" -> values.asJson - }): _* - ).asJson + implicit val encQuery: Encoder[List[Query]] = { queries => + Map( + queries map { + case Equals(value) => "eq" -> value.asJson + case NotEqualTo(value) => "neq" -> value.asJson + case GreaterThan(floor) => "gt" -> floor.asJson + case GreaterThanEqual(floor) => "gte" -> floor.asJson + case LessThan(ceiling) => "lt" -> ceiling.asJson + case LessThanEqual(ceiling) => "lte" -> ceiling.asJson + case StartsWith(prefix) => "startsWith" -> prefix.asJson + case EndsWith(postfix) => "endsWith" -> postfix.asJson + case Contains(substring) => "contains" -> substring.asJson + case In(values) => "in" -> values.asJson + case Superset(values) => "superset" -> values.asJson + }: _* + ).asJson } implicit val decQueries: Decoder[List[Query]] = Decoder[JsonObject].emap { jsonObj => queriesFromMap(jsonObj.toMap) } diff --git a/modules/client/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala similarity index 95% rename from modules/client/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala rename to modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala index 3cc5ad08..0fca422c 100644 --- a/modules/client/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala +++ b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala @@ -1,14 +1,16 @@ package com.azavea.stac4s.api.client -import com.azavea.stac4s.{Bbox, TemporalExtent} +import com.azavea.stac4s.Bbox +import com.azavea.stac4s.types.TemporalExtent + +import cats.instances.either._ +import cats.syntax.apply._ +import cats.syntax.either._ +import eu.timepit.refined.types.numeric.NonNegInt +import geotrellis.vector.{io => _, _} import io.circe._ import io.circe.generic.semiauto._ import io.circe.refined._ -import geotrellis.vector._ -import cats.syntax.either._ -import cats.syntax.apply._ -import cats.instances.either._ -import eu.timepit.refined.types.numeric.NonNegInt import java.time.Instant @@ -26,6 +28,7 @@ case class SearchFilters( object SearchFilters { // TemporalExtent STAC API compatible serialization + // Ported from https://github.com/azavea/franklin/ private def stringToInstant(s: String): Either[Throwable, Instant] = Either.catchNonFatal(Instant.parse(s)) diff --git a/modules/client/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala similarity index 99% rename from modules/client/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala rename to modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala index 67c69d3a..2605325b 100644 --- a/modules/client/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala +++ b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala @@ -17,6 +17,7 @@ package com.azavea.stac4s.api.client import com.azavea.stac4s._ + import eu.timepit.refined.types.string.NonEmptyString trait StacClient[F[_]] { From 31a0bf0500a7a6e83afd20ff857bb648eb099231 Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Wed, 23 Dec 2020 12:35:26 -0500 Subject: [PATCH 04/27] Add item and collection creation endpoints --- build.sbt | 33 ++++++++++--------- .../stac4s/api/client/Http4sStacClient.scala | 21 ++++++++++++ .../azavea/stac4s/api/client/StacClient.scala | 2 ++ 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/build.sbt b/build.sbt index 9ff49494..5390b251 100644 --- a/build.sbt +++ b/build.sbt @@ -209,22 +209,23 @@ lazy val client = crossProject(JSPlatform, JVMPlatform) .settings(publishSettings) .settings( libraryDependencies ++= Seq( - "io.circe" %% "circe-core" % Versions.CirceVersion, - "io.circe" %% "circe-generic" % Versions.CirceVersion, - "io.circe" %% "circe-refined" % Versions.CirceVersion, - "com.chuusai" %% "shapeless" % Versions.ShapelessVersion, - "eu.timepit" %% "refined" % Versions.RefinedVersion, - "org.locationtech.geotrellis" %% "geotrellis-vector" % Versions.GeoTrellisVersion, - "org.locationtech.jts" % "jts-core" % Versions.Jts, - "org.typelevel" %% "cats-core" % Versions.CatsVersion, - "co.fs2" %% "fs2-core" % "2.4.2", - "org.http4s" %% "http4s-blaze-client" % "0.21.7", - "org.http4s" %% "http4s-circe" % "0.21.7", - "org.http4s" %% "http4s-client" % "0.21.7", - "org.http4s" %% "http4s-core" % "0.21.7", - "org.typelevel" %% "cats-effect" % "2.1.4", - "io.chrisdavenport" %% "vault" % "2.0.0", - "io.chrisdavenport" %% "log4cats-core" % "1.1.1" + "io.circe" %% "circe-core" % Versions.CirceVersion, + "io.circe" %% "circe-generic" % Versions.CirceVersion, + "io.circe" %% "circe-refined" % Versions.CirceVersion, + "com.chuusai" %% "shapeless" % Versions.ShapelessVersion, + "eu.timepit" %% "refined" % Versions.RefinedVersion, + "org.locationtech.geotrellis" %% "geotrellis-vector" % Versions.GeoTrellisVersion, + "org.locationtech.jts" % "jts-core" % Versions.Jts, + "org.typelevel" %% "cats-core" % Versions.CatsVersion, + "com.softwaremill.sttp.client3" %% "core" % "3.0.0-RC13", + "co.fs2" %% "fs2-core" % "2.4.2", + "org.http4s" %% "http4s-blaze-client" % "0.21.7", + "org.http4s" %% "http4s-circe" % "0.21.7", + "org.http4s" %% "http4s-client" % "0.21.7", + "org.http4s" %% "http4s-core" % "0.21.7", + "org.typelevel" %% "cats-effect" % "2.1.4", + "io.chrisdavenport" %% "vault" % "2.0.0", + "io.chrisdavenport" %% "log4cats-core" % "1.1.1" ) ) diff --git a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/Http4sStacClient.scala b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/Http4sStacClient.scala index f82d1ad6..3f6cfd6c 100644 --- a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/Http4sStacClient.scala +++ b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/Http4sStacClient.scala @@ -5,6 +5,7 @@ import com.azavea.stac4s.{StacCollection, StacItem} import cats.effect.{ConcurrentEffect, Resource, Sync} import cats.syntax.apply._ import cats.syntax.either._ +import cats.syntax.flatMap._ import cats.syntax.functor._ import eu.timepit.refined.types.string.NonEmptyString import io.chrisdavenport.log4cats.Logger @@ -54,6 +55,26 @@ case class Http4sStacClient[F[_]: Sync: Logger]( client .expect(getRequest.withUri(baseUri.withPath(s"/collections/$collectionId/items/$itemId"))) .map(_.as[Option[StacItem]].bimap(_ => None, identity).merge) + + def itemCreate(collectionId: NonEmptyString, item: StacItem): F[StacItem] = + logger.trace(s"createItem: $collectionId, $item") *> + client + .expect( + postRequest + .withUri(baseUri.withPath(s"/collections/$collectionId/items")) + .withEntity(item.asJson.noSpaces) + ) + .flatMap { json => Sync[F].fromEither(json.as[StacItem].leftMap(_.getCause)) } + + def collectionCreate(collection: StacCollection): F[StacCollection] = + logger.trace(s"createCollection: $collection") *> + client + .expect( + postRequest + .withUri(baseUri.withPath(s"/collections/")) + .withEntity(collection.asJson.noSpaces) + ) + .flatMap { json => Sync[F].fromEither(json.as[StacCollection].leftMap(_.getCause)) } } object Http4sStacClient { diff --git a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala index 2605325b..8ddd6e1d 100644 --- a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala +++ b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala @@ -26,4 +26,6 @@ trait StacClient[F[_]] { def collection(collectionId: NonEmptyString): F[Option[StacCollection]] def items(collectionId: NonEmptyString): F[List[StacItem]] def item(collectionId: NonEmptyString, itemId: NonEmptyString): F[Option[StacItem]] + def itemCreate(collectionId: NonEmptyString, item: StacItem): F[StacItem] + def collectionCreate(collection: StacCollection): F[StacCollection] } From c732bae06116d9917930c437793ccce734e2c175 Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Wed, 23 Dec 2020 15:21:18 -0500 Subject: [PATCH 05/27] Move to sttp client to have some layer of abstraction over the client library --- build.sbt | 89 +++--- .../client/jvm/src/test/resources/logback.xml | 11 + .../src/test/scala/com/azavea/IOSpec.scala | 22 ++ .../stac4s/api/client/StacClientSpec.scala | 279 ++++++++++++++++++ .../stac4s/api/client/Http4sStacClient.scala | 90 ------ .../azavea/stac4s/api/client/StacClient.scala | 1 + .../stac4s/api/client/SttpStacClient.scala | 86 ++++++ .../scala/com/azavea/stac4s/TestObject.scala | 1 - .../scala/com/azavea/stac4s/TestObject.scala | 1 - project/Versions.scala | 26 +- 10 files changed, 459 insertions(+), 147 deletions(-) create mode 100644 modules/client/jvm/src/test/resources/logback.xml create mode 100644 modules/client/jvm/src/test/scala/com/azavea/IOSpec.scala create mode 100644 modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala delete mode 100644 modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/Http4sStacClient.scala create mode 100644 modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala delete mode 100644 modules/core/js/src/main/scala/com/azavea/stac4s/TestObject.scala delete mode 100644 modules/core/jvm/src/main/scala/com/azavea/stac4s/TestObject.scala diff --git a/build.sbt b/build.sbt index 5390b251..c4457e92 100644 --- a/build.sbt +++ b/build.sbt @@ -12,10 +12,10 @@ lazy val commonSettings = Seq( else git.gitDescribedVersion.value.get }, - scalaVersion := "2.12.11", + scalaVersion := "2.12.12", cancelable in Global := true, scalafmtOnCompile := true, - scapegoatVersion in ThisBuild := Versions.ScapegoatVersion, + scapegoatVersion in ThisBuild := Versions.Scapegoat, scapegoatDisabledInspections := Seq("ObjectNames", "EmptyCaseClass"), unusedCompileDependenciesFilter -= moduleFilter("com.sksamuel.scapegoat", "scalac-scapegoat-plugin"), addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.11.2" cross CrossVersion.full), @@ -105,17 +105,17 @@ lazy val credentialSettings = Seq( val coreDependenciesJVM = Seq( "org.locationtech.jts" % "jts-core" % Versions.Jts, - "org.locationtech.geotrellis" %% "geotrellis-vector" % Versions.GeoTrellisVersion + "org.locationtech.geotrellis" %% "geotrellis-vector" % Versions.GeoTrellis ) val testingDependenciesJVM = Seq( - "org.locationtech.geotrellis" %% "geotrellis-vector" % Versions.GeoTrellisVersion, + "org.locationtech.geotrellis" %% "geotrellis-vector" % Versions.GeoTrellis, "org.locationtech.jts" % "jts-core" % Versions.Jts ) val testRunnerDependenciesJVM = Seq( - "io.circe" %% "circe-testing" % Versions.CirceVersion % Test, - "org.scalatest" %% "scalatest" % Versions.ScalatestVersion % Test, + "io.circe" %% "circe-testing" % Versions.Circe % Test, + "org.scalatest" %% "scalatest" % Versions.Scalatest % Test, "org.scalatestplus" %% "scalacheck-1-14" % Versions.ScalatestPlusScalacheck % Test ) @@ -133,16 +133,16 @@ lazy val core = crossProject(JSPlatform, JVMPlatform) .settings(publishSettings) .settings({ libraryDependencies ++= Seq( - "com.beachape" %%% "enumeratum" % Versions.EnumeratumVersion, - "com.beachape" %%% "enumeratum-circe" % Versions.EnumeratumVersion, - "com.chuusai" %%% "shapeless" % Versions.ShapelessVersion, - "eu.timepit" %%% "refined" % Versions.RefinedVersion, - "io.circe" %%% "circe-core" % Versions.CirceVersion, - "io.circe" %%% "circe-generic" % Versions.CirceVersion, - "io.circe" %%% "circe-parser" % Versions.CirceVersion, - "io.circe" %%% "circe-refined" % Versions.CirceVersion, - "org.typelevel" %%% "cats-core" % Versions.CatsVersion, - "org.typelevel" %%% "cats-kernel" % Versions.CatsVersion + "com.beachape" %%% "enumeratum" % Versions.Enumeratum, + "com.beachape" %%% "enumeratum-circe" % Versions.Enumeratum, + "com.chuusai" %%% "shapeless" % Versions.Shapeless, + "eu.timepit" %%% "refined" % Versions.Refined, + "io.circe" %%% "circe-core" % Versions.Circe, + "io.circe" %%% "circe-generic" % Versions.Circe, + "io.circe" %%% "circe-parser" % Versions.Circe, + "io.circe" %%% "circe-refined" % Versions.Circe, + "org.typelevel" %%% "cats-core" % Versions.Cats, + "org.typelevel" %%% "cats-kernel" % Versions.Cats ) }) .jvmSettings( @@ -164,15 +164,15 @@ lazy val testing = crossProject(JSPlatform, JVMPlatform) .settings(publishSettings) .settings( libraryDependencies ++= Seq( - "com.beachape" %%% "enumeratum" % Versions.EnumeratumVersion, - "com.beachape" %%% "enumeratum-scalacheck" % Versions.EnumeratumVersion, - "com.chuusai" %%% "shapeless" % Versions.ShapelessVersion, - "eu.timepit" %%% "refined-scalacheck" % Versions.RefinedVersion, - "eu.timepit" %%% "refined" % Versions.RefinedVersion, - "io.chrisdavenport" %%% "cats-scalacheck" % Versions.ScalacheckCatsVersion, - "io.circe" %%% "circe-core" % Versions.CirceVersion, - "org.scalacheck" %%% "scalacheck" % Versions.ScalacheckVersion, - "org.typelevel" %%% "cats-core" % Versions.CatsVersion + "com.beachape" %%% "enumeratum" % Versions.Enumeratum, + "com.beachape" %%% "enumeratum-scalacheck" % Versions.Enumeratum, + "com.chuusai" %%% "shapeless" % Versions.Shapeless, + "eu.timepit" %%% "refined-scalacheck" % Versions.Refined, + "eu.timepit" %%% "refined" % Versions.Refined, + "io.chrisdavenport" %%% "cats-scalacheck" % Versions.ScalacheckCats, + "io.circe" %%% "circe-core" % Versions.Circe, + "org.scalacheck" %%% "scalacheck" % Versions.Scalacheck, + "org.typelevel" %%% "cats-core" % Versions.Cats ) ) .jvmSettings(libraryDependencies ++= testingDependenciesJVM) @@ -187,8 +187,8 @@ lazy val coreTest = crossProject(JSPlatform, JVMPlatform) .settings(noPublishSettings) .settings( libraryDependencies ++= Seq( - "io.circe" %%% "circe-testing" % Versions.CirceVersion % Test, - "org.scalatest" %%% "scalatest" % Versions.ScalatestVersion % Test, + "io.circe" %%% "circe-testing" % Versions.Circe % Test, + "org.scalatest" %%% "scalatest" % Versions.Scalatest % Test, "org.scalatestplus" %%% "scalacheck-1-14" % Versions.ScalatestPlusScalacheck % Test ) ) @@ -209,23 +209,24 @@ lazy val client = crossProject(JSPlatform, JVMPlatform) .settings(publishSettings) .settings( libraryDependencies ++= Seq( - "io.circe" %% "circe-core" % Versions.CirceVersion, - "io.circe" %% "circe-generic" % Versions.CirceVersion, - "io.circe" %% "circe-refined" % Versions.CirceVersion, - "com.chuusai" %% "shapeless" % Versions.ShapelessVersion, - "eu.timepit" %% "refined" % Versions.RefinedVersion, - "org.locationtech.geotrellis" %% "geotrellis-vector" % Versions.GeoTrellisVersion, - "org.locationtech.jts" % "jts-core" % Versions.Jts, - "org.typelevel" %% "cats-core" % Versions.CatsVersion, - "com.softwaremill.sttp.client3" %% "core" % "3.0.0-RC13", - "co.fs2" %% "fs2-core" % "2.4.2", - "org.http4s" %% "http4s-blaze-client" % "0.21.7", - "org.http4s" %% "http4s-circe" % "0.21.7", - "org.http4s" %% "http4s-client" % "0.21.7", - "org.http4s" %% "http4s-core" % "0.21.7", - "org.typelevel" %% "cats-effect" % "2.1.4", - "io.chrisdavenport" %% "vault" % "2.0.0", - "io.chrisdavenport" %% "log4cats-core" % "1.1.1" + "io.circe" %% "circe-core" % Versions.Circe, + "io.circe" %% "circe-generic" % Versions.Circe, + "io.circe" %% "circe-refined" % Versions.Circe, + "com.chuusai" %% "shapeless" % Versions.Shapeless, + "eu.timepit" %% "refined" % Versions.Refined, + "org.locationtech.geotrellis" %% "geotrellis-vector" % Versions.GeoTrellis, + "org.locationtech.jts" % "jts-core" % Versions.Jts, + "org.typelevel" %% "cats-core" % Versions.Cats, + "com.softwaremill.sttp.client3" %% "core" % Versions.Sttp, + "com.softwaremill.sttp.client3" %% "circe" % Versions.Sttp, + "com.softwaremill.sttp.client3" %% "json-common" % Versions.Sttp, + "com.softwaremill.sttp.model" %% "core" % Versions.SttpModel, + "com.softwaremill.sttp.shared" %% "core" % Versions.SttpShared, + "com.softwaremill.sttp.client3" %% "http4s-backend" % Versions.Sttp % Test, + "com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % Versions.Sttp % Test, + "org.scalatest" %%% "scalatest" % Versions.Scalatest % Test, + "io.chrisdavenport" %% "log4cats-core" % Versions.Log4Cats, + "io.chrisdavenport" %% "log4cats-slf4j" % Versions.Log4Cats % Test ) ) diff --git a/modules/client/jvm/src/test/resources/logback.xml b/modules/client/jvm/src/test/resources/logback.xml new file mode 100644 index 00000000..0120ed6a --- /dev/null +++ b/modules/client/jvm/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + + + + + + + diff --git a/modules/client/jvm/src/test/scala/com/azavea/IOSpec.scala b/modules/client/jvm/src/test/scala/com/azavea/IOSpec.scala new file mode 100644 index 00000000..018bcb85 --- /dev/null +++ b/modules/client/jvm/src/test/scala/com/azavea/IOSpec.scala @@ -0,0 +1,22 @@ +package com.azavea + +import cats.effect.{ContextShift, IO, Timer} +import io.chrisdavenport.log4cats.Logger +import io.chrisdavenport.log4cats.slf4j.Slf4jLogger +import org.scalatest.funspec.AsyncFunSpec +import org.scalatest.matchers.should.Matchers +import org.scalatest.{Assertion, Assertions} + +trait IOSpec extends AsyncFunSpec with Assertions with Matchers { + implicit val contextShift: ContextShift[IO] = IO.contextShift(executionContext) + implicit val timer: Timer[IO] = IO.timer(executionContext) + implicit val logger: Logger[IO] = Slf4jLogger.getLogger[IO] + + private val itWord = new ItWord + + def it(name: String)(test: => IO[Assertion]): Unit = itWord.apply(name)(test.unsafeToFuture()) + + def ignore(name: String)(test: => IO[Assertion]): Unit = super.ignore(name)(test.unsafeToFuture()) + + def describe(description: String)(fun: => Unit): Unit = super.describe(description)(fun) +} diff --git a/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala b/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala new file mode 100644 index 00000000..8d84c2fd --- /dev/null +++ b/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala @@ -0,0 +1,279 @@ +package com.azavea.stac4s.api.client + +import cats.effect.{Blocker, IO} +import cats.syntax.either._ +import com.azavea.IOSpec +import eu.timepit.refined.collection.Empty +import eu.timepit.refined.types.all.NonEmptyString +import io.circe.parser._ +import sttp.client3.asynchttpclient.cats.AsyncHttpClientCatsBackend +import sttp.client3.http4s.Http4sBackend +import sttp.client3.impl.cats.CatsMonadAsyncError +import sttp.client3.testing.SttpBackendStub +import sttp.client3.{Response, UriContext} +import sttp.model.StatusCode + +class StacClientSpec extends IOSpec { + + lazy val backend: SttpBackendStub[IO, Nothing] = + SttpBackendStub(new CatsMonadAsyncError[IO]()) + .whenRequestMatches(_.uri.path == Seq("collections")) + .thenRespondF { _ => + IO( + Response( + Right(parse(""" + |{ + | "collections":[ + | { + | "stac_version":"1.0.0-beta.2", + | "stac_extensions":[ + | + | ], + | "id":"aviris_2006", + | "title":null, + | "description":"aviris_2006", + | "keywords":[ + | + | ], + | "license":"proprietary", + | "providers":[ + | + | ], + | "extent":{ + | "spatial":{ + | "bbox":[ + | [ + | -122.857491, + | 32.093266, + | -76.55229, + | 48.142484 + | ] + | ] + | }, + | "temporal":{ + | "interval":[ + | [ + | "2006-04-26T17:52:00Z", + | "2006-11-15T19:42:00Z" + | ] + | ] + | } + | }, + | "summaries":{ + | + | }, + | "properties":{ + | + | }, + | "links":[ + | { + | "href":"http://localhost:9090/collections/aviris_2006/items", + | "rel":"items", + | "type":"application/json", + | "title":null + | }, + | { + | "href":"http://localhost:9090/collections/aviris_2006", + | "rel":"self", + | "type":"application/json", + | "title":null + | } + | ] + | } + | ] + |} + |""".stripMargin).valueOr(throw _)), + StatusCode.Ok, + "" + ) + ) + } + .whenRequestMatches(_.uri.path == Seq("collections", "aviris_2006", "items")) + .thenRespondF { _ => + IO( + Response( + Right(parse(""" + |{ + | "type":"FeatureCollection", + | "features":[ + | { + | "id":"aviris_f060426t01p00r03_sc01", + | "stac_version":"1.0.0-beta.2", + | "stac_extensions":[ + | + | ], + | "type":"Feature", + | "geometry":{ + | "type":"Polygon", + | "coordinates":[ + | [ + | [ + | -107.771817, + | 37.913396 + | ], + | [ + | -107.739984, + | 37.914142 + | ], + | [ + | -107.744691, + | 38.040563 + | ], + | [ + | -107.776579, + | 38.039814 + | ], + | [ + | -107.771817, + | 37.913396 + | ] + | ] + | ] + | }, + | "bbox":[ + | -107.776579, + | 37.913396, + | -107.739984, + | 38.040563 + | ], + | "links":[ + | { + | "href":"http://localhost:9090/collections/aviris_2006_60426", + | "rel":"collection", + | "type":"application/json", + | "title":null + | }, + | { + | "href":"http://localhost:9090/collections/aviris_2006_60426/items/aviris_f060426t01p00r03_sc01", + | "rel":"self", + | "type":"application/json", + | "title":null + | } + | ], + | "assets":{ + | "ftp":{ + | "href":"ftp://avoil:Gulf0il$pill@popo.jpl.nasa.gov/y06_data/f060426t01p00r03.tar.gz", + | "title":"ftp", + | "description":"AVIRIS data archive. The file size is described by the 'Gzip File Size' property.", + | "roles":[ + | + | ], + | "type":"application/gzip" + | }, + | "rgb":{ + | "href":"http://aviris.jpl.nasa.gov/aviris_locator/y06_RGB/f060426t01p00r03_sc01_RGB.jpeg", + | "title":"rgb", + | "description":"Full resolution RGB image captured by the flight", + | "roles":[ + | + | ], + | "type":"image/jpeg" + | }, + | "kml_overlay":{ + | "href":"http://aviris.jpl.nasa.gov/aviris_locator/y06_KML/f060426t01p00r03_sc01_overlay_KML.kml", + | "title":"kml_overlay", + | "description":"KML file describing the bounding box of the flight", + | "roles":[ + | + | ], + | "type":"application/vnd.google-earth.kml+xml" + | }, + | "rgb_small":{ + | "href":"http://aviris.jpl.nasa.gov/aviris_locator/y06_RGB/f060426t01p00r03_sc01_RGB-W200.jpg", + | "title":"rgb_small", + | "description":"A lower resolution thumbnail of the same image as the 'rgb' asset.", + | "roles":[ + | + | ], + | "type":"image/jpeg" + | }, + | "flight_log":{ + | "href":"http://aviris.jpl.nasa.gov/cgi/flights_06.cgi?step=view_flightlog&flight_id=f060426t01", + | "title":"flight_log", + | "description":"HTML page with table listing the runs for this flight.", + | "roles":[ + | + | ], + | "type":"text/html" + | }, + | "kml_outline":{ + | "href":"http://aviris.jpl.nasa.gov/aviris_locator/y06_KML/f060426t01p00r03_sc01_outline_KML.kml", + | "title":"kml_outline", + | "description":"KML file describing the flight outline", + | "roles":[ + | + | ], + | "type":"application/vnd.google-earth.kml+xml" + | } + | }, + | "collection":"aviris_2006_60426", + | "properties":{ + | "YY":6, + | "Run":3, + | "Tape":"t01", + | "Year":2006, + | "Scene":"sc01", + | "Flight":60426, + | "GEO Ver":"ort", + | "RDN Ver":"c", + | "Comments":"Alt = 21Kft
SOG = 103 kts
CLEAR !!", + | "NASA Log":"6T010", + | "Rotation":0, + | "datetime":"2006-04-26T17:52:00Z", + | "Flight ID":"f060426t01", + | "Site Name":"Red Mtn Pass 1, CO", + | "Pixel Size":2.1, + | "Flight Scene":"f060426t01p00r03_sc01", + | "Investigator":"Thomas Painter", + | "Solar Azimuth":139.9, + | "Number of Lines":6688, + | "Solar Elevation":60.21, + | "File Size (Bytes)":7475366912, + | "Number of Samples":1335, + | "Max Scene Elevation":4097.59, + | "Min Scene Elevation":3163.91, + | "Mean Scene Elevation":3680.71, + | "Gzip File Size (Bytes)":2673260903 + | } + | } + | ] + |} + |""".stripMargin).valueOr(throw _)), + StatusCode.Ok, + "" + ) + ) + } + + describe("StacClientSpec") { + it("SttpBackendStub collections") { + SttpStacClient(backend, uri"http://localhost:9090").collections + .map(_.map(_.id) shouldBe "aviris_2006" :: Nil) + } + + it("SttpBackendStub items") { + SttpStacClient(backend, uri"http://localhost:9090") + .items(NonEmptyString.unsafeFrom("aviris_2006")) + .map(_.map(_.id) shouldBe "aviris_f060426t01p00r03_sc01" :: Nil) + } + + ignore("AsyncHttpClientCatsBackend") { + + val res = AsyncHttpClientCatsBackend[IO]().flatMap { backend => + val client = SttpStacClient(backend, uri"http://localhost:9090") + client.collections + } + + res map (_ shouldNot be(Empty)) + } + + ignore("Http4sBackend") { + val res = Blocker[IO].flatMap(Http4sBackend.usingDefaultClientBuilder[IO](_)).use { backend => + val client = SttpStacClient(backend, uri"http://localhost:9090") + client.collections + } + + res map (_ shouldNot be(Empty)) + } + } +} diff --git a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/Http4sStacClient.scala b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/Http4sStacClient.scala deleted file mode 100644 index 3f6cfd6c..00000000 --- a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/Http4sStacClient.scala +++ /dev/null @@ -1,90 +0,0 @@ -package com.azavea.stac4s.api.client - -import com.azavea.stac4s.{StacCollection, StacItem} - -import cats.effect.{ConcurrentEffect, Resource, Sync} -import cats.syntax.apply._ -import cats.syntax.either._ -import cats.syntax.flatMap._ -import cats.syntax.functor._ -import eu.timepit.refined.types.string.NonEmptyString -import io.chrisdavenport.log4cats.Logger -import io.circe.syntax._ -import org.http4s.Method.{GET, POST} -import org.http4s.circe._ -import org.http4s.client.Client -import org.http4s.client.blaze.BlazeClientBuilder -import org.http4s.{Request, Uri} - -import scala.concurrent.ExecutionContext - -case class Http4sStacClient[F[_]: Sync: Logger]( - client: Client[F], - baseUri: Uri -) extends StacClient[F] { - private lazy val logger = Logger[F] - private def postRequest = Request[F]().withMethod(POST) - private def getRequest = Request[F]().withMethod(GET) - - def search(filter: SearchFilters = SearchFilters()): F[List[StacItem]] = - logger.trace(s"search: ${filter.asJson.spaces4}") *> - client - .expect(postRequest.withUri(baseUri.withPath("/search")).withEntity(filter.asJson.noSpaces)) - .map(_.hcursor.downField("features").as[List[StacItem]].bimap(_ => Nil, identity).merge) - - def collections: F[List[StacCollection]] = - logger.trace("collections") *> - client - .expect(getRequest.withUri(baseUri.withPath("/collections"))) - .map(_.hcursor.downField("collections").as[List[StacCollection]].bimap(_ => Nil, identity).merge) - - def collection(collectionId: NonEmptyString): F[Option[StacCollection]] = - logger.trace(s"collection: $collectionId") *> - client - .expect(getRequest.withUri(baseUri.withPath(s"/collections/$collectionId"))) - .map(_.as[Option[StacCollection]].bimap(_ => None, identity).merge) - - def items(collectionId: NonEmptyString): F[List[StacItem]] = - logger.trace(s"items: $collectionId") *> - client - .expect(getRequest.withUri(baseUri.withPath(s"/collections/$collectionId/items"))) - .map(_.hcursor.downField("features").as[List[StacItem]].bimap(_ => Nil, identity).merge) - - def item(collectionId: NonEmptyString, itemId: NonEmptyString): F[Option[StacItem]] = - logger.trace(s"items: $collectionId, $itemId") *> - client - .expect(getRequest.withUri(baseUri.withPath(s"/collections/$collectionId/items/$itemId"))) - .map(_.as[Option[StacItem]].bimap(_ => None, identity).merge) - - def itemCreate(collectionId: NonEmptyString, item: StacItem): F[StacItem] = - logger.trace(s"createItem: $collectionId, $item") *> - client - .expect( - postRequest - .withUri(baseUri.withPath(s"/collections/$collectionId/items")) - .withEntity(item.asJson.noSpaces) - ) - .flatMap { json => Sync[F].fromEither(json.as[StacItem].leftMap(_.getCause)) } - - def collectionCreate(collection: StacCollection): F[StacCollection] = - logger.trace(s"createCollection: $collection") *> - client - .expect( - postRequest - .withUri(baseUri.withPath(s"/collections/")) - .withEntity(collection.asJson.noSpaces) - ) - .flatMap { json => Sync[F].fromEither(json.as[StacCollection].leftMap(_.getCause)) } -} - -object Http4sStacClient { - - def apply[F[_]: ConcurrentEffect: Logger](baseUri: Uri)(implicit ec: ExecutionContext): F[Http4sStacClient[F]] = { - BlazeClientBuilder[F](ec).resource.use { client => ConcurrentEffect[F].delay(Http4sStacClient[F](client, baseUri)) } - } - - def resource[F[_]: ConcurrentEffect: Logger]( - baseUri: Uri - )(implicit ec: ExecutionContext): Resource[F, Http4sStacClient[F]] = - BlazeClientBuilder[F](ec).resource.map(Http4sStacClient[F](_, baseUri)) -} diff --git a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala index 8ddd6e1d..8231972b 100644 --- a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala +++ b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala @@ -20,6 +20,7 @@ import com.azavea.stac4s._ import eu.timepit.refined.types.string.NonEmptyString +/** TODO: instead of returning F[List] we can return fs2.Stream */ trait StacClient[F[_]] { def search(filter: SearchFilters = SearchFilters()): F[List[StacItem]] def collections: F[List[StacCollection]] diff --git a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala new file mode 100644 index 00000000..0170503c --- /dev/null +++ b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala @@ -0,0 +1,86 @@ +package com.azavea.stac4s.api.client + +import com.azavea.stac4s.{StacCollection, StacItem} + +import cats.MonadError +import cats.syntax.apply._ +import cats.syntax.flatMap._ +import cats.syntax.functor._ +import eu.timepit.refined.types.string.NonEmptyString +import io.chrisdavenport.log4cats.Logger +import io.circe.Json +import io.circe.syntax._ +import sttp.client3._ +import sttp.client3.circe.asJson +import sttp.model.Uri + +case class SttpStacClient[F[_]: MonadError[*[_], Throwable]: Logger]( + client: SttpBackend[F, Any], + baseUri: Uri +) extends StacClient[F] { + + def search(filter: SearchFilters = SearchFilters()): F[List[StacItem]] = + Logger[F].trace(s"search: ${filter.asJson.spaces4}") *> + client + .send(basicRequest.post(baseUri.withPath("search")).body(filter.asJson.noSpaces).response(asJson[Json])) + .map(_.body.flatMap(_.hcursor.downField("features").as[List[StacItem]])) + .flatMap(MonadError[F, Throwable].fromEither) + + def collections: F[List[StacCollection]] = + Logger[F].trace("collections") *> + client + .send(basicRequest.get(baseUri.withPath("collections")).response(asJson[Json])) + .map(_.body.flatMap(_.hcursor.downField("collections").as[List[StacCollection]])) + .flatMap(MonadError[F, Throwable].fromEither) + + def collection(collectionId: NonEmptyString): F[Option[StacCollection]] = + Logger[F].trace(s"collection: $collectionId") *> + client + .send( + basicRequest.get(baseUri.withPath("collections", collectionId.value)).response(asJson[Option[StacCollection]]) + ) + .map(_.body) + .flatMap(MonadError[F, Throwable].fromEither) + + def items(collectionId: NonEmptyString): F[List[StacItem]] = + Logger[F].trace(s"items: $collectionId") *> + client + .send(basicRequest.get(baseUri.withPath("collections", collectionId.value, "items")).response(asJson[Json])) + .map(_.body.flatMap(_.hcursor.downField("features").as[List[StacItem]])) + .flatMap(MonadError[F, Throwable].fromEither) + + def item(collectionId: NonEmptyString, itemId: NonEmptyString): F[Option[StacItem]] = + Logger[F].trace(s"items: $collectionId, $itemId") *> + client + .send( + basicRequest + .get(baseUri.withPath("collections", collectionId.value, "items", itemId.value)) + .response(asJson[Option[StacItem]]) + ) + .map(_.body) + .flatMap(MonadError[F, Throwable].fromEither) + + def itemCreate(collectionId: NonEmptyString, item: StacItem): F[StacItem] = + Logger[F].trace(s"createItem: ($collectionId, $item)") *> + client + .send( + basicRequest + .post(baseUri.withPath("collections", collectionId.value, "items")) + .body(item.asJson.noSpaces) + .response(asJson[StacItem]) + ) + .map(_.body) + .flatMap(MonadError[F, Throwable].fromEither) + + def collectionCreate(collection: StacCollection): F[StacCollection] = + Logger[F].trace(s"createCollection: $collection") *> + client + .send( + basicRequest + .post(baseUri.withPath("collections")) + .body(collection.asJson.noSpaces) + .response(asJson[StacCollection]) + ) + .map(_.body) + .flatMap(MonadError[F, Throwable].fromEither) +} diff --git a/modules/core/js/src/main/scala/com/azavea/stac4s/TestObject.scala b/modules/core/js/src/main/scala/com/azavea/stac4s/TestObject.scala deleted file mode 100644 index 8b137891..00000000 --- a/modules/core/js/src/main/scala/com/azavea/stac4s/TestObject.scala +++ /dev/null @@ -1 +0,0 @@ - diff --git a/modules/core/jvm/src/main/scala/com/azavea/stac4s/TestObject.scala b/modules/core/jvm/src/main/scala/com/azavea/stac4s/TestObject.scala deleted file mode 100644 index 8b137891..00000000 --- a/modules/core/jvm/src/main/scala/com/azavea/stac4s/TestObject.scala +++ /dev/null @@ -1 +0,0 @@ - diff --git a/project/Versions.scala b/project/Versions.scala index 41201513..642e44e9 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -1,15 +1,19 @@ object Versions { - val CatsVersion = "2.3.1" - val CirceVersion = "0.13.0" - val EnumeratumVersion = "1.6.1" - val GeoTrellisVersion = "3.5.1" + val Cats = "2.3.1" + val Circe = "0.13.0" + val Enumeratum = "1.6.1" + val GeoTrellis = "3.5.1" val Jts = "1.16.1" - val RefinedVersion = "0.9.19" - val ScalacheckCatsVersion = "0.3.0" - val ScalacheckVersion = "1.15.2" + val Refined = "0.9.19" + val ScalacheckCats = "0.3.0" + val Scalacheck = "1.15.2" val ScalatestPlusScalacheck = "3.2.2.0" - val ScalatestVersion = "3.2.3" - val ScapegoatVersion = "1.3.11" - val ShapelessVersion = "2.3.3" - val SpdxCheckerVersion = "1.0.0" + val Scalatest = "3.2.3" + val Scapegoat = "1.3.11" + val Shapeless = "2.3.3" + val SpdxChecker = "1.0.0" + val Sttp = "3.0.0-RC13" + val SttpModel = "1.2.0-RC9" + val SttpShared = "1.0.0-RC11" + val Log4Cats = "1.1.1" } From be0770310bfade484eef76d8925d9f4a0ec2f53f Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Wed, 23 Dec 2020 17:35:14 -0500 Subject: [PATCH 06/27] Add ClientJS --- build.sbt | 48 ++-- .../stac4s/api/client/SearchFilters.scala | 103 +++++++ .../azavea/stac4s/api/client/StacClient.scala | 0 .../stac4s/api/client/SttpStacClient.scala | 79 ++++++ .../client/js/src/test/resources/logback.xml | 11 + .../stac4s/api/client/StacClientSpec.scala | 259 ++++++++++++++++++ .../stac4s/api/client/SearchFilters.scala | 0 .../azavea/stac4s/api/client/StacClient.scala | 32 +++ .../stac4s/api/client/SttpStacClient.scala | 19 +- .../com/azavea/stac4s/api/client/Query.scala | 12 +- .../com/azavea/stac4s/geometry/Geometry.scala | 122 ++++----- 11 files changed, 576 insertions(+), 109 deletions(-) create mode 100644 modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala rename modules/client/{shared => js}/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala (100%) create mode 100644 modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala create mode 100644 modules/client/js/src/test/resources/logback.xml create mode 100644 modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala rename modules/client/{shared => jvm}/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala (100%) create mode 100644 modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala rename modules/client/{shared => jvm}/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala (84%) diff --git a/build.sbt b/build.sbt index c4457e92..7de5a6d6 100644 --- a/build.sbt +++ b/build.sbt @@ -145,14 +145,8 @@ lazy val core = crossProject(JSPlatform, JVMPlatform) "org.typelevel" %%% "cats-kernel" % Versions.Cats ) }) - .jvmSettings( - libraryDependencies ++= coreDependenciesJVM - ) - .jsSettings( - libraryDependencies ++= Seq( - "io.github.cquiroz" %% "scala-java-time" % "2.1.0" - ) - ) + .jvmSettings(libraryDependencies ++= coreDependenciesJVM) + .jsSettings(libraryDependencies +"io.github.cquiroz" %%% "scala-java-time" % "2.1.0") lazy val coreJVM = core.jvm lazy val coreJS = core.js @@ -209,26 +203,28 @@ lazy val client = crossProject(JSPlatform, JVMPlatform) .settings(publishSettings) .settings( libraryDependencies ++= Seq( - "io.circe" %% "circe-core" % Versions.Circe, - "io.circe" %% "circe-generic" % Versions.Circe, - "io.circe" %% "circe-refined" % Versions.Circe, - "com.chuusai" %% "shapeless" % Versions.Shapeless, - "eu.timepit" %% "refined" % Versions.Refined, - "org.locationtech.geotrellis" %% "geotrellis-vector" % Versions.GeoTrellis, - "org.locationtech.jts" % "jts-core" % Versions.Jts, - "org.typelevel" %% "cats-core" % Versions.Cats, - "com.softwaremill.sttp.client3" %% "core" % Versions.Sttp, - "com.softwaremill.sttp.client3" %% "circe" % Versions.Sttp, - "com.softwaremill.sttp.client3" %% "json-common" % Versions.Sttp, - "com.softwaremill.sttp.model" %% "core" % Versions.SttpModel, - "com.softwaremill.sttp.shared" %% "core" % Versions.SttpShared, - "com.softwaremill.sttp.client3" %% "http4s-backend" % Versions.Sttp % Test, - "com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % Versions.Sttp % Test, - "org.scalatest" %%% "scalatest" % Versions.Scalatest % Test, - "io.chrisdavenport" %% "log4cats-core" % Versions.Log4Cats, - "io.chrisdavenport" %% "log4cats-slf4j" % Versions.Log4Cats % Test + "io.circe" %%% "circe-core" % Versions.Circe, + "io.circe" %%% "circe-generic" % Versions.Circe, + "io.circe" %%% "circe-refined" % Versions.Circe, + "com.chuusai" %%% "shapeless" % Versions.Shapeless, + "eu.timepit" %%% "refined" % Versions.Refined, + "org.typelevel" %%% "cats-core" % Versions.Cats, + "com.softwaremill.sttp.client3" %%% "core" % Versions.Sttp, + "com.softwaremill.sttp.client3" %%% "circe" % Versions.Sttp, + "com.softwaremill.sttp.client3" %%% "json-common" % Versions.Sttp, + "com.softwaremill.sttp.model" %%% "core" % Versions.SttpModel, + "com.softwaremill.sttp.shared" %%% "core" % Versions.SttpShared, + "org.scalatest" %%% "scalatest" % Versions.Scalatest % Test, ) ) + .jsSettings(libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % "2.1.0") + .jvmSettings(libraryDependencies ++= coreDependenciesJVM) + .jvmSettings(libraryDependencies ++= Seq( + "io.chrisdavenport" %%% "log4cats-core" % Versions.Log4Cats, + "io.chrisdavenport" %%% "log4cats-slf4j" % Versions.Log4Cats % Test, + "com.softwaremill.sttp.client3" %% "http4s-backend" % Versions.Sttp % Test, + "com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % Versions.Sttp % Test + )) lazy val clientJVM = client.jvm lazy val clientJS = client.js diff --git a/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala new file mode 100644 index 00000000..9cfccb1e --- /dev/null +++ b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala @@ -0,0 +1,103 @@ +package com.azavea.stac4s.api.client + +import com.azavea.stac4s.Bbox +import com.azavea.stac4s.geometry.Geometry +import com.azavea.stac4s.types.TemporalExtent + +import cats.instances.either._ +import cats.syntax.apply._ +import cats.syntax.either._ +import eu.timepit.refined.types.numeric.NonNegInt +import io.circe._ +import io.circe.generic.semiauto._ +import io.circe.refined._ + +import java.time.Instant + +case class SearchFilters( + bbox: Option[Bbox] = None, + datetime: Option[TemporalExtent] = None, + intersects: Option[Geometry] = None, + collections: List[String] = Nil, + items: List[String] = Nil, + limit: Option[NonNegInt] = None, + query: Map[String, List[Query]] = Map.empty, + next: Option[PaginationToken] = None +) + +object SearchFilters { + + // TemporalExtent STAC API compatible serialization + // Ported from https://github.com/azavea/franklin/ + private def stringToInstant(s: String): Either[Throwable, Instant] = + Either.catchNonFatal(Instant.parse(s)) + + private def temporalExtentToString(te: TemporalExtent): String = + te.value match { + case Some(start) :: Some(end) :: _ if start != end => s"${start.toString}/${end.toString}" + case Some(start) :: Some(end) :: _ if start == end => s"${start.toString}" + case Some(start) :: None :: _ => s"${start.toString}/.." + case None :: Some(end) :: _ => s"../${end.toString}" + } + + private def temporalExtentFromString(str: String): Either[String, TemporalExtent] = { + str.split("/").toList match { + case ".." :: endString :: _ => + val parsedEnd = stringToInstant(endString) + parsedEnd match { + case Left(_) => s"Could not decode instant: $str".asLeft + case Right(end: Instant) => TemporalExtent(None, end).asRight + } + case startString :: ".." :: _ => + val parsedStart = stringToInstant(startString) + parsedStart match { + case Left(_) => s"Could not decode instant: $str".asLeft + case Right(start: Instant) => TemporalExtent(start, None).asRight + } + case startString :: endString :: _ => + val parsedStart = stringToInstant(startString) + val parsedEnd = stringToInstant(endString) + (parsedStart, parsedEnd).tupled match { + case Left(_) => s"Could not decode instant: $str".asLeft + case Right((start: Instant, end: Instant)) => TemporalExtent(start, end).asRight + } + case _ => + Either.catchNonFatal(Instant.parse(str)) match { + case Left(_) => s"Could not decode instant: $str".asLeft + case Right(t: Instant) => TemporalExtent(t, t).asRight + } + } + } + + implicit val encoderTemporalExtent: Encoder[TemporalExtent] = + Encoder.encodeString.contramap[TemporalExtent](temporalExtentToString) + + implicit val decoderTemporalExtent: Decoder[TemporalExtent] = + Decoder.decodeString.emap(temporalExtentFromString) + + implicit val searchFilterDecoder: Decoder[SearchFilters] = { c => + for { + bbox <- c.downField("bbox").as[Option[Bbox]] + datetime <- c.downField("datetime").as[Option[TemporalExtent]] + intersects <- c.downField("intersects").as[Option[Geometry]] + collectionsOption <- c.downField("collections").as[Option[List[String]]] + itemsOption <- c.downField("items").as[Option[List[String]]] + limit <- c.downField("limit").as[Option[NonNegInt]] + query <- c.get[Option[Map[String, List[Query]]]]("query") + paginationToken <- c.get[Option[PaginationToken]]("next") + } yield { + SearchFilters( + bbox, + datetime, + intersects, + collectionsOption.getOrElse(Nil), + itemsOption.getOrElse(Nil), + limit, + query getOrElse Map.empty, + paginationToken + ) + } + } + + implicit val searchFilterEncoder: Encoder[SearchFilters] = deriveEncoder +} diff --git a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala similarity index 100% rename from modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala rename to modules/client/js/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala diff --git a/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala new file mode 100644 index 00000000..ba91fa57 --- /dev/null +++ b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala @@ -0,0 +1,79 @@ +package com.azavea.stac4s.api.client + +import com.azavea.stac4s.{StacCollection, StacItem} + +import cats.MonadError +import cats.syntax.flatMap._ +import cats.syntax.functor._ +import eu.timepit.refined.types.string.NonEmptyString +import io.circe.Json +import io.circe.syntax._ +import sttp.client3._ +import sttp.client3.circe.asJson +import sttp.model.Uri + +case class SttpStacClient[F[_]: MonadError[*[_], Throwable]]( + client: SttpBackend[F, Any], + baseUri: Uri +) extends StacClient[F] { + + def search(filter: SearchFilters = SearchFilters()): F[List[StacItem]] = + client + .send(basicRequest.post(baseUri.withPath("search")).body(filter.asJson.noSpaces).response(asJson[Json])) + .map(_.body.flatMap(_.hcursor.downField("features").as[List[StacItem]])) + .flatMap(MonadError[F, Throwable].fromEither) + + def collections: F[List[StacCollection]] = + client + .send(basicRequest.get(baseUri.withPath("collections")).response(asJson[Json])) + .map(_.body.flatMap(_.hcursor.downField("collections").as[List[StacCollection]])) + .flatMap(MonadError[F, Throwable].fromEither) + + def collection(collectionId: NonEmptyString): F[Option[StacCollection]] = + client + .send( + basicRequest + .get(baseUri.withPath("collections", collectionId.value)) + .response(asJson[Option[StacCollection]]) + ) + .map(_.body) + .flatMap(MonadError[F, Throwable].fromEither) + + def items(collectionId: NonEmptyString): F[List[StacItem]] = + client + .send(basicRequest.get(baseUri.withPath("collections", collectionId.value, "items")).response(asJson[Json])) + .map(_.body.flatMap(_.hcursor.downField("features").as[List[StacItem]])) + .flatMap(MonadError[F, Throwable].fromEither) + + def item(collectionId: NonEmptyString, itemId: NonEmptyString): F[Option[StacItem]] = + client + .send( + basicRequest + .get(baseUri.withPath("collections", collectionId.value, "items", itemId.value)) + .response(asJson[Option[StacItem]]) + ) + .map(_.body) + .flatMap(MonadError[F, Throwable].fromEither) + + def itemCreate(collectionId: NonEmptyString, item: StacItem): F[StacItem] = + client + .send( + basicRequest + .post(baseUri.withPath("collections", collectionId.value, "items")) + .body(item.asJson.noSpaces) + .response(asJson[StacItem]) + ) + .map(_.body) + .flatMap(MonadError[F, Throwable].fromEither) + + def collectionCreate(collection: StacCollection): F[StacCollection] = + client + .send( + basicRequest + .post(baseUri.withPath("collections")) + .body(collection.asJson.noSpaces) + .response(asJson[StacCollection]) + ) + .map(_.body) + .flatMap(MonadError[F, Throwable].fromEither) +} diff --git a/modules/client/js/src/test/resources/logback.xml b/modules/client/js/src/test/resources/logback.xml new file mode 100644 index 00000000..0120ed6a --- /dev/null +++ b/modules/client/js/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + + + + + + + diff --git a/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala b/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala new file mode 100644 index 00000000..4c31e719 --- /dev/null +++ b/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala @@ -0,0 +1,259 @@ +package com.azavea.stac4s.api.client + +import cats.syntax.either._ +import eu.timepit.refined.types.all.NonEmptyString +import io.circe.parser._ +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import sttp.client3.testing.SttpBackendStub +import sttp.client3.{Response, UriContext} +import sttp.model.StatusCode +import sttp.monad.EitherMonad + +class StacClientSpec extends AnyFunSpec with Matchers { + + lazy val backend = + SttpBackendStub(EitherMonad) + .whenRequestMatches(_.uri.path == Seq("collections")) + .thenRespondF { _ => + Right( + Response( + Right(parse(""" + |{ + | "collections":[ + | { + | "stac_version":"1.0.0-beta.2", + | "stac_extensions":[ + | + | ], + | "id":"aviris_2006", + | "title":null, + | "description":"aviris_2006", + | "keywords":[ + | + | ], + | "license":"proprietary", + | "providers":[ + | + | ], + | "extent":{ + | "spatial":{ + | "bbox":[ + | [ + | -122.857491, + | 32.093266, + | -76.55229, + | 48.142484 + | ] + | ] + | }, + | "temporal":{ + | "interval":[ + | [ + | "2006-04-26T17:52:00Z", + | "2006-11-15T19:42:00Z" + | ] + | ] + | } + | }, + | "summaries":{ + | + | }, + | "properties":{ + | + | }, + | "links":[ + | { + | "href":"http://localhost:9090/collections/aviris_2006/items", + | "rel":"items", + | "type":"application/json", + | "title":null + | }, + | { + | "href":"http://localhost:9090/collections/aviris_2006", + | "rel":"self", + | "type":"application/json", + | "title":null + | } + | ] + | } + | ] + |} + |""".stripMargin).valueOr(throw _)), + StatusCode.Ok, + "" + ) + ) + } + .whenRequestMatches(_.uri.path == Seq("collections", "aviris_2006", "items")) + .thenRespondF { _ => + Right( + Response( + Right(parse(""" + |{ + | "type":"FeatureCollection", + | "features":[ + | { + | "id":"aviris_f060426t01p00r03_sc01", + | "stac_version":"1.0.0-beta.2", + | "stac_extensions":[ + | + | ], + | "type":"Feature", + | "geometry":{ + | "type":"Polygon", + | "coordinates":[ + | [ + | [ + | -107.771817, + | 37.913396 + | ], + | [ + | -107.739984, + | 37.914142 + | ], + | [ + | -107.744691, + | 38.040563 + | ], + | [ + | -107.776579, + | 38.039814 + | ], + | [ + | -107.771817, + | 37.913396 + | ] + | ] + | ] + | }, + | "bbox":[ + | -107.776579, + | 37.913396, + | -107.739984, + | 38.040563 + | ], + | "links":[ + | { + | "href":"http://localhost:9090/collections/aviris_2006_60426", + | "rel":"collection", + | "type":"application/json", + | "title":null + | }, + | { + | "href":"http://localhost:9090/collections/aviris_2006_60426/items/aviris_f060426t01p00r03_sc01", + | "rel":"self", + | "type":"application/json", + | "title":null + | } + | ], + | "assets":{ + | "ftp":{ + | "href":"ftp://avoil:Gulf0il$pill@popo.jpl.nasa.gov/y06_data/f060426t01p00r03.tar.gz", + | "title":"ftp", + | "description":"AVIRIS data archive. The file size is described by the 'Gzip File Size' property.", + | "roles":[ + | + | ], + | "type":"application/gzip" + | }, + | "rgb":{ + | "href":"http://aviris.jpl.nasa.gov/aviris_locator/y06_RGB/f060426t01p00r03_sc01_RGB.jpeg", + | "title":"rgb", + | "description":"Full resolution RGB image captured by the flight", + | "roles":[ + | + | ], + | "type":"image/jpeg" + | }, + | "kml_overlay":{ + | "href":"http://aviris.jpl.nasa.gov/aviris_locator/y06_KML/f060426t01p00r03_sc01_overlay_KML.kml", + | "title":"kml_overlay", + | "description":"KML file describing the bounding box of the flight", + | "roles":[ + | + | ], + | "type":"application/vnd.google-earth.kml+xml" + | }, + | "rgb_small":{ + | "href":"http://aviris.jpl.nasa.gov/aviris_locator/y06_RGB/f060426t01p00r03_sc01_RGB-W200.jpg", + | "title":"rgb_small", + | "description":"A lower resolution thumbnail of the same image as the 'rgb' asset.", + | "roles":[ + | + | ], + | "type":"image/jpeg" + | }, + | "flight_log":{ + | "href":"http://aviris.jpl.nasa.gov/cgi/flights_06.cgi?step=view_flightlog&flight_id=f060426t01", + | "title":"flight_log", + | "description":"HTML page with table listing the runs for this flight.", + | "roles":[ + | + | ], + | "type":"text/html" + | }, + | "kml_outline":{ + | "href":"http://aviris.jpl.nasa.gov/aviris_locator/y06_KML/f060426t01p00r03_sc01_outline_KML.kml", + | "title":"kml_outline", + | "description":"KML file describing the flight outline", + | "roles":[ + | + | ], + | "type":"application/vnd.google-earth.kml+xml" + | } + | }, + | "collection":"aviris_2006_60426", + | "properties":{ + | "YY":6, + | "Run":3, + | "Tape":"t01", + | "Year":2006, + | "Scene":"sc01", + | "Flight":60426, + | "GEO Ver":"ort", + | "RDN Ver":"c", + | "Comments":"Alt = 21Kft
SOG = 103 kts
CLEAR !!", + | "NASA Log":"6T010", + | "Rotation":0, + | "datetime":"2006-04-26T17:52:00Z", + | "Flight ID":"f060426t01", + | "Site Name":"Red Mtn Pass 1, CO", + | "Pixel Size":2.1, + | "Flight Scene":"f060426t01p00r03_sc01", + | "Investigator":"Thomas Painter", + | "Solar Azimuth":139.9, + | "Number of Lines":6688, + | "Solar Elevation":60.21, + | "File Size (Bytes)":7475366912, + | "Number of Samples":1335, + | "Max Scene Elevation":4097.59, + | "Min Scene Elevation":3163.91, + | "Mean Scene Elevation":3680.71, + | "Gzip File Size (Bytes)":2673260903 + | } + | } + | ] + |} + |""".stripMargin).valueOr(throw _)), + StatusCode.Ok, + "" + ) + ) + } + + describe("StacClientSpec") { + it("SttpBackendStub collections") { + SttpStacClient(backend, uri"http://localhost:9090").collections + .valueOr(throw _) + .map(_.id) shouldBe "aviris_2006" :: Nil + } + + it("SttpBackendStub items") { + SttpStacClient(backend, uri"http://localhost:9090") + .items(NonEmptyString.unsafeFrom("aviris_2006")) + .valueOr(throw _) + .map(_.id) shouldBe "aviris_f060426t01p00r03_sc01" :: Nil + } + } +} diff --git a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala similarity index 100% rename from modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala rename to modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala diff --git a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala new file mode 100644 index 00000000..8231972b --- /dev/null +++ b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala @@ -0,0 +1,32 @@ +/* + * Copyright 2020 Azavea + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.azavea.stac4s.api.client + +import com.azavea.stac4s._ + +import eu.timepit.refined.types.string.NonEmptyString + +/** TODO: instead of returning F[List] we can return fs2.Stream */ +trait StacClient[F[_]] { + def search(filter: SearchFilters = SearchFilters()): F[List[StacItem]] + def collections: F[List[StacCollection]] + def collection(collectionId: NonEmptyString): F[Option[StacCollection]] + def items(collectionId: NonEmptyString): F[List[StacItem]] + def item(collectionId: NonEmptyString, itemId: NonEmptyString): F[Option[StacItem]] + def itemCreate(collectionId: NonEmptyString, item: StacItem): F[StacItem] + def collectionCreate(collection: StacCollection): F[StacCollection] +} diff --git a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala similarity index 84% rename from modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala rename to modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala index 0170503c..dec7e1d6 100644 --- a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala +++ b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala @@ -3,7 +3,6 @@ package com.azavea.stac4s.api.client import com.azavea.stac4s.{StacCollection, StacItem} import cats.MonadError -import cats.syntax.apply._ import cats.syntax.flatMap._ import cats.syntax.functor._ import eu.timepit.refined.types.string.NonEmptyString @@ -20,37 +19,39 @@ case class SttpStacClient[F[_]: MonadError[*[_], Throwable]: Logger]( ) extends StacClient[F] { def search(filter: SearchFilters = SearchFilters()): F[List[StacItem]] = - Logger[F].trace(s"search: ${filter.asJson.spaces4}") *> + Logger[F].trace(s"search: ${filter.asJson.spaces4}") >> client .send(basicRequest.post(baseUri.withPath("search")).body(filter.asJson.noSpaces).response(asJson[Json])) .map(_.body.flatMap(_.hcursor.downField("features").as[List[StacItem]])) .flatMap(MonadError[F, Throwable].fromEither) def collections: F[List[StacCollection]] = - Logger[F].trace("collections") *> + Logger[F].trace("collections") >> client .send(basicRequest.get(baseUri.withPath("collections")).response(asJson[Json])) .map(_.body.flatMap(_.hcursor.downField("collections").as[List[StacCollection]])) .flatMap(MonadError[F, Throwable].fromEither) def collection(collectionId: NonEmptyString): F[Option[StacCollection]] = - Logger[F].trace(s"collection: $collectionId") *> + Logger[F].trace(s"collection: $collectionId") >> client .send( - basicRequest.get(baseUri.withPath("collections", collectionId.value)).response(asJson[Option[StacCollection]]) + basicRequest + .get(baseUri.withPath("collections", collectionId.value)) + .response(asJson[Option[StacCollection]]) ) .map(_.body) .flatMap(MonadError[F, Throwable].fromEither) def items(collectionId: NonEmptyString): F[List[StacItem]] = - Logger[F].trace(s"items: $collectionId") *> + Logger[F].trace(s"items: $collectionId") >> client .send(basicRequest.get(baseUri.withPath("collections", collectionId.value, "items")).response(asJson[Json])) .map(_.body.flatMap(_.hcursor.downField("features").as[List[StacItem]])) .flatMap(MonadError[F, Throwable].fromEither) def item(collectionId: NonEmptyString, itemId: NonEmptyString): F[Option[StacItem]] = - Logger[F].trace(s"items: $collectionId, $itemId") *> + Logger[F].trace(s"items: $collectionId, $itemId") >> client .send( basicRequest @@ -61,7 +62,7 @@ case class SttpStacClient[F[_]: MonadError[*[_], Throwable]: Logger]( .flatMap(MonadError[F, Throwable].fromEither) def itemCreate(collectionId: NonEmptyString, item: StacItem): F[StacItem] = - Logger[F].trace(s"createItem: ($collectionId, $item)") *> + Logger[F].trace(s"createItem: ($collectionId, $item)") >> client .send( basicRequest @@ -73,7 +74,7 @@ case class SttpStacClient[F[_]: MonadError[*[_], Throwable]: Logger]( .flatMap(MonadError[F, Throwable].fromEither) def collectionCreate(collection: StacCollection): F[StacCollection] = - Logger[F].trace(s"createCollection: $collection") *> + Logger[F].trace(s"createCollection: $collection") >> client .send( basicRequest diff --git a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/Query.scala b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/Query.scala index 58ce4ea6..2f4ce51a 100644 --- a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/Query.scala +++ b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/Query.scala @@ -36,9 +36,9 @@ object Query { s"Cannot construct `$operator` query with $json" def queriesFromMap(unparsed: Map[String, Json]): Either[String, List[Query]] = - (unparsed.toList traverse { - case (op @ "eq", json) => Right(Equals(json)) - case (op @ "neq", json) => Right(NotEqualTo(json)) + unparsed.toList traverse { + case (_ @ "eq", json) => Right(Equals(json)) + case (_ @ "neq", json) => Right(NotEqualTo(json)) case (op @ "lt", json) => Either.fromOption(fromStringOrNum(json, LessThan.apply), errMessage(op, json)) case (op @ "lte", json) => @@ -82,7 +82,7 @@ object Query { errMessage(op, json) ) case (k, _) => Left(s"$k is not a valid operator") - }) + } implicit val encQuery: Encoder[List[Query]] = { queries => Map( @@ -102,5 +102,7 @@ object Query { ).asJson } - implicit val decQueries: Decoder[List[Query]] = Decoder[JsonObject].emap { jsonObj => queriesFromMap(jsonObj.toMap) } + implicit val decQueries: Decoder[List[Query]] = Decoder.decodeJsonObject.emap { jsonObj => + queriesFromMap(jsonObj.toMap) + } } diff --git a/modules/core/js/src/main/scala/com/azavea/stac4s/geometry/Geometry.scala b/modules/core/js/src/main/scala/com/azavea/stac4s/geometry/Geometry.scala index 4417648d..e5db4584 100644 --- a/modules/core/js/src/main/scala/com/azavea/stac4s/geometry/Geometry.scala +++ b/modules/core/js/src/main/scala/com/azavea/stac4s/geometry/Geometry.scala @@ -16,11 +16,11 @@ object Geometry { } case class Polygon private (coords: List[Point2d]) extends Geometry { - def asCoordinateArray: List[List[Double]] = coords.map(_.asCoordinateArray) + def asCoordinateArray: List[List[List[Double]]] = List(coords.map(_.asCoordinateArray)) } case class MultiPolygon private (polys: List[Polygon]) extends Geometry { - def asCoordinateArray: List[List[List[Double]]] = polys.map(_.asCoordinateArray) + def asCoordinateArray: List[List[List[List[Double]]]] = polys.map(_.asCoordinateArray) } private def point2dFromArray(arr: List[Double]): Either[String, Point2d] = arr match { @@ -28,8 +28,8 @@ object Geometry { case _ => Left("Point can only be constructed from exactly two coordinates") } - private def polygonFromArray(arr: List[List[Double]]): Either[String, Polygon] = - arr traverse { point2dFromArray } flatMap { points => + private def polygonFromArray(arr: List[List[List[Double]]]): Either[String, Polygon] = + arr.flatten traverse { point2dFromArray } flatMap { points => (points.headOption, points.lastOption) match { case (Some(h), Some(t)) if h === t && points.size >= 4 => Either.right[String, Polygon](Polygon(points)) @@ -40,7 +40,7 @@ object Geometry { } } - private def multiPolygonFromArray(arr: List[List[List[Double]]]): Either[String, MultiPolygon] = + private def multiPolygonFromArray(arr: List[List[List[List[Double]]]]): Either[String, MultiPolygon] = arr traverse { polygonFromArray } map { MultiPolygon } implicit val eqPoint2d: Eq[Point2d] = Eq.fromUniversalEquals @@ -49,83 +49,67 @@ object Geometry { implicit val eqGeometry: Eq[Geometry] = Eq.fromUniversalEquals - implicit val encPoint2d: Encoder[Point2d] = new Encoder[Point2d] { - - def apply(a: Point2d): Json = - Map( - "type" -> "Point".asJson, - "coordinates" -> List(a.x, a.y).asJson - ).asJson + implicit val encPoint2d: Encoder[Point2d] = { point2d => + Map( + "type" -> "Point".asJson, + "coordinates" -> List(point2d.x, point2d.y).asJson + ).asJson } - implicit val encPolygon: Encoder[Polygon] = new Encoder[Polygon] { - - // for now, I'm ignoring polygons with holes - def apply(a: Polygon): Json = - Map( - "type" -> "Polygon".asJson, - "coordinates" -> a.asCoordinateArray.asJson - ).asJson + // for now, I'm ignoring polygons with holes + // however polygons still store coordinates as List[List[List[Double]]] + implicit val encPolygon: Encoder[Polygon] = { polygon => + Map( + "type" -> "Polygon".asJson, + "coordinates" -> polygon.asCoordinateArray.asJson + ).asJson } - implicit val encMultiPolygon: Encoder[MultiPolygon] = new Encoder[MultiPolygon] { - - def apply(a: MultiPolygon): Json = - Map( - "type" -> "MultiPolygon".asJson, - "coordinates" -> a.asCoordinateArray.asJson - ).asJson + // for now, I'm ignoring multi polygons with holes + // however polygons still store coordinates as List[List[List[List[Double]]]] + implicit val encMultiPolygon: Encoder[MultiPolygon] = { mpolygon => + Map( + "type" -> "MultiPolygon".asJson, + "coordinates" -> mpolygon.asCoordinateArray.asJson + ).asJson } - implicit val decPoint2d: Decoder[Point2d] = new Decoder[Point2d] { - - def apply(c: HCursor): Decoder.Result[Point2d] = - c.get[List[Double]]("coordinates") match { - case Right(arr) => point2dFromArray(arr).leftMap(DecodingFailure(_, c.history)) - case Left(err) => Left(err) // re-wrap to get the correct RHS - } - } - - implicit val decPolygon: Decoder[Polygon] = new Decoder[Polygon] { - - def apply(c: HCursor): Decoder.Result[Polygon] = - c.get[List[List[Double]]]("coordinates") match { - case Right(arr) => - polygonFromArray(arr).leftMap(DecodingFailure(_, c.history)) - case Left(err) => Left(err) // re-wrap to get the correct RHS - } - + implicit val decPoint2d: Decoder[Point2d] = { c => + c.get[List[Double]]("coordinates") match { + case Right(arr) => point2dFromArray(arr).leftMap(DecodingFailure(_, c.history)) + case Left(err) => Left(err) // re-wrap to get the correct RHS + } } - implicit val decMultiPolygon: Decoder[MultiPolygon] = new Decoder[MultiPolygon] { - - def apply(c: HCursor): Decoder.Result[MultiPolygon] = - c.get[List[List[List[Double]]]]("coordinates") flatMap { arr => - multiPolygonFromArray(arr).leftMap(DecodingFailure(_, c.history)) - } + implicit val decPolygon: Decoder[Polygon] = { c => + c.get[List[List[List[Double]]]]("coordinates") match { + case Right(arr) => + polygonFromArray(arr).leftMap(DecodingFailure(_, c.history)) + case Left(err) => Left(err) // re-wrap to get the correct RHS + } } - implicit val encGeometry: Encoder[Geometry] = new Encoder[Geometry] { - - def apply(g: Geometry): Json = g match { - case mp @ MultiPolygon(_) => mp.asJson - case p @ Polygon(_) => p.asJson - case p2d @ Point2d(_, _) => p2d.asJson + implicit val decMultiPolygon: Decoder[MultiPolygon] = { c => + c.get[List[List[List[List[Double]]]]]("coordinates") flatMap { arr => + multiPolygonFromArray(arr).leftMap(DecodingFailure(_, c.history)) } } - implicit val decGeometry: Decoder[Geometry] = new Decoder[Geometry] { - - def apply(c: HCursor): Decoder.Result[Geometry] = - for { - geomType <- c.get[String]("type") - result <- geomType.toLowerCase match { - case "polygon" => Decoder[Polygon].decodeJson(c.value) - case "point" => Decoder[Point2d].decodeJson(c.value) - case "multipolygon" => Decoder[MultiPolygon].decodeJson(c.value) - case _ => Left(DecodingFailure(s"Unrecognized geometry: $geomType", c.history)) - } - } yield result + implicit val encGeometry: Encoder[Geometry] = { + case mp @ MultiPolygon(_) => mp.asJson + case p @ Polygon(_) => p.asJson + case p2d @ Point2d(_, _) => p2d.asJson } + implicit val decGeometry: Decoder[Geometry] = { c => + for { + geomType <- c.get[String]("type") + result <- geomType.toLowerCase match { + case "polygon" => Decoder[Polygon].decodeJson(c.value) + case "point" => Decoder[Point2d].decodeJson(c.value) + case "multipolygon" => Decoder[MultiPolygon].decodeJson(c.value) + case _ => Left(DecodingFailure(s"Unrecognized geometry: $geomType", c.history)) + } + } yield result + } } From 739912d450d342139ddc3c5f902f032fd179be04 Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Wed, 23 Dec 2020 17:45:28 -0500 Subject: [PATCH 07/27] Formatting --- .gitignore | 1 - build.sbt | 40 ++++++++++--------- .../stac4s/api/client/StacClientSpec.scala | 2 +- scripts/test | 2 + 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index ba1a60e8..25cd10f0 100644 --- a/.gitignore +++ b/.gitignore @@ -77,7 +77,6 @@ project/plugins/project/ /project/.sbtboot /project/.boot/ /project/.ivy/ -/.sbtopts # Molecule .molecule/ diff --git a/build.sbt b/build.sbt index 7de5a6d6..6063a185 100644 --- a/build.sbt +++ b/build.sbt @@ -146,7 +146,7 @@ lazy val core = crossProject(JSPlatform, JVMPlatform) ) }) .jvmSettings(libraryDependencies ++= coreDependenciesJVM) - .jsSettings(libraryDependencies +"io.github.cquiroz" %%% "scala-java-time" % "2.1.0") + .jsSettings(libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % "2.1.0") lazy val coreJVM = core.jvm lazy val coreJS = core.js @@ -203,28 +203,30 @@ lazy val client = crossProject(JSPlatform, JVMPlatform) .settings(publishSettings) .settings( libraryDependencies ++= Seq( - "io.circe" %%% "circe-core" % Versions.Circe, - "io.circe" %%% "circe-generic" % Versions.Circe, - "io.circe" %%% "circe-refined" % Versions.Circe, - "com.chuusai" %%% "shapeless" % Versions.Shapeless, - "eu.timepit" %%% "refined" % Versions.Refined, - "org.typelevel" %%% "cats-core" % Versions.Cats, - "com.softwaremill.sttp.client3" %%% "core" % Versions.Sttp, - "com.softwaremill.sttp.client3" %%% "circe" % Versions.Sttp, - "com.softwaremill.sttp.client3" %%% "json-common" % Versions.Sttp, - "com.softwaremill.sttp.model" %%% "core" % Versions.SttpModel, - "com.softwaremill.sttp.shared" %%% "core" % Versions.SttpShared, - "org.scalatest" %%% "scalatest" % Versions.Scalatest % Test, + "io.circe" %%% "circe-core" % Versions.Circe, + "io.circe" %%% "circe-generic" % Versions.Circe, + "io.circe" %%% "circe-refined" % Versions.Circe, + "com.chuusai" %%% "shapeless" % Versions.Shapeless, + "eu.timepit" %%% "refined" % Versions.Refined, + "org.typelevel" %%% "cats-core" % Versions.Cats, + "com.softwaremill.sttp.client3" %%% "core" % Versions.Sttp, + "com.softwaremill.sttp.client3" %%% "circe" % Versions.Sttp, + "com.softwaremill.sttp.client3" %%% "json-common" % Versions.Sttp, + "com.softwaremill.sttp.model" %%% "core" % Versions.SttpModel, + "com.softwaremill.sttp.shared" %%% "core" % Versions.SttpShared, + "org.scalatest" %%% "scalatest" % Versions.Scalatest % Test ) ) .jsSettings(libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % "2.1.0") .jvmSettings(libraryDependencies ++= coreDependenciesJVM) - .jvmSettings(libraryDependencies ++= Seq( - "io.chrisdavenport" %%% "log4cats-core" % Versions.Log4Cats, - "io.chrisdavenport" %%% "log4cats-slf4j" % Versions.Log4Cats % Test, - "com.softwaremill.sttp.client3" %% "http4s-backend" % Versions.Sttp % Test, - "com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % Versions.Sttp % Test - )) + .jvmSettings( + libraryDependencies ++= Seq( + "io.chrisdavenport" %%% "log4cats-core" % Versions.Log4Cats, + "io.chrisdavenport" %%% "log4cats-slf4j" % Versions.Log4Cats % Test, + "com.softwaremill.sttp.client3" %% "http4s-backend" % Versions.Sttp % Test, + "com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % Versions.Sttp % Test + ) + ) lazy val clientJVM = client.jvm lazy val clientJS = client.js diff --git a/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala b/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala index 4c31e719..999e3f22 100644 --- a/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala +++ b/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala @@ -12,7 +12,7 @@ import sttp.monad.EitherMonad class StacClientSpec extends AnyFunSpec with Matchers { - lazy val backend = + lazy val backend: SttpBackendStub[Either[Throwable, *], Nothing] = SttpBackendStub(EitherMonad) .whenRequestMatches(_.uri.path == Seq("collections")) .thenRespondF { _ => diff --git a/scripts/test b/scripts/test index e5ceb06f..6b0aeabc 100755 --- a/scripts/test +++ b/scripts/test @@ -26,9 +26,11 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then coreJVM/undeclaredCompileDependenciesTest; \ testingJVM/undeclaredCompileDependenciesTest; \ coreTestJVM/undeclaredCompileDependenciesTest; \ + clientJVM/undeclaredCompileDependenciesTest; \ coreJVM/unusedCompileDependenciesTest; \ testingJVM/unusedCompileDependenciesTest; \ coreTestJVM/unusedCompileDependenciesTest; \ + clientJVM/unusedCompileDependenciesTest; \ test " fi From 7a275a24ff7307a99534cd526464e29142d5ec24 Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Wed, 23 Dec 2020 19:25:21 -0500 Subject: [PATCH 08/27] Generate STAC Client specs --- CHANGELOG.md | 3 + build.sbt | 7 +- .../stac4s/api/client/StacClientSpec.scala | 275 +++-------------- .../stac4s/api/client/StacClientSpec.scala | 280 +++--------------- .../js/src/main/scala/JsInstances.scala | 78 ++++- .../jvm/src/main/scala/JvmInstances.scala | 50 ++++ 6 files changed, 226 insertions(+), 467 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8eca499..0208fde9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Added +- Add a client module [#140](https://github.com/azavea/stac4s/pull/140) + ### Fixed - Repaired build.sbt configuration to get sonatype publication to cooperate [#186](https://github.com/azavea/stac4s/pull/186) diff --git a/build.sbt b/build.sbt index 6063a185..c14e7f16 100644 --- a/build.sbt +++ b/build.sbt @@ -170,6 +170,11 @@ lazy val testing = crossProject(JSPlatform, JVMPlatform) ) ) .jvmSettings(libraryDependencies ++= testingDependenciesJVM) + .jsSettings( + libraryDependencies ++= Seq( + "io.github.cquiroz" %%% "scala-java-time" % "2.1.0" % Test + ) + ) lazy val testingJVM = testing.jvm lazy val testingJS = testing.js @@ -198,7 +203,7 @@ lazy val coreTestRef = LocalProject("modules/core-test") lazy val client = crossProject(JSPlatform, JVMPlatform) .in(file("modules/client")) - .dependsOn(core) + .dependsOn(core, testing % Test) .settings(commonSettings) .settings(publishSettings) .settings( diff --git a/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala b/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala index 999e3f22..42375852 100644 --- a/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala +++ b/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala @@ -1,259 +1,72 @@ package com.azavea.stac4s.api.client +import com.azavea.stac4s.testing.JsInstances + import cats.syntax.either._ import eu.timepit.refined.types.all.NonEmptyString -import io.circe.parser._ +import io.circe.JsonObject +import io.circe.syntax._ import org.scalatest.funspec.AnyFunSpec import org.scalatest.matchers.should.Matchers import sttp.client3.testing.SttpBackendStub import sttp.client3.{Response, UriContext} -import sttp.model.StatusCode import sttp.monad.EitherMonad -class StacClientSpec extends AnyFunSpec with Matchers { +class StacClientSpec extends AnyFunSpec with Matchers with JsInstances { - lazy val backend: SttpBackendStub[Either[Throwable, *], Nothing] = + lazy val backend = SttpBackendStub(EitherMonad) + .whenRequestMatches(_.uri.path == Seq("search")) + .thenRespondF { _ => + Response + .ok(arbItemCollectionShort.arbitrary.sample.asJson.asRight) + .asRight + } .whenRequestMatches(_.uri.path == Seq("collections")) .thenRespondF { _ => - Right( - Response( - Right(parse(""" - |{ - | "collections":[ - | { - | "stac_version":"1.0.0-beta.2", - | "stac_extensions":[ - | - | ], - | "id":"aviris_2006", - | "title":null, - | "description":"aviris_2006", - | "keywords":[ - | - | ], - | "license":"proprietary", - | "providers":[ - | - | ], - | "extent":{ - | "spatial":{ - | "bbox":[ - | [ - | -122.857491, - | 32.093266, - | -76.55229, - | 48.142484 - | ] - | ] - | }, - | "temporal":{ - | "interval":[ - | [ - | "2006-04-26T17:52:00Z", - | "2006-11-15T19:42:00Z" - | ] - | ] - | } - | }, - | "summaries":{ - | - | }, - | "properties":{ - | - | }, - | "links":[ - | { - | "href":"http://localhost:9090/collections/aviris_2006/items", - | "rel":"items", - | "type":"application/json", - | "title":null - | }, - | { - | "href":"http://localhost:9090/collections/aviris_2006", - | "rel":"self", - | "type":"application/json", - | "title":null - | } - | ] - | } - | ] - |} - |""".stripMargin).valueOr(throw _)), - StatusCode.Ok, - "" - ) - ) + Response + .ok(JsonObject("collections" -> arbCollectionShort.arbitrary.sample.toList.asJson).asJson.asRight) + .asRight } - .whenRequestMatches(_.uri.path == Seq("collections", "aviris_2006", "items")) + .whenRequestMatches(_.uri.path == Seq("collections", "collection_id", "items")) .thenRespondF { _ => - Right( - Response( - Right(parse(""" - |{ - | "type":"FeatureCollection", - | "features":[ - | { - | "id":"aviris_f060426t01p00r03_sc01", - | "stac_version":"1.0.0-beta.2", - | "stac_extensions":[ - | - | ], - | "type":"Feature", - | "geometry":{ - | "type":"Polygon", - | "coordinates":[ - | [ - | [ - | -107.771817, - | 37.913396 - | ], - | [ - | -107.739984, - | 37.914142 - | ], - | [ - | -107.744691, - | 38.040563 - | ], - | [ - | -107.776579, - | 38.039814 - | ], - | [ - | -107.771817, - | 37.913396 - | ] - | ] - | ] - | }, - | "bbox":[ - | -107.776579, - | 37.913396, - | -107.739984, - | 38.040563 - | ], - | "links":[ - | { - | "href":"http://localhost:9090/collections/aviris_2006_60426", - | "rel":"collection", - | "type":"application/json", - | "title":null - | }, - | { - | "href":"http://localhost:9090/collections/aviris_2006_60426/items/aviris_f060426t01p00r03_sc01", - | "rel":"self", - | "type":"application/json", - | "title":null - | } - | ], - | "assets":{ - | "ftp":{ - | "href":"ftp://avoil:Gulf0il$pill@popo.jpl.nasa.gov/y06_data/f060426t01p00r03.tar.gz", - | "title":"ftp", - | "description":"AVIRIS data archive. The file size is described by the 'Gzip File Size' property.", - | "roles":[ - | - | ], - | "type":"application/gzip" - | }, - | "rgb":{ - | "href":"http://aviris.jpl.nasa.gov/aviris_locator/y06_RGB/f060426t01p00r03_sc01_RGB.jpeg", - | "title":"rgb", - | "description":"Full resolution RGB image captured by the flight", - | "roles":[ - | - | ], - | "type":"image/jpeg" - | }, - | "kml_overlay":{ - | "href":"http://aviris.jpl.nasa.gov/aviris_locator/y06_KML/f060426t01p00r03_sc01_overlay_KML.kml", - | "title":"kml_overlay", - | "description":"KML file describing the bounding box of the flight", - | "roles":[ - | - | ], - | "type":"application/vnd.google-earth.kml+xml" - | }, - | "rgb_small":{ - | "href":"http://aviris.jpl.nasa.gov/aviris_locator/y06_RGB/f060426t01p00r03_sc01_RGB-W200.jpg", - | "title":"rgb_small", - | "description":"A lower resolution thumbnail of the same image as the 'rgb' asset.", - | "roles":[ - | - | ], - | "type":"image/jpeg" - | }, - | "flight_log":{ - | "href":"http://aviris.jpl.nasa.gov/cgi/flights_06.cgi?step=view_flightlog&flight_id=f060426t01", - | "title":"flight_log", - | "description":"HTML page with table listing the runs for this flight.", - | "roles":[ - | - | ], - | "type":"text/html" - | }, - | "kml_outline":{ - | "href":"http://aviris.jpl.nasa.gov/aviris_locator/y06_KML/f060426t01p00r03_sc01_outline_KML.kml", - | "title":"kml_outline", - | "description":"KML file describing the flight outline", - | "roles":[ - | - | ], - | "type":"application/vnd.google-earth.kml+xml" - | } - | }, - | "collection":"aviris_2006_60426", - | "properties":{ - | "YY":6, - | "Run":3, - | "Tape":"t01", - | "Year":2006, - | "Scene":"sc01", - | "Flight":60426, - | "GEO Ver":"ort", - | "RDN Ver":"c", - | "Comments":"Alt = 21Kft
SOG = 103 kts
CLEAR !!", - | "NASA Log":"6T010", - | "Rotation":0, - | "datetime":"2006-04-26T17:52:00Z", - | "Flight ID":"f060426t01", - | "Site Name":"Red Mtn Pass 1, CO", - | "Pixel Size":2.1, - | "Flight Scene":"f060426t01p00r03_sc01", - | "Investigator":"Thomas Painter", - | "Solar Azimuth":139.9, - | "Number of Lines":6688, - | "Solar Elevation":60.21, - | "File Size (Bytes)":7475366912, - | "Number of Samples":1335, - | "Max Scene Elevation":4097.59, - | "Min Scene Elevation":3163.91, - | "Mean Scene Elevation":3680.71, - | "Gzip File Size (Bytes)":2673260903 - | } - | } - | ] - |} - |""".stripMargin).valueOr(throw _)), - StatusCode.Ok, - "" - ) - ) + Response + .ok(arbItemCollectionShort.arbitrary.sample.asJson.asRight) + .asRight + } + .whenRequestMatches(_.uri.path == Seq("collections", "collection_id", "items", "item_id")) + .thenRespondF { _ => + Response + .ok(arbItemShort.arbitrary.sample.asRight) + .asRight } describe("StacClientSpec") { - it("SttpBackendStub collections") { + it("search") { + SttpStacClient(backend, uri"http://localhost:9090") + .search() + .valueOr(throw _) + .size should be > 0 + } + + it("collections") { SttpStacClient(backend, uri"http://localhost:9090").collections .valueOr(throw _) - .map(_.id) shouldBe "aviris_2006" :: Nil + .size should be > 0 + } + + it("items") { + SttpStacClient(backend, uri"http://localhost:9090") + .items(NonEmptyString.unsafeFrom("collection_id")) + .valueOr(throw _) + .size should be > 0 } - it("SttpBackendStub items") { + it("item") { SttpStacClient(backend, uri"http://localhost:9090") - .items(NonEmptyString.unsafeFrom("aviris_2006")) + .item(NonEmptyString.unsafeFrom("collection_id"), NonEmptyString.unsafeFrom("item_id")) .valueOr(throw _) - .map(_.id) shouldBe "aviris_f060426t01p00r03_sc01" :: Nil + .size should be > 0 } } } diff --git a/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala b/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala index 8d84c2fd..5bf9eb82 100644 --- a/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala +++ b/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala @@ -1,267 +1,80 @@ package com.azavea.stac4s.api.client +import com.azavea.stac4s.testing.JvmInstances + import cats.effect.{Blocker, IO} +import cats.syntax.applicative._ import cats.syntax.either._ import com.azavea.IOSpec import eu.timepit.refined.collection.Empty import eu.timepit.refined.types.all.NonEmptyString -import io.circe.parser._ +import io.circe.JsonObject +import io.circe.syntax._ import sttp.client3.asynchttpclient.cats.AsyncHttpClientCatsBackend import sttp.client3.http4s.Http4sBackend import sttp.client3.impl.cats.CatsMonadAsyncError import sttp.client3.testing.SttpBackendStub import sttp.client3.{Response, UriContext} -import sttp.model.StatusCode -class StacClientSpec extends IOSpec { +class StacClientSpec extends IOSpec with JvmInstances { lazy val backend: SttpBackendStub[IO, Nothing] = SttpBackendStub(new CatsMonadAsyncError[IO]()) + .whenRequestMatches(_.uri.path == Seq("search")) + .thenRespondF { _ => + Response + .ok(arbItemCollectionShort.arbitrary.sample.asJson.asRight) + .pure[IO] + } .whenRequestMatches(_.uri.path == Seq("collections")) .thenRespondF { _ => - IO( - Response( - Right(parse(""" - |{ - | "collections":[ - | { - | "stac_version":"1.0.0-beta.2", - | "stac_extensions":[ - | - | ], - | "id":"aviris_2006", - | "title":null, - | "description":"aviris_2006", - | "keywords":[ - | - | ], - | "license":"proprietary", - | "providers":[ - | - | ], - | "extent":{ - | "spatial":{ - | "bbox":[ - | [ - | -122.857491, - | 32.093266, - | -76.55229, - | 48.142484 - | ] - | ] - | }, - | "temporal":{ - | "interval":[ - | [ - | "2006-04-26T17:52:00Z", - | "2006-11-15T19:42:00Z" - | ] - | ] - | } - | }, - | "summaries":{ - | - | }, - | "properties":{ - | - | }, - | "links":[ - | { - | "href":"http://localhost:9090/collections/aviris_2006/items", - | "rel":"items", - | "type":"application/json", - | "title":null - | }, - | { - | "href":"http://localhost:9090/collections/aviris_2006", - | "rel":"self", - | "type":"application/json", - | "title":null - | } - | ] - | } - | ] - |} - |""".stripMargin).valueOr(throw _)), - StatusCode.Ok, - "" - ) - ) + Response + .ok(JsonObject("collections" -> arbCollectionShort.arbitrary.sample.toList.asJson).asJson.asRight) + .pure[IO] } - .whenRequestMatches(_.uri.path == Seq("collections", "aviris_2006", "items")) + .whenRequestMatches(_.uri.path == Seq("collections", "collection_id", "items")) .thenRespondF { _ => - IO( - Response( - Right(parse(""" - |{ - | "type":"FeatureCollection", - | "features":[ - | { - | "id":"aviris_f060426t01p00r03_sc01", - | "stac_version":"1.0.0-beta.2", - | "stac_extensions":[ - | - | ], - | "type":"Feature", - | "geometry":{ - | "type":"Polygon", - | "coordinates":[ - | [ - | [ - | -107.771817, - | 37.913396 - | ], - | [ - | -107.739984, - | 37.914142 - | ], - | [ - | -107.744691, - | 38.040563 - | ], - | [ - | -107.776579, - | 38.039814 - | ], - | [ - | -107.771817, - | 37.913396 - | ] - | ] - | ] - | }, - | "bbox":[ - | -107.776579, - | 37.913396, - | -107.739984, - | 38.040563 - | ], - | "links":[ - | { - | "href":"http://localhost:9090/collections/aviris_2006_60426", - | "rel":"collection", - | "type":"application/json", - | "title":null - | }, - | { - | "href":"http://localhost:9090/collections/aviris_2006_60426/items/aviris_f060426t01p00r03_sc01", - | "rel":"self", - | "type":"application/json", - | "title":null - | } - | ], - | "assets":{ - | "ftp":{ - | "href":"ftp://avoil:Gulf0il$pill@popo.jpl.nasa.gov/y06_data/f060426t01p00r03.tar.gz", - | "title":"ftp", - | "description":"AVIRIS data archive. The file size is described by the 'Gzip File Size' property.", - | "roles":[ - | - | ], - | "type":"application/gzip" - | }, - | "rgb":{ - | "href":"http://aviris.jpl.nasa.gov/aviris_locator/y06_RGB/f060426t01p00r03_sc01_RGB.jpeg", - | "title":"rgb", - | "description":"Full resolution RGB image captured by the flight", - | "roles":[ - | - | ], - | "type":"image/jpeg" - | }, - | "kml_overlay":{ - | "href":"http://aviris.jpl.nasa.gov/aviris_locator/y06_KML/f060426t01p00r03_sc01_overlay_KML.kml", - | "title":"kml_overlay", - | "description":"KML file describing the bounding box of the flight", - | "roles":[ - | - | ], - | "type":"application/vnd.google-earth.kml+xml" - | }, - | "rgb_small":{ - | "href":"http://aviris.jpl.nasa.gov/aviris_locator/y06_RGB/f060426t01p00r03_sc01_RGB-W200.jpg", - | "title":"rgb_small", - | "description":"A lower resolution thumbnail of the same image as the 'rgb' asset.", - | "roles":[ - | - | ], - | "type":"image/jpeg" - | }, - | "flight_log":{ - | "href":"http://aviris.jpl.nasa.gov/cgi/flights_06.cgi?step=view_flightlog&flight_id=f060426t01", - | "title":"flight_log", - | "description":"HTML page with table listing the runs for this flight.", - | "roles":[ - | - | ], - | "type":"text/html" - | }, - | "kml_outline":{ - | "href":"http://aviris.jpl.nasa.gov/aviris_locator/y06_KML/f060426t01p00r03_sc01_outline_KML.kml", - | "title":"kml_outline", - | "description":"KML file describing the flight outline", - | "roles":[ - | - | ], - | "type":"application/vnd.google-earth.kml+xml" - | } - | }, - | "collection":"aviris_2006_60426", - | "properties":{ - | "YY":6, - | "Run":3, - | "Tape":"t01", - | "Year":2006, - | "Scene":"sc01", - | "Flight":60426, - | "GEO Ver":"ort", - | "RDN Ver":"c", - | "Comments":"Alt = 21Kft
SOG = 103 kts
CLEAR !!", - | "NASA Log":"6T010", - | "Rotation":0, - | "datetime":"2006-04-26T17:52:00Z", - | "Flight ID":"f060426t01", - | "Site Name":"Red Mtn Pass 1, CO", - | "Pixel Size":2.1, - | "Flight Scene":"f060426t01p00r03_sc01", - | "Investigator":"Thomas Painter", - | "Solar Azimuth":139.9, - | "Number of Lines":6688, - | "Solar Elevation":60.21, - | "File Size (Bytes)":7475366912, - | "Number of Samples":1335, - | "Max Scene Elevation":4097.59, - | "Min Scene Elevation":3163.91, - | "Mean Scene Elevation":3680.71, - | "Gzip File Size (Bytes)":2673260903 - | } - | } - | ] - |} - |""".stripMargin).valueOr(throw _)), - StatusCode.Ok, - "" - ) - ) + Response + .ok(arbItemCollectionShort.arbitrary.sample.asJson.asRight) + .pure[IO] + } + .whenRequestMatches(_.uri.path == Seq("collections", "collection_id", "items", "item_id")) + .thenRespondF { _ => + Response + .ok(arbItemShort.arbitrary.sample.asRight) + .pure[IO] } describe("StacClientSpec") { - it("SttpBackendStub collections") { + it("search") { + SttpStacClient(backend, uri"http://localhost:9090") + .search() + .map(_.size should be > 0) + } + + it("collections") { SttpStacClient(backend, uri"http://localhost:9090").collections - .map(_.map(_.id) shouldBe "aviris_2006" :: Nil) + .map(_.size should be > 0) + } + + it("items") { + SttpStacClient(backend, uri"http://localhost:9090") + .items(NonEmptyString.unsafeFrom("collection_id")) + .map(_.size should be > 0) } - it("SttpBackendStub items") { + it("item") { SttpStacClient(backend, uri"http://localhost:9090") - .items(NonEmptyString.unsafeFrom("aviris_2006")) - .map(_.map(_.id) shouldBe "aviris_f060426t01p00r03_sc01" :: Nil) + .item(NonEmptyString.unsafeFrom("collection_id"), NonEmptyString.unsafeFrom("item_id")) + .map(_.size should be > 0) } + } + describe("STAC Client Examples") { ignore("AsyncHttpClientCatsBackend") { val res = AsyncHttpClientCatsBackend[IO]().flatMap { backend => - val client = SttpStacClient(backend, uri"http://localhost:9090") - client.collections + SttpStacClient(backend, uri"http://localhost:9090").collections } res map (_ shouldNot be(Empty)) @@ -269,8 +82,7 @@ class StacClientSpec extends IOSpec { ignore("Http4sBackend") { val res = Blocker[IO].flatMap(Http4sBackend.usingDefaultClientBuilder[IO](_)).use { backend => - val client = SttpStacClient(backend, uri"http://localhost:9090") - client.collections + SttpStacClient(backend, uri"http://localhost:9090").collections } res map (_ shouldNot be(Empty)) diff --git a/modules/testing/js/src/main/scala/JsInstances.scala b/modules/testing/js/src/main/scala/JsInstances.scala index 228217d5..077a6e95 100644 --- a/modules/testing/js/src/main/scala/JsInstances.scala +++ b/modules/testing/js/src/main/scala/JsInstances.scala @@ -2,19 +2,46 @@ package com.azavea.stac4s.testing import com.azavea.stac4s.geometry.Geometry.{MultiPolygon, Point2d, Polygon} import com.azavea.stac4s.geometry._ -import com.azavea.stac4s.{ItemCollection, StacItem, StacLink, StacVersion} +import com.azavea.stac4s.types.TemporalExtent +import com.azavea.stac4s.{ + Bbox, + Interval, + ItemCollection, + Proprietary, + SpatialExtent, + StacCollection, + StacExtent, + StacItem, + StacItemAsset, + StacLink, + StacVersion +} import cats.syntax.apply._ +import cats.syntax.option._ +import io.circe.JsonObject import io.circe.syntax._ import org.scalacheck.cats.implicits._ import org.scalacheck.{Arbitrary, Gen} +import java.time.Instant + trait JsInstances { private[testing] def finiteDoubleGen: Gen[Double] = Arbitrary.arbitrary[Double].filterNot(_.isNaN) private[testing] def point2dGen: Gen[Point2d] = (finiteDoubleGen, finiteDoubleGen).mapN(Point2d.apply) + private[testing] def temporalExtentGen: Gen[TemporalExtent] = + (Gen.const(Instant.now), Gen.const(Instant.now)).tupled + .map { case (start, end) => TemporalExtent(start, end) } + + private[testing] def stacExtentGen: Gen[StacExtent] = + ( + TestInstances.bboxGen, + temporalExtentGen + ).mapN((bbox: Bbox, interval: TemporalExtent) => StacExtent(SpatialExtent(List(bbox)), Interval(List(interval)))) + /** We know for sure that we have five points, so there's no risk in calling .head */ @SuppressWarnings(Array("TraversableHead")) private[testing] def polygonGen: Gen[Polygon] = Gen.listOfN(5, point2dGen).map(points => Polygon(points :+ points.head)) @@ -40,6 +67,20 @@ trait JsInstances { TestInstances.itemExtensionFieldsGen ).mapN(StacItem.apply) + private[testing] def stacItemShortGen: Gen[StacItem] = + ( + nonEmptyStringGen, + Gen.const("0.8.0"), + Gen.const(List.empty[String]), + Gen.const("Feature"), + geometryGen, + TestInstances.twoDimBboxGen, + Gen.const(Nil), + Gen.const(Map.empty[String, StacItemAsset]), + Gen.option(nonEmptyStringGen), + TestInstances.itemExtensionFieldsGen + ).mapN(StacItem.apply) + private[testing] def itemCollectionGen: Gen[ItemCollection] = ( Gen.const("FeatureCollection"), @@ -50,13 +91,48 @@ trait JsInstances { Gen.const(().asJsonObject) ).mapN(ItemCollection.apply) + private[testing] def itemCollectionShortGen: Gen[ItemCollection] = + ( + Gen.const("FeatureCollection"), + Gen.const(StacVersion.unsafeFrom("0.9.0")), + Gen.const(Nil), + Gen.listOf[StacItem](stacItemGen), + Gen.const(Nil), + Gen.const(().asJsonObject) + ).mapN(ItemCollection.apply) + + private[testing] def stacCollectionShortGen: Gen[StacCollection] = + ( + Gen.const("0.9.0"), + Gen.const(Nil), + nonEmptyStringGen, + nonEmptyStringGen.map(_.some), + nonEmptyStringGen, + Gen.const(Nil), + Gen.const(Proprietary()), + Gen.const(Nil), + stacExtentGen, + Gen.const(JsonObject.empty), + Gen.const(JsonObject.empty), + Gen.const(Nil), + Gen.const(().asJsonObject) + ).mapN(StacCollection.apply) + implicit val arbItem: Arbitrary[StacItem] = Arbitrary { stacItemGen } + val arbItemShort: Arbitrary[StacItem] = Arbitrary { stacItemShortGen } + implicit val arbItemCollection: Arbitrary[ItemCollection] = Arbitrary { itemCollectionGen } + val arbItemCollectionShort: Arbitrary[ItemCollection] = Arbitrary { + itemCollectionShortGen + } + implicit val arbGeometry: Arbitrary[Geometry] = Arbitrary { geometryGen } + + val arbCollectionShort: Arbitrary[StacCollection] = Arbitrary { stacCollectionShortGen } } object JsInstances extends JsInstances {} diff --git a/modules/testing/jvm/src/main/scala/JvmInstances.scala b/modules/testing/jvm/src/main/scala/JvmInstances.scala index b781b661..72cbf045 100644 --- a/modules/testing/jvm/src/main/scala/JvmInstances.scala +++ b/modules/testing/jvm/src/main/scala/JvmInstances.scala @@ -9,6 +9,7 @@ import com.azavea.stac4s.{ StacCollection, StacExtent, StacItem, + StacItemAsset, StacLink, StacVersion } @@ -61,6 +62,20 @@ trait JvmInstances { TestInstances.itemExtensionFieldsGen ).mapN(StacItem.apply) + private[testing] def stacItemShortGen: Gen[StacItem] = + ( + nonEmptyStringGen, + Gen.const("0.8.0"), + Gen.const(List.empty[String]), + Gen.const("Feature"), + rectangleGen, + TestInstances.twoDimBboxGen, + Gen.const(Nil), + Gen.const(Map.empty[String, StacItemAsset]), + Gen.option(nonEmptyStringGen), + TestInstances.itemExtensionFieldsGen + ).mapN(StacItem.apply) + private[testing] def itemCollectionGen: Gen[ItemCollection] = ( Gen.const("FeatureCollection"), @@ -71,6 +86,16 @@ trait JvmInstances { Gen.const(().asJsonObject) ).mapN(ItemCollection.apply) + private[testing] def itemCollectionShortGen: Gen[ItemCollection] = + ( + Gen.const("FeatureCollection"), + Gen.const(StacVersion.unsafeFrom("0.9.0")), + Gen.const(Nil), + Gen.listOf[StacItem](stacItemGen), + Gen.const(Nil), + Gen.const(().asJsonObject) + ).mapN(ItemCollection.apply) + private[testing] def stacExtentGen: Gen[StacExtent] = ( TestInstances.bboxGen, @@ -94,12 +119,35 @@ trait JvmInstances { TestInstances.collectionExtensionFieldsGen ).mapN(StacCollection.apply) + private[testing] def stacCollectionShortGen: Gen[StacCollection] = + ( + nonEmptyStringGen, + possiblyEmptyListGen(nonEmptyStringGen), + nonEmptyStringGen, + Gen.option(nonEmptyStringGen), + nonEmptyStringGen, + Gen.const(Nil), + TestInstances.stacLicenseGen, + Gen.const(Nil), + stacExtentGen, + Gen.const(().asJsonObject), + Gen.const(JsonObject.fromMap(Map.empty)), + Gen.const(Nil), + Gen.const(().asJsonObject) + ).mapN(StacCollection.apply) + implicit val arbItem: Arbitrary[StacItem] = Arbitrary { stacItemGen } + val arbItemShort: Arbitrary[StacItem] = Arbitrary { stacItemShortGen } + implicit val arbItemCollection: Arbitrary[ItemCollection] = Arbitrary { itemCollectionGen } + val arbItemCollectionShort: Arbitrary[ItemCollection] = Arbitrary { + itemCollectionShortGen + } + implicit val arbGeometry: Arbitrary[Geometry] = Arbitrary { rectangleGen } implicit val arbInstant: Arbitrary[Instant] = Arbitrary { instantGen } @@ -108,6 +156,8 @@ trait JvmInstances { stacCollectionGen } + val arbCollectionShort: Arbitrary[StacCollection] = Arbitrary { stacCollectionShortGen } + implicit val arbStacExtent: Arbitrary[StacExtent] = Arbitrary { stacExtentGen } From ed8fda84fc7c88843e518c9991360a8c8bf3201c Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Wed, 23 Dec 2020 19:39:08 -0500 Subject: [PATCH 09/27] Code cleanup --- .../stac4s/api/client/SearchFilters.scala | 56 +---------------- .../stac4s/api/client/SearchFilters.scala | 56 +---------------- .../api/client/utils/ClientCodecs.scala | 61 +++++++++++++++++++ 3 files changed, 65 insertions(+), 108 deletions(-) create mode 100644 modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/utils/ClientCodecs.scala diff --git a/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala index 9cfccb1e..57849d7d 100644 --- a/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala +++ b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala @@ -1,19 +1,15 @@ package com.azavea.stac4s.api.client import com.azavea.stac4s.Bbox +import com.azavea.stac4s.api.client.utils.ClientCodecs import com.azavea.stac4s.geometry.Geometry import com.azavea.stac4s.types.TemporalExtent -import cats.instances.either._ -import cats.syntax.apply._ -import cats.syntax.either._ import eu.timepit.refined.types.numeric.NonNegInt import io.circe._ import io.circe.generic.semiauto._ import io.circe.refined._ -import java.time.Instant - case class SearchFilters( bbox: Option[Bbox] = None, datetime: Option[TemporalExtent] = None, @@ -25,55 +21,7 @@ case class SearchFilters( next: Option[PaginationToken] = None ) -object SearchFilters { - - // TemporalExtent STAC API compatible serialization - // Ported from https://github.com/azavea/franklin/ - private def stringToInstant(s: String): Either[Throwable, Instant] = - Either.catchNonFatal(Instant.parse(s)) - - private def temporalExtentToString(te: TemporalExtent): String = - te.value match { - case Some(start) :: Some(end) :: _ if start != end => s"${start.toString}/${end.toString}" - case Some(start) :: Some(end) :: _ if start == end => s"${start.toString}" - case Some(start) :: None :: _ => s"${start.toString}/.." - case None :: Some(end) :: _ => s"../${end.toString}" - } - - private def temporalExtentFromString(str: String): Either[String, TemporalExtent] = { - str.split("/").toList match { - case ".." :: endString :: _ => - val parsedEnd = stringToInstant(endString) - parsedEnd match { - case Left(_) => s"Could not decode instant: $str".asLeft - case Right(end: Instant) => TemporalExtent(None, end).asRight - } - case startString :: ".." :: _ => - val parsedStart = stringToInstant(startString) - parsedStart match { - case Left(_) => s"Could not decode instant: $str".asLeft - case Right(start: Instant) => TemporalExtent(start, None).asRight - } - case startString :: endString :: _ => - val parsedStart = stringToInstant(startString) - val parsedEnd = stringToInstant(endString) - (parsedStart, parsedEnd).tupled match { - case Left(_) => s"Could not decode instant: $str".asLeft - case Right((start: Instant, end: Instant)) => TemporalExtent(start, end).asRight - } - case _ => - Either.catchNonFatal(Instant.parse(str)) match { - case Left(_) => s"Could not decode instant: $str".asLeft - case Right(t: Instant) => TemporalExtent(t, t).asRight - } - } - } - - implicit val encoderTemporalExtent: Encoder[TemporalExtent] = - Encoder.encodeString.contramap[TemporalExtent](temporalExtentToString) - - implicit val decoderTemporalExtent: Decoder[TemporalExtent] = - Decoder.decodeString.emap(temporalExtentFromString) +object SearchFilters extends ClientCodecs { implicit val searchFilterDecoder: Decoder[SearchFilters] = { c => for { diff --git a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala index 0fca422c..f8d10342 100644 --- a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala +++ b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala @@ -1,19 +1,15 @@ package com.azavea.stac4s.api.client import com.azavea.stac4s.Bbox +import com.azavea.stac4s.api.client.utils.ClientCodecs import com.azavea.stac4s.types.TemporalExtent -import cats.instances.either._ -import cats.syntax.apply._ -import cats.syntax.either._ import eu.timepit.refined.types.numeric.NonNegInt import geotrellis.vector.{io => _, _} import io.circe._ import io.circe.generic.semiauto._ import io.circe.refined._ -import java.time.Instant - case class SearchFilters( bbox: Option[Bbox] = None, datetime: Option[TemporalExtent] = None, @@ -25,55 +21,7 @@ case class SearchFilters( next: Option[PaginationToken] = None ) -object SearchFilters { - - // TemporalExtent STAC API compatible serialization - // Ported from https://github.com/azavea/franklin/ - private def stringToInstant(s: String): Either[Throwable, Instant] = - Either.catchNonFatal(Instant.parse(s)) - - private def temporalExtentToString(te: TemporalExtent): String = - te.value match { - case Some(start) :: Some(end) :: _ if start != end => s"${start.toString}/${end.toString}" - case Some(start) :: Some(end) :: _ if start == end => s"${start.toString}" - case Some(start) :: None :: _ => s"${start.toString}/.." - case None :: Some(end) :: _ => s"../${end.toString}" - } - - private def temporalExtentFromString(str: String): Either[String, TemporalExtent] = { - str.split("/").toList match { - case ".." :: endString :: _ => - val parsedEnd = stringToInstant(endString) - parsedEnd match { - case Left(_) => s"Could not decode instant: $str".asLeft - case Right(end: Instant) => TemporalExtent(None, end).asRight - } - case startString :: ".." :: _ => - val parsedStart = stringToInstant(startString) - parsedStart match { - case Left(_) => s"Could not decode instant: $str".asLeft - case Right(start: Instant) => TemporalExtent(start, None).asRight - } - case startString :: endString :: _ => - val parsedStart = stringToInstant(startString) - val parsedEnd = stringToInstant(endString) - (parsedStart, parsedEnd).tupled match { - case Left(_) => s"Could not decode instant: $str".asLeft - case Right((start: Instant, end: Instant)) => TemporalExtent(start, end).asRight - } - case _ => - Either.catchNonFatal(Instant.parse(str)) match { - case Left(_) => s"Could not decode instant: $str".asLeft - case Right(t: Instant) => TemporalExtent(t, t).asRight - } - } - } - - implicit val encoderTemporalExtent: Encoder[TemporalExtent] = - Encoder.encodeString.contramap[TemporalExtent](temporalExtentToString) - - implicit val decoderTemporalExtent: Decoder[TemporalExtent] = - Decoder.decodeString.emap(temporalExtentFromString) +object SearchFilters extends ClientCodecs { implicit val searchFilterDecoder: Decoder[SearchFilters] = { c => for { diff --git a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/utils/ClientCodecs.scala b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/utils/ClientCodecs.scala new file mode 100644 index 00000000..81ebd48a --- /dev/null +++ b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/utils/ClientCodecs.scala @@ -0,0 +1,61 @@ +package com.azavea.stac4s.api.client.utils + +import com.azavea.stac4s.types.TemporalExtent + +import cats.syntax.apply._ +import cats.syntax.either._ +import io.circe.{Decoder, Encoder} + +import java.time.Instant + +trait ClientCodecs { + + // TemporalExtent STAC API compatible serialization + // Ported from https://github.com/azavea/franklin/ + private def stringToInstant(s: String): Either[Throwable, Instant] = + Either.catchNonFatal(Instant.parse(s)) + + private def temporalExtentToString(te: TemporalExtent): String = + te.value match { + case Some(start) :: Some(end) :: _ if start != end => s"${start.toString}/${end.toString}" + case Some(start) :: Some(end) :: _ if start == end => s"${start.toString}" + case Some(start) :: None :: _ => s"${start.toString}/.." + case None :: Some(end) :: _ => s"../${end.toString}" + } + + private def temporalExtentFromString(str: String): Either[String, TemporalExtent] = { + str.split("/").toList match { + case ".." :: endString :: _ => + val parsedEnd = stringToInstant(endString) + parsedEnd match { + case Left(_) => s"Could not decode instant: $str".asLeft + case Right(end: Instant) => TemporalExtent(None, end).asRight + } + case startString :: ".." :: _ => + val parsedStart = stringToInstant(startString) + parsedStart match { + case Left(_) => s"Could not decode instant: $str".asLeft + case Right(start: Instant) => TemporalExtent(start, None).asRight + } + case startString :: endString :: _ => + val parsedStart = stringToInstant(startString) + val parsedEnd = stringToInstant(endString) + (parsedStart, parsedEnd).tupled match { + case Left(_) => s"Could not decode instant: $str".asLeft + case Right((start: Instant, end: Instant)) => TemporalExtent(start, end).asRight + } + case _ => + Either.catchNonFatal(Instant.parse(str)) match { + case Left(_) => s"Could not decode instant: $str".asLeft + case Right(t: Instant) => TemporalExtent(t, t).asRight + } + } + } + + implicit lazy val encoderTemporalExtent: Encoder[TemporalExtent] = + Encoder.encodeString.contramap[TemporalExtent](temporalExtentToString) + + implicit lazy val decoderTemporalExtent: Decoder[TemporalExtent] = + Decoder.decodeString.emap(temporalExtentFromString) + +} From 46d9c577020cf0b14b70e39be3b043dccc161127 Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Wed, 23 Dec 2020 19:45:29 -0500 Subject: [PATCH 10/27] Configure sbt --- .circleci/config.yml | 2 +- .sbtopts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .sbtopts diff --git a/.circleci/config.yml b/.circleci/config.yml index 04c71a13..5aa01c3d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -35,7 +35,7 @@ aliases: - image: circleci/openjdk:8-jdk-node environment: # https://circleci.com/docs/2.0/java-oom/ - _JAVA_OPTIONS: "-Xms128m -Xmx2g" + _JAVA_OPTIONS: "-Xms64m -Xmx2g" version: 2 workflows: diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 00000000..d7afdda3 --- /dev/null +++ b/.sbtopts @@ -0,0 +1,5 @@ +-J-Xmx2g +-J-Xms64m +-J-Xss2M +-J-XX:+UseConcMarkSweepGC +-J-XX:+CMSClassUnloadingEnabled From 0469396259ec01cb5d56cf53f4e9749f43a33d82 Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Wed, 23 Dec 2020 19:46:57 -0500 Subject: [PATCH 11/27] Close sttp client in tests --- .circleci/config.yml | 2 +- .sbtopts | 5 ----- .../com/azavea/stac4s/api/client/StacClientSpec.scala | 6 ++++-- .../com/azavea/stac4s/api/client/StacClientSpec.scala | 8 +++++--- 4 files changed, 10 insertions(+), 11 deletions(-) delete mode 100644 .sbtopts diff --git a/.circleci/config.yml b/.circleci/config.yml index 5aa01c3d..04c71a13 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -35,7 +35,7 @@ aliases: - image: circleci/openjdk:8-jdk-node environment: # https://circleci.com/docs/2.0/java-oom/ - _JAVA_OPTIONS: "-Xms64m -Xmx2g" + _JAVA_OPTIONS: "-Xms128m -Xmx2g" version: 2 workflows: diff --git a/.sbtopts b/.sbtopts deleted file mode 100644 index d7afdda3..00000000 --- a/.sbtopts +++ /dev/null @@ -1,5 +0,0 @@ --J-Xmx2g --J-Xms64m --J-Xss2M --J-XX:+UseConcMarkSweepGC --J-XX:+CMSClassUnloadingEnabled diff --git a/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala b/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala index 42375852..3c375dd3 100644 --- a/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala +++ b/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala @@ -1,18 +1,18 @@ package com.azavea.stac4s.api.client import com.azavea.stac4s.testing.JsInstances - import cats.syntax.either._ import eu.timepit.refined.types.all.NonEmptyString import io.circe.JsonObject import io.circe.syntax._ +import org.scalatest.BeforeAndAfterAll import org.scalatest.funspec.AnyFunSpec import org.scalatest.matchers.should.Matchers import sttp.client3.testing.SttpBackendStub import sttp.client3.{Response, UriContext} import sttp.monad.EitherMonad -class StacClientSpec extends AnyFunSpec with Matchers with JsInstances { +class StacClientSpec extends AnyFunSpec with Matchers with JsInstances with BeforeAndAfterAll { lazy val backend = SttpBackendStub(EitherMonad) @@ -69,4 +69,6 @@ class StacClientSpec extends AnyFunSpec with Matchers with JsInstances { .size should be > 0 } } + + override def afterAll(): Unit = backend.close().valueOr(throw _) } diff --git a/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala b/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala index 5bf9eb82..1fa1fdf5 100644 --- a/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala +++ b/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala @@ -1,7 +1,6 @@ package com.azavea.stac4s.api.client import com.azavea.stac4s.testing.JvmInstances - import cats.effect.{Blocker, IO} import cats.syntax.applicative._ import cats.syntax.either._ @@ -10,15 +9,16 @@ import eu.timepit.refined.collection.Empty import eu.timepit.refined.types.all.NonEmptyString import io.circe.JsonObject import io.circe.syntax._ +import org.scalatest.BeforeAndAfterAll import sttp.client3.asynchttpclient.cats.AsyncHttpClientCatsBackend import sttp.client3.http4s.Http4sBackend import sttp.client3.impl.cats.CatsMonadAsyncError import sttp.client3.testing.SttpBackendStub import sttp.client3.{Response, UriContext} -class StacClientSpec extends IOSpec with JvmInstances { +class StacClientSpec extends IOSpec with JvmInstances with BeforeAndAfterAll { - lazy val backend: SttpBackendStub[IO, Nothing] = + lazy val backend = SttpBackendStub(new CatsMonadAsyncError[IO]()) .whenRequestMatches(_.uri.path == Seq("search")) .thenRespondF { _ => @@ -88,4 +88,6 @@ class StacClientSpec extends IOSpec with JvmInstances { res map (_ shouldNot be(Empty)) } } + + override def afterAll(): Unit = backend.close().unsafeRunSync() } From 8821a2fe0547c1c7d28fbf6e07a451f647dc67ae Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Thu, 24 Dec 2020 09:18:52 -0500 Subject: [PATCH 12/27] Tune CircleCI --- .circleci/config.yml | 2 +- .sbtopts | 3 +++ .../scala/com/azavea/stac4s/api/client/StacClientSpec.scala | 1 + .../scala/com/azavea/stac4s/api/client/StacClientSpec.scala | 1 + 4 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .sbtopts diff --git a/.circleci/config.yml b/.circleci/config.yml index 04c71a13..ff350af5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -35,7 +35,7 @@ aliases: - image: circleci/openjdk:8-jdk-node environment: # https://circleci.com/docs/2.0/java-oom/ - _JAVA_OPTIONS: "-Xms128m -Xmx2g" + _JAVA_OPTIONS: "-Xms128m -Xmx1536m" version: 2 workflows: diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 00000000..94cdba09 --- /dev/null +++ b/.sbtopts @@ -0,0 +1,3 @@ +-J-Xmx1536m +-J-Xms64m +-J-Xss2M diff --git a/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala b/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala index 3c375dd3..7ef4b910 100644 --- a/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala +++ b/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala @@ -1,6 +1,7 @@ package com.azavea.stac4s.api.client import com.azavea.stac4s.testing.JsInstances + import cats.syntax.either._ import eu.timepit.refined.types.all.NonEmptyString import io.circe.JsonObject diff --git a/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala b/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala index 1fa1fdf5..caeb48ca 100644 --- a/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala +++ b/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala @@ -1,6 +1,7 @@ package com.azavea.stac4s.api.client import com.azavea.stac4s.testing.JvmInstances + import cats.effect.{Blocker, IO} import cats.syntax.applicative._ import cats.syntax.either._ From f06064415b2707ff179f4c688c10b10ad404684b Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Thu, 24 Dec 2020 09:21:49 -0500 Subject: [PATCH 13/27] Tune SBT --- .sbtopts | 3 --- modules/client/js/src/test/resources/logback.xml | 11 ----------- modules/client/jvm/src/test/resources/logback.xml | 11 ----------- 3 files changed, 25 deletions(-) delete mode 100644 .sbtopts delete mode 100644 modules/client/js/src/test/resources/logback.xml delete mode 100644 modules/client/jvm/src/test/resources/logback.xml diff --git a/.sbtopts b/.sbtopts deleted file mode 100644 index 94cdba09..00000000 --- a/.sbtopts +++ /dev/null @@ -1,3 +0,0 @@ --J-Xmx1536m --J-Xms64m --J-Xss2M diff --git a/modules/client/js/src/test/resources/logback.xml b/modules/client/js/src/test/resources/logback.xml deleted file mode 100644 index 0120ed6a..00000000 --- a/modules/client/js/src/test/resources/logback.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n - - - - - - - diff --git a/modules/client/jvm/src/test/resources/logback.xml b/modules/client/jvm/src/test/resources/logback.xml deleted file mode 100644 index 0120ed6a..00000000 --- a/modules/client/jvm/src/test/resources/logback.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n - - - - - - - From c44cffcaa7396bc2dfd2604b7d59222912f1853b Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Thu, 24 Dec 2020 10:02:02 -0500 Subject: [PATCH 14/27] Move StacClient into a shared dir --- CHANGELOG.md | 2 +- .../stac4s/api/client/SttpStacClient.scala | 2 ++ .../azavea/stac4s/api/client/StacClient.scala | 32 ------------------- .../stac4s/api/client/SttpStacClient.scala | 8 +++-- .../azavea/stac4s/api/client/StacClient.scala | 3 +- 5 files changed, 10 insertions(+), 37 deletions(-) delete mode 100644 modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala rename modules/client/{js => shared}/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala (94%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0208fde9..b33759ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Added -- Add a client module [#140](https://github.com/azavea/stac4s/pull/140) +- Сlient module [#140](https://github.com/azavea/stac4s/pull/140) ### Fixed - Repaired build.sbt configuration to get sonatype publication to cooperate [#186](https://github.com/azavea/stac4s/pull/186) diff --git a/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala index ba91fa57..d08110ba 100644 --- a/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala +++ b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala @@ -17,6 +17,8 @@ case class SttpStacClient[F[_]: MonadError[*[_], Throwable]]( baseUri: Uri ) extends StacClient[F] { + type Filter = SearchFilters + def search(filter: SearchFilters = SearchFilters()): F[List[StacItem]] = client .send(basicRequest.post(baseUri.withPath("search")).body(filter.asJson.noSpaces).response(asJson[Json])) diff --git a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala deleted file mode 100644 index 8231972b..00000000 --- a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2020 Azavea - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.azavea.stac4s.api.client - -import com.azavea.stac4s._ - -import eu.timepit.refined.types.string.NonEmptyString - -/** TODO: instead of returning F[List] we can return fs2.Stream */ -trait StacClient[F[_]] { - def search(filter: SearchFilters = SearchFilters()): F[List[StacItem]] - def collections: F[List[StacCollection]] - def collection(collectionId: NonEmptyString): F[Option[StacCollection]] - def items(collectionId: NonEmptyString): F[List[StacItem]] - def item(collectionId: NonEmptyString, itemId: NonEmptyString): F[Option[StacItem]] - def itemCreate(collectionId: NonEmptyString, item: StacItem): F[StacItem] - def collectionCreate(collection: StacCollection): F[StacCollection] -} diff --git a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala index dec7e1d6..224b00c8 100644 --- a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala +++ b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala @@ -18,8 +18,10 @@ case class SttpStacClient[F[_]: MonadError[*[_], Throwable]: Logger]( baseUri: Uri ) extends StacClient[F] { + type Filter = SearchFilters + def search(filter: SearchFilters = SearchFilters()): F[List[StacItem]] = - Logger[F].trace(s"search: ${filter.asJson.spaces4}") >> + Logger[F].trace(s"search: ${filter.asJson.spaces2}") >> client .send(basicRequest.post(baseUri.withPath("search")).body(filter.asJson.noSpaces).response(asJson[Json])) .map(_.body.flatMap(_.hcursor.downField("features").as[List[StacItem]])) @@ -62,7 +64,7 @@ case class SttpStacClient[F[_]: MonadError[*[_], Throwable]: Logger]( .flatMap(MonadError[F, Throwable].fromEither) def itemCreate(collectionId: NonEmptyString, item: StacItem): F[StacItem] = - Logger[F].trace(s"createItem: ($collectionId, $item)") >> + Logger[F].trace(s"createItem: ($collectionId, ${item.asJson.spaces2})") >> client .send( basicRequest @@ -74,7 +76,7 @@ case class SttpStacClient[F[_]: MonadError[*[_], Throwable]: Logger]( .flatMap(MonadError[F, Throwable].fromEither) def collectionCreate(collection: StacCollection): F[StacCollection] = - Logger[F].trace(s"createCollection: $collection") >> + Logger[F].trace(s"createCollection: ${collection.asJson.spaces2}") >> client .send( basicRequest diff --git a/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala similarity index 94% rename from modules/client/js/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala rename to modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala index 8231972b..54154a82 100644 --- a/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala +++ b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala @@ -22,7 +22,8 @@ import eu.timepit.refined.types.string.NonEmptyString /** TODO: instead of returning F[List] we can return fs2.Stream */ trait StacClient[F[_]] { - def search(filter: SearchFilters = SearchFilters()): F[List[StacItem]] + type Filter + def search(filter: Filter): F[List[StacItem]] def collections: F[List[StacCollection]] def collection(collectionId: NonEmptyString): F[Option[StacCollection]] def items(collectionId: NonEmptyString): F[List[StacItem]] From 1d8a5f8f96851517a4093097561fa4bb62b21861 Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Thu, 24 Dec 2020 11:20:19 -0500 Subject: [PATCH 15/27] Add more tests --- .../stac4s/api/client/StacClientSpec.scala | 41 ++++++++++++++++++- .../stac4s/api/client/SttpStacClient.scala | 3 +- .../stac4s/api/client/StacClientSpec.scala | 41 ++++++++++++++++++- 3 files changed, 80 insertions(+), 5 deletions(-) diff --git a/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala b/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala index 7ef4b910..b4447583 100644 --- a/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala +++ b/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala @@ -11,6 +11,7 @@ import org.scalatest.funspec.AnyFunSpec import org.scalatest.matchers.should.Matchers import sttp.client3.testing.SttpBackendStub import sttp.client3.{Response, UriContext} +import sttp.model.Method import sttp.monad.EitherMonad class StacClientSpec extends AnyFunSpec with Matchers with JsInstances with BeforeAndAfterAll { @@ -23,13 +24,19 @@ class StacClientSpec extends AnyFunSpec with Matchers with JsInstances with Befo .ok(arbItemCollectionShort.arbitrary.sample.asJson.asRight) .asRight } - .whenRequestMatches(_.uri.path == Seq("collections")) + .whenRequestMatches { + case req if req.method == Method.GET => req.uri.path == Seq("collections") + case _ => false + } .thenRespondF { _ => Response .ok(JsonObject("collections" -> arbCollectionShort.arbitrary.sample.toList.asJson).asJson.asRight) .asRight } - .whenRequestMatches(_.uri.path == Seq("collections", "collection_id", "items")) + .whenRequestMatches { + case req if req.method == Method.GET => req.uri.path == Seq("collections", "collection_id", "items") + case _ => false + } .thenRespondF { _ => Response .ok(arbItemCollectionShort.arbitrary.sample.asJson.asRight) @@ -41,6 +48,24 @@ class StacClientSpec extends AnyFunSpec with Matchers with JsInstances with Befo .ok(arbItemShort.arbitrary.sample.asRight) .asRight } + .whenRequestMatches { + case req if req.method == Method.POST => req.uri.path == Seq("collections", "collection_id", "items") + case _ => false + } + .thenRespondF { _ => + Response + .ok(arbItemShort.arbitrary.sample.get.asRight) + .asRight + } + .whenRequestMatches { + case req if req.method == Method.POST => req.uri.path == Seq("collections") + case _ => false + } + .thenRespondF { _ => + Response + .ok(arbCollectionShort.arbitrary.sample.get.asRight) + .asRight + } describe("StacClientSpec") { it("search") { @@ -69,6 +94,18 @@ class StacClientSpec extends AnyFunSpec with Matchers with JsInstances with Befo .valueOr(throw _) .size should be > 0 } + + it("itemCreate") { + SttpStacClient(backend, uri"http://localhost:9090") + .itemCreate(NonEmptyString.unsafeFrom("collection_id"), arbItemShort.arbitrary.sample.get) + .map(_.id should not be empty) + } + + it("collectionCreate") { + SttpStacClient(backend, uri"http://localhost:9090") + .collectionCreate(arbCollectionShort.arbitrary.sample.get) + .map(_.id should not be empty) + } } override def afterAll(): Unit = backend.close().valueOr(throw _) diff --git a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala index 224b00c8..26993622 100644 --- a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala +++ b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala @@ -5,6 +5,7 @@ import com.azavea.stac4s.{StacCollection, StacItem} import cats.MonadError import cats.syntax.flatMap._ import cats.syntax.functor._ +import cats.syntax.option._ import eu.timepit.refined.types.string.NonEmptyString import io.chrisdavenport.log4cats.Logger import io.circe.Json @@ -69,7 +70,7 @@ case class SttpStacClient[F[_]: MonadError[*[_], Throwable]: Logger]( .send( basicRequest .post(baseUri.withPath("collections", collectionId.value, "items")) - .body(item.asJson.noSpaces) + .body(item.copy(collection = collectionId.value.some).asJson.noSpaces) .response(asJson[StacItem]) ) .map(_.body) diff --git a/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala b/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala index caeb48ca..0af8c1d8 100644 --- a/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala +++ b/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala @@ -16,6 +16,7 @@ import sttp.client3.http4s.Http4sBackend import sttp.client3.impl.cats.CatsMonadAsyncError import sttp.client3.testing.SttpBackendStub import sttp.client3.{Response, UriContext} +import sttp.model.Method class StacClientSpec extends IOSpec with JvmInstances with BeforeAndAfterAll { @@ -27,13 +28,19 @@ class StacClientSpec extends IOSpec with JvmInstances with BeforeAndAfterAll { .ok(arbItemCollectionShort.arbitrary.sample.asJson.asRight) .pure[IO] } - .whenRequestMatches(_.uri.path == Seq("collections")) + .whenRequestMatches { + case req if req.method == Method.GET => req.uri.path == Seq("collections") + case _ => false + } .thenRespondF { _ => Response .ok(JsonObject("collections" -> arbCollectionShort.arbitrary.sample.toList.asJson).asJson.asRight) .pure[IO] } - .whenRequestMatches(_.uri.path == Seq("collections", "collection_id", "items")) + .whenRequestMatches { + case req if req.method == Method.GET => req.uri.path == Seq("collections", "collection_id", "items") + case _ => false + } .thenRespondF { _ => Response .ok(arbItemCollectionShort.arbitrary.sample.asJson.asRight) @@ -45,6 +52,24 @@ class StacClientSpec extends IOSpec with JvmInstances with BeforeAndAfterAll { .ok(arbItemShort.arbitrary.sample.asRight) .pure[IO] } + .whenRequestMatches { + case req if req.method == Method.POST => req.uri.path == Seq("collections", "collection_id", "items") + case _ => false + } + .thenRespondF { _ => + Response + .ok(arbItemShort.arbitrary.sample.get.asRight) + .pure[IO] + } + .whenRequestMatches { + case req if req.method == Method.POST => req.uri.path == Seq("collections") + case _ => false + } + .thenRespondF { _ => + Response + .ok(arbCollectionShort.arbitrary.sample.get.asRight) + .pure[IO] + } describe("StacClientSpec") { it("search") { @@ -69,6 +94,18 @@ class StacClientSpec extends IOSpec with JvmInstances with BeforeAndAfterAll { .item(NonEmptyString.unsafeFrom("collection_id"), NonEmptyString.unsafeFrom("item_id")) .map(_.size should be > 0) } + + it("itemCreate") { + SttpStacClient(backend, uri"http://localhost:9090") + .itemCreate(NonEmptyString.unsafeFrom("collection_id"), arbItemShort.arbitrary.sample.get) + .map(_.id should not be empty) + } + + it("collectionCreate") { + SttpStacClient(backend, uri"http://localhost:9090") + .collectionCreate(arbCollectionShort.arbitrary.sample.get) + .map(_.id should not be empty) + } } describe("STAC Client Examples") { From 0647db764ce65f73aac44bc3b2bc6ee24e975f73 Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Thu, 24 Dec 2020 11:23:04 -0500 Subject: [PATCH 16/27] Remove log4cats from specs --- build.sbt | 2 - .../stac4s/api/client/SttpStacClient.scala | 102 ++++++++---------- .../src/test/scala/com/azavea/IOSpec.scala | 3 - 3 files changed, 47 insertions(+), 60 deletions(-) diff --git a/build.sbt b/build.sbt index c14e7f16..9b69d553 100644 --- a/build.sbt +++ b/build.sbt @@ -226,8 +226,6 @@ lazy val client = crossProject(JSPlatform, JVMPlatform) .jvmSettings(libraryDependencies ++= coreDependenciesJVM) .jvmSettings( libraryDependencies ++= Seq( - "io.chrisdavenport" %%% "log4cats-core" % Versions.Log4Cats, - "io.chrisdavenport" %%% "log4cats-slf4j" % Versions.Log4Cats % Test, "com.softwaremill.sttp.client3" %% "http4s-backend" % Versions.Sttp % Test, "com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % Versions.Sttp % Test ) diff --git a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala index 26993622..034e9358 100644 --- a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala +++ b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala @@ -7,14 +7,13 @@ import cats.syntax.flatMap._ import cats.syntax.functor._ import cats.syntax.option._ import eu.timepit.refined.types.string.NonEmptyString -import io.chrisdavenport.log4cats.Logger import io.circe.Json import io.circe.syntax._ import sttp.client3._ import sttp.client3.circe.asJson import sttp.model.Uri -case class SttpStacClient[F[_]: MonadError[*[_], Throwable]: Logger]( +case class SttpStacClient[F[_]: MonadError[*[_], Throwable]]( client: SttpBackend[F, Any], baseUri: Uri ) extends StacClient[F] { @@ -22,69 +21,62 @@ case class SttpStacClient[F[_]: MonadError[*[_], Throwable]: Logger]( type Filter = SearchFilters def search(filter: SearchFilters = SearchFilters()): F[List[StacItem]] = - Logger[F].trace(s"search: ${filter.asJson.spaces2}") >> - client - .send(basicRequest.post(baseUri.withPath("search")).body(filter.asJson.noSpaces).response(asJson[Json])) - .map(_.body.flatMap(_.hcursor.downField("features").as[List[StacItem]])) - .flatMap(MonadError[F, Throwable].fromEither) + client + .send(basicRequest.post(baseUri.withPath("search")).body(filter.asJson.noSpaces).response(asJson[Json])) + .map(_.body.flatMap(_.hcursor.downField("features").as[List[StacItem]])) + .flatMap(MonadError[F, Throwable].fromEither) def collections: F[List[StacCollection]] = - Logger[F].trace("collections") >> - client - .send(basicRequest.get(baseUri.withPath("collections")).response(asJson[Json])) - .map(_.body.flatMap(_.hcursor.downField("collections").as[List[StacCollection]])) - .flatMap(MonadError[F, Throwable].fromEither) + client + .send(basicRequest.get(baseUri.withPath("collections")).response(asJson[Json])) + .map(_.body.flatMap(_.hcursor.downField("collections").as[List[StacCollection]])) + .flatMap(MonadError[F, Throwable].fromEither) def collection(collectionId: NonEmptyString): F[Option[StacCollection]] = - Logger[F].trace(s"collection: $collectionId") >> - client - .send( - basicRequest - .get(baseUri.withPath("collections", collectionId.value)) - .response(asJson[Option[StacCollection]]) - ) - .map(_.body) - .flatMap(MonadError[F, Throwable].fromEither) + client + .send( + basicRequest + .get(baseUri.withPath("collections", collectionId.value)) + .response(asJson[Option[StacCollection]]) + ) + .map(_.body) + .flatMap(MonadError[F, Throwable].fromEither) def items(collectionId: NonEmptyString): F[List[StacItem]] = - Logger[F].trace(s"items: $collectionId") >> - client - .send(basicRequest.get(baseUri.withPath("collections", collectionId.value, "items")).response(asJson[Json])) - .map(_.body.flatMap(_.hcursor.downField("features").as[List[StacItem]])) - .flatMap(MonadError[F, Throwable].fromEither) + client + .send(basicRequest.get(baseUri.withPath("collections", collectionId.value, "items")).response(asJson[Json])) + .map(_.body.flatMap(_.hcursor.downField("features").as[List[StacItem]])) + .flatMap(MonadError[F, Throwable].fromEither) def item(collectionId: NonEmptyString, itemId: NonEmptyString): F[Option[StacItem]] = - Logger[F].trace(s"items: $collectionId, $itemId") >> - client - .send( - basicRequest - .get(baseUri.withPath("collections", collectionId.value, "items", itemId.value)) - .response(asJson[Option[StacItem]]) - ) - .map(_.body) - .flatMap(MonadError[F, Throwable].fromEither) + client + .send( + basicRequest + .get(baseUri.withPath("collections", collectionId.value, "items", itemId.value)) + .response(asJson[Option[StacItem]]) + ) + .map(_.body) + .flatMap(MonadError[F, Throwable].fromEither) def itemCreate(collectionId: NonEmptyString, item: StacItem): F[StacItem] = - Logger[F].trace(s"createItem: ($collectionId, ${item.asJson.spaces2})") >> - client - .send( - basicRequest - .post(baseUri.withPath("collections", collectionId.value, "items")) - .body(item.copy(collection = collectionId.value.some).asJson.noSpaces) - .response(asJson[StacItem]) - ) - .map(_.body) - .flatMap(MonadError[F, Throwable].fromEither) + client + .send( + basicRequest + .post(baseUri.withPath("collections", collectionId.value, "items")) + .body(item.copy(collection = collectionId.value.some).asJson.noSpaces) + .response(asJson[StacItem]) + ) + .map(_.body) + .flatMap(MonadError[F, Throwable].fromEither) def collectionCreate(collection: StacCollection): F[StacCollection] = - Logger[F].trace(s"createCollection: ${collection.asJson.spaces2}") >> - client - .send( - basicRequest - .post(baseUri.withPath("collections")) - .body(collection.asJson.noSpaces) - .response(asJson[StacCollection]) - ) - .map(_.body) - .flatMap(MonadError[F, Throwable].fromEither) + client + .send( + basicRequest + .post(baseUri.withPath("collections")) + .body(collection.asJson.noSpaces) + .response(asJson[StacCollection]) + ) + .map(_.body) + .flatMap(MonadError[F, Throwable].fromEither) } diff --git a/modules/client/jvm/src/test/scala/com/azavea/IOSpec.scala b/modules/client/jvm/src/test/scala/com/azavea/IOSpec.scala index 018bcb85..1fd967f8 100644 --- a/modules/client/jvm/src/test/scala/com/azavea/IOSpec.scala +++ b/modules/client/jvm/src/test/scala/com/azavea/IOSpec.scala @@ -1,8 +1,6 @@ package com.azavea import cats.effect.{ContextShift, IO, Timer} -import io.chrisdavenport.log4cats.Logger -import io.chrisdavenport.log4cats.slf4j.Slf4jLogger import org.scalatest.funspec.AsyncFunSpec import org.scalatest.matchers.should.Matchers import org.scalatest.{Assertion, Assertions} @@ -10,7 +8,6 @@ import org.scalatest.{Assertion, Assertions} trait IOSpec extends AsyncFunSpec with Assertions with Matchers { implicit val contextShift: ContextShift[IO] = IO.contextShift(executionContext) implicit val timer: Timer[IO] = IO.timer(executionContext) - implicit val logger: Logger[IO] = Slf4jLogger.getLogger[IO] private val itWord = new ItWord From 2733d69999e0adc231da6cc104f98d396362427e Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Thu, 24 Dec 2020 12:14:28 -0500 Subject: [PATCH 17/27] Move BaseSttpStacClient into shared folder --- build.sbt | 29 +++--- .../stac4s/api/client/SearchFilters.scala | 3 + .../stac4s/api/client/SttpStacClient.scala | 81 ++------------- .../stac4s/api/client/SearchFilters.scala | 3 + .../stac4s/api/client/SttpStacClient.scala | 82 ++-------------- .../api/client/BaseSttpStacClient.scala | 98 +++++++++++++++++++ 6 files changed, 133 insertions(+), 163 deletions(-) create mode 100644 modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/BaseSttpStacClient.scala diff --git a/build.sbt b/build.sbt index 9b69d553..3779279a 100644 --- a/build.sbt +++ b/build.sbt @@ -208,26 +208,27 @@ lazy val client = crossProject(JSPlatform, JVMPlatform) .settings(publishSettings) .settings( libraryDependencies ++= Seq( - "io.circe" %%% "circe-core" % Versions.Circe, - "io.circe" %%% "circe-generic" % Versions.Circe, - "io.circe" %%% "circe-refined" % Versions.Circe, - "com.chuusai" %%% "shapeless" % Versions.Shapeless, - "eu.timepit" %%% "refined" % Versions.Refined, - "org.typelevel" %%% "cats-core" % Versions.Cats, - "com.softwaremill.sttp.client3" %%% "core" % Versions.Sttp, - "com.softwaremill.sttp.client3" %%% "circe" % Versions.Sttp, - "com.softwaremill.sttp.client3" %%% "json-common" % Versions.Sttp, - "com.softwaremill.sttp.model" %%% "core" % Versions.SttpModel, - "com.softwaremill.sttp.shared" %%% "core" % Versions.SttpShared, - "org.scalatest" %%% "scalatest" % Versions.Scalatest % Test + "io.circe" %%% "circe-core" % Versions.Circe, + "io.circe" %%% "circe-generic" % Versions.Circe, + "io.circe" %%% "circe-refined" % Versions.Circe, + "com.chuusai" %%% "shapeless" % Versions.Shapeless, + "eu.timepit" %%% "refined" % Versions.Refined, + "org.typelevel" %%% "cats-core" % Versions.Cats, + "org.typelevel" %%% "alleycats-core" % Versions.Cats, + "com.softwaremill.sttp.client3" %%% "core" % Versions.Sttp, + "com.softwaremill.sttp.client3" %%% "circe" % Versions.Sttp, + "com.softwaremill.sttp.client3" %%% "json-common" % Versions.Sttp, + "com.softwaremill.sttp.model" %%% "core" % Versions.SttpModel, + "com.softwaremill.sttp.shared" %%% "core" % Versions.SttpShared, + "org.scalatest" %%% "scalatest" % Versions.Scalatest % Test ) ) .jsSettings(libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % "2.1.0") .jvmSettings(libraryDependencies ++= coreDependenciesJVM) .jvmSettings( libraryDependencies ++= Seq( - "com.softwaremill.sttp.client3" %% "http4s-backend" % Versions.Sttp % Test, - "com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % Versions.Sttp % Test + "com.softwaremill.sttp.client3" %% "http4s-backend" % Versions.Sttp % Test, + "com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % Versions.Sttp % Test ) ) diff --git a/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala index 57849d7d..f3eda256 100644 --- a/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala +++ b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala @@ -5,6 +5,7 @@ import com.azavea.stac4s.api.client.utils.ClientCodecs import com.azavea.stac4s.geometry.Geometry import com.azavea.stac4s.types.TemporalExtent +import alleycats.Empty import eu.timepit.refined.types.numeric.NonNegInt import io.circe._ import io.circe.generic.semiauto._ @@ -23,6 +24,8 @@ case class SearchFilters( object SearchFilters extends ClientCodecs { + implicit val searchFiltersEmpty: Empty[SearchFilters] = Empty(SearchFilters()) + implicit val searchFilterDecoder: Decoder[SearchFilters] = { c => for { bbox <- c.downField("bbox").as[Option[Bbox]] diff --git a/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala index d08110ba..e5e9b3b9 100644 --- a/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala +++ b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala @@ -1,81 +1,14 @@ package com.azavea.stac4s.api.client -import com.azavea.stac4s.{StacCollection, StacItem} - import cats.MonadError -import cats.syntax.flatMap._ -import cats.syntax.functor._ -import eu.timepit.refined.types.string.NonEmptyString -import io.circe.Json -import io.circe.syntax._ -import sttp.client3._ -import sttp.client3.circe.asJson +import sttp.client3.SttpBackend import sttp.model.Uri -case class SttpStacClient[F[_]: MonadError[*[_], Throwable]]( - client: SttpBackend[F, Any], - baseUri: Uri -) extends StacClient[F] { - - type Filter = SearchFilters - - def search(filter: SearchFilters = SearchFilters()): F[List[StacItem]] = - client - .send(basicRequest.post(baseUri.withPath("search")).body(filter.asJson.noSpaces).response(asJson[Json])) - .map(_.body.flatMap(_.hcursor.downField("features").as[List[StacItem]])) - .flatMap(MonadError[F, Throwable].fromEither) - - def collections: F[List[StacCollection]] = - client - .send(basicRequest.get(baseUri.withPath("collections")).response(asJson[Json])) - .map(_.body.flatMap(_.hcursor.downField("collections").as[List[StacCollection]])) - .flatMap(MonadError[F, Throwable].fromEither) - - def collection(collectionId: NonEmptyString): F[Option[StacCollection]] = - client - .send( - basicRequest - .get(baseUri.withPath("collections", collectionId.value)) - .response(asJson[Option[StacCollection]]) - ) - .map(_.body) - .flatMap(MonadError[F, Throwable].fromEither) - - def items(collectionId: NonEmptyString): F[List[StacItem]] = - client - .send(basicRequest.get(baseUri.withPath("collections", collectionId.value, "items")).response(asJson[Json])) - .map(_.body.flatMap(_.hcursor.downField("features").as[List[StacItem]])) - .flatMap(MonadError[F, Throwable].fromEither) - - def item(collectionId: NonEmptyString, itemId: NonEmptyString): F[Option[StacItem]] = - client - .send( - basicRequest - .get(baseUri.withPath("collections", collectionId.value, "items", itemId.value)) - .response(asJson[Option[StacItem]]) - ) - .map(_.body) - .flatMap(MonadError[F, Throwable].fromEither) - - def itemCreate(collectionId: NonEmptyString, item: StacItem): F[StacItem] = - client - .send( - basicRequest - .post(baseUri.withPath("collections", collectionId.value, "items")) - .body(item.asJson.noSpaces) - .response(asJson[StacItem]) - ) - .map(_.body) - .flatMap(MonadError[F, Throwable].fromEither) +object SttpStacClient { - def collectionCreate(collection: StacCollection): F[StacCollection] = - client - .send( - basicRequest - .post(baseUri.withPath("collections")) - .body(collection.asJson.noSpaces) - .response(asJson[StacCollection]) - ) - .map(_.body) - .flatMap(MonadError[F, Throwable].fromEither) + def apply[F[_]: MonadError[*[_], Throwable]]( + client: SttpBackend[F, Any], + baseUri: Uri + ): BaseSttpStacClient.Aux[F, SearchFilters] = + BaseSttpStacClient.instance[F, SearchFilters](client, baseUri) } diff --git a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala index f8d10342..0e7efbcc 100644 --- a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala +++ b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala @@ -4,6 +4,7 @@ import com.azavea.stac4s.Bbox import com.azavea.stac4s.api.client.utils.ClientCodecs import com.azavea.stac4s.types.TemporalExtent +import alleycats.Empty import eu.timepit.refined.types.numeric.NonNegInt import geotrellis.vector.{io => _, _} import io.circe._ @@ -23,6 +24,8 @@ case class SearchFilters( object SearchFilters extends ClientCodecs { + implicit val searchFiltersEmpty: Empty[SearchFilters] = Empty(SearchFilters()) + implicit val searchFilterDecoder: Decoder[SearchFilters] = { c => for { bbox <- c.downField("bbox").as[Option[Bbox]] diff --git a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala index 034e9358..e5e9b3b9 100644 --- a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala +++ b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala @@ -1,82 +1,14 @@ package com.azavea.stac4s.api.client -import com.azavea.stac4s.{StacCollection, StacItem} - import cats.MonadError -import cats.syntax.flatMap._ -import cats.syntax.functor._ -import cats.syntax.option._ -import eu.timepit.refined.types.string.NonEmptyString -import io.circe.Json -import io.circe.syntax._ -import sttp.client3._ -import sttp.client3.circe.asJson +import sttp.client3.SttpBackend import sttp.model.Uri -case class SttpStacClient[F[_]: MonadError[*[_], Throwable]]( - client: SttpBackend[F, Any], - baseUri: Uri -) extends StacClient[F] { - - type Filter = SearchFilters - - def search(filter: SearchFilters = SearchFilters()): F[List[StacItem]] = - client - .send(basicRequest.post(baseUri.withPath("search")).body(filter.asJson.noSpaces).response(asJson[Json])) - .map(_.body.flatMap(_.hcursor.downField("features").as[List[StacItem]])) - .flatMap(MonadError[F, Throwable].fromEither) - - def collections: F[List[StacCollection]] = - client - .send(basicRequest.get(baseUri.withPath("collections")).response(asJson[Json])) - .map(_.body.flatMap(_.hcursor.downField("collections").as[List[StacCollection]])) - .flatMap(MonadError[F, Throwable].fromEither) - - def collection(collectionId: NonEmptyString): F[Option[StacCollection]] = - client - .send( - basicRequest - .get(baseUri.withPath("collections", collectionId.value)) - .response(asJson[Option[StacCollection]]) - ) - .map(_.body) - .flatMap(MonadError[F, Throwable].fromEither) - - def items(collectionId: NonEmptyString): F[List[StacItem]] = - client - .send(basicRequest.get(baseUri.withPath("collections", collectionId.value, "items")).response(asJson[Json])) - .map(_.body.flatMap(_.hcursor.downField("features").as[List[StacItem]])) - .flatMap(MonadError[F, Throwable].fromEither) - - def item(collectionId: NonEmptyString, itemId: NonEmptyString): F[Option[StacItem]] = - client - .send( - basicRequest - .get(baseUri.withPath("collections", collectionId.value, "items", itemId.value)) - .response(asJson[Option[StacItem]]) - ) - .map(_.body) - .flatMap(MonadError[F, Throwable].fromEither) - - def itemCreate(collectionId: NonEmptyString, item: StacItem): F[StacItem] = - client - .send( - basicRequest - .post(baseUri.withPath("collections", collectionId.value, "items")) - .body(item.copy(collection = collectionId.value.some).asJson.noSpaces) - .response(asJson[StacItem]) - ) - .map(_.body) - .flatMap(MonadError[F, Throwable].fromEither) +object SttpStacClient { - def collectionCreate(collection: StacCollection): F[StacCollection] = - client - .send( - basicRequest - .post(baseUri.withPath("collections")) - .body(collection.asJson.noSpaces) - .response(asJson[StacCollection]) - ) - .map(_.body) - .flatMap(MonadError[F, Throwable].fromEither) + def apply[F[_]: MonadError[*[_], Throwable]]( + client: SttpBackend[F, Any], + baseUri: Uri + ): BaseSttpStacClient.Aux[F, SearchFilters] = + BaseSttpStacClient.instance[F, SearchFilters](client, baseUri) } diff --git a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/BaseSttpStacClient.scala b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/BaseSttpStacClient.scala new file mode 100644 index 00000000..dd1b591a --- /dev/null +++ b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/BaseSttpStacClient.scala @@ -0,0 +1,98 @@ +package com.azavea.stac4s.api.client + +import com.azavea.stac4s.{StacCollection, StacItem} + +import alleycats.Empty +import cats.MonadError +import cats.syntax.flatMap._ +import cats.syntax.functor._ +import eu.timepit.refined.types.string.NonEmptyString +import io.circe.syntax._ +import io.circe.{Encoder, Json} +import sttp.client3.circe.asJson +import sttp.client3.{SttpBackend, basicRequest} +import sttp.model.Uri + +abstract class BaseSttpStacClient[F[_]: MonadError[*[_], Throwable]]( + client: SttpBackend[F, Any], + baseUri: Uri +) extends StacClient[F] { + + type Filter + + protected implicit def filterEncoder: Encoder[Filter] + protected implicit def filterEmpty: Empty[Filter] + + def search(filter: Filter = filterEmpty.empty): F[List[StacItem]] = + client + .send(basicRequest.post(baseUri.withPath("search")).body(filter.asJson.noSpaces).response(asJson[Json])) + .map(_.body.flatMap(_.hcursor.downField("features").as[List[StacItem]])) + .flatMap(MonadError[F, Throwable].fromEither) + + def collections: F[List[StacCollection]] = + client + .send(basicRequest.get(baseUri.withPath("collections")).response(asJson[Json])) + .map(_.body.flatMap(_.hcursor.downField("collections").as[List[StacCollection]])) + .flatMap(MonadError[F, Throwable].fromEither) + + def collection(collectionId: NonEmptyString): F[Option[StacCollection]] = + client + .send( + basicRequest + .get(baseUri.withPath("collections", collectionId.value)) + .response(asJson[Option[StacCollection]]) + ) + .map(_.body) + .flatMap(MonadError[F, Throwable].fromEither) + + def items(collectionId: NonEmptyString): F[List[StacItem]] = + client + .send(basicRequest.get(baseUri.withPath("collections", collectionId.value, "items")).response(asJson[Json])) + .map(_.body.flatMap(_.hcursor.downField("features").as[List[StacItem]])) + .flatMap(MonadError[F, Throwable].fromEither) + + def item(collectionId: NonEmptyString, itemId: NonEmptyString): F[Option[StacItem]] = + client + .send( + basicRequest + .get(baseUri.withPath("collections", collectionId.value, "items", itemId.value)) + .response(asJson[Option[StacItem]]) + ) + .map(_.body) + .flatMap(MonadError[F, Throwable].fromEither) + + def itemCreate(collectionId: NonEmptyString, item: StacItem): F[StacItem] = + client + .send( + basicRequest + .post(baseUri.withPath("collections", collectionId.value, "items")) + .body(item.asJson.noSpaces) + .response(asJson[StacItem]) + ) + .map(_.body) + .flatMap(MonadError[F, Throwable].fromEither) + + def collectionCreate(collection: StacCollection): F[StacCollection] = + client + .send( + basicRequest + .post(baseUri.withPath("collections")) + .body(collection.asJson.noSpaces) + .response(asJson[StacCollection]) + ) + .map(_.body) + .flatMap(MonadError[F, Throwable].fromEither) +} + +object BaseSttpStacClient { + type Aux[F[_], S] = BaseSttpStacClient[F] { type Filter = S } + + def instance[F[_]: MonadError[*[_], Throwable], S]( + client: SttpBackend[F, Any], + baseUri: Uri + )(implicit sencoder: Encoder[S], sempty: Empty[S]): Aux[F, S] = new BaseSttpStacClient[F](client, baseUri) { + type Filter = S + protected val filterEncoder: Encoder[Filter] = sencoder + protected val filterEmpty: Empty[Filter] = sempty + } +} From 7377a3f61c9708b63045b2bd22a70de49a111e18 Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Thu, 24 Dec 2020 12:24:36 -0500 Subject: [PATCH 18/27] Try to limit JS mem usage --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index ff350af5..4eed8e62 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -36,6 +36,7 @@ aliases: environment: # https://circleci.com/docs/2.0/java-oom/ _JAVA_OPTIONS: "-Xms128m -Xmx1536m" + TOOL_NODE_FLAGS: "--max_old_space_size=512" version: 2 workflows: From 990ad20183485fdeb2c6d7066c247efb8c9c3018 Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Thu, 24 Dec 2020 13:19:20 -0500 Subject: [PATCH 19/27] Remove unnecessary dep --- build.sbt | 25 +++++++++---------- .../stac4s/api/client/SearchFilters.scala | 3 --- .../stac4s/api/client/StacClientSpec.scala | 3 +-- .../stac4s/api/client/SearchFilters.scala | 3 --- .../stac4s/api/client/StacClientSpec.scala | 3 +-- .../api/client/BaseSttpStacClient.scala | 19 +++++++++----- .../azavea/stac4s/api/client/StacClient.scala | 1 + 7 files changed, 28 insertions(+), 29 deletions(-) diff --git a/build.sbt b/build.sbt index 3779279a..65fb1578 100644 --- a/build.sbt +++ b/build.sbt @@ -208,19 +208,18 @@ lazy val client = crossProject(JSPlatform, JVMPlatform) .settings(publishSettings) .settings( libraryDependencies ++= Seq( - "io.circe" %%% "circe-core" % Versions.Circe, - "io.circe" %%% "circe-generic" % Versions.Circe, - "io.circe" %%% "circe-refined" % Versions.Circe, - "com.chuusai" %%% "shapeless" % Versions.Shapeless, - "eu.timepit" %%% "refined" % Versions.Refined, - "org.typelevel" %%% "cats-core" % Versions.Cats, - "org.typelevel" %%% "alleycats-core" % Versions.Cats, - "com.softwaremill.sttp.client3" %%% "core" % Versions.Sttp, - "com.softwaremill.sttp.client3" %%% "circe" % Versions.Sttp, - "com.softwaremill.sttp.client3" %%% "json-common" % Versions.Sttp, - "com.softwaremill.sttp.model" %%% "core" % Versions.SttpModel, - "com.softwaremill.sttp.shared" %%% "core" % Versions.SttpShared, - "org.scalatest" %%% "scalatest" % Versions.Scalatest % Test + "io.circe" %%% "circe-core" % Versions.Circe, + "io.circe" %%% "circe-generic" % Versions.Circe, + "io.circe" %%% "circe-refined" % Versions.Circe, + "com.chuusai" %%% "shapeless" % Versions.Shapeless, + "eu.timepit" %%% "refined" % Versions.Refined, + "org.typelevel" %%% "cats-core" % Versions.Cats, + "com.softwaremill.sttp.client3" %%% "core" % Versions.Sttp, + "com.softwaremill.sttp.client3" %%% "circe" % Versions.Sttp, + "com.softwaremill.sttp.client3" %%% "json-common" % Versions.Sttp, + "com.softwaremill.sttp.model" %%% "core" % Versions.SttpModel, + "com.softwaremill.sttp.shared" %%% "core" % Versions.SttpShared, + "org.scalatest" %%% "scalatest" % Versions.Scalatest % Test ) ) .jsSettings(libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % "2.1.0") diff --git a/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala index f3eda256..57849d7d 100644 --- a/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala +++ b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala @@ -5,7 +5,6 @@ import com.azavea.stac4s.api.client.utils.ClientCodecs import com.azavea.stac4s.geometry.Geometry import com.azavea.stac4s.types.TemporalExtent -import alleycats.Empty import eu.timepit.refined.types.numeric.NonNegInt import io.circe._ import io.circe.generic.semiauto._ @@ -24,8 +23,6 @@ case class SearchFilters( object SearchFilters extends ClientCodecs { - implicit val searchFiltersEmpty: Empty[SearchFilters] = Empty(SearchFilters()) - implicit val searchFilterDecoder: Decoder[SearchFilters] = { c => for { bbox <- c.downField("bbox").as[Option[Bbox]] diff --git a/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala b/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala index b4447583..b0c1b8b6 100644 --- a/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala +++ b/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala @@ -69,8 +69,7 @@ class StacClientSpec extends AnyFunSpec with Matchers with JsInstances with Befo describe("StacClientSpec") { it("search") { - SttpStacClient(backend, uri"http://localhost:9090") - .search() + SttpStacClient(backend, uri"http://localhost:9090").search .valueOr(throw _) .size should be > 0 } diff --git a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala index 0e7efbcc..f8d10342 100644 --- a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala +++ b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SearchFilters.scala @@ -4,7 +4,6 @@ import com.azavea.stac4s.Bbox import com.azavea.stac4s.api.client.utils.ClientCodecs import com.azavea.stac4s.types.TemporalExtent -import alleycats.Empty import eu.timepit.refined.types.numeric.NonNegInt import geotrellis.vector.{io => _, _} import io.circe._ @@ -24,8 +23,6 @@ case class SearchFilters( object SearchFilters extends ClientCodecs { - implicit val searchFiltersEmpty: Empty[SearchFilters] = Empty(SearchFilters()) - implicit val searchFilterDecoder: Decoder[SearchFilters] = { c => for { bbox <- c.downField("bbox").as[Option[Bbox]] diff --git a/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala b/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala index 0af8c1d8..010dd3de 100644 --- a/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala +++ b/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala @@ -73,8 +73,7 @@ class StacClientSpec extends IOSpec with JvmInstances with BeforeAndAfterAll { describe("StacClientSpec") { it("search") { - SttpStacClient(backend, uri"http://localhost:9090") - .search() + SttpStacClient(backend, uri"http://localhost:9090").search .map(_.size should be > 0) } diff --git a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/BaseSttpStacClient.scala b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/BaseSttpStacClient.scala index dd1b591a..60d6ebc9 100644 --- a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/BaseSttpStacClient.scala +++ b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/BaseSttpStacClient.scala @@ -2,10 +2,10 @@ package com.azavea.stac4s.api.client import com.azavea.stac4s.{StacCollection, StacItem} -import alleycats.Empty import cats.MonadError import cats.syntax.flatMap._ import cats.syntax.functor._ +import cats.syntax.option._ import eu.timepit.refined.types.string.NonEmptyString import io.circe.syntax._ import io.circe.{Encoder, Json} @@ -21,11 +21,19 @@ abstract class BaseSttpStacClient[F[_]: MonadError[*[_], Throwable]]( type Filter protected implicit def filterEncoder: Encoder[Filter] - protected implicit def filterEmpty: Empty[Filter] - def search(filter: Filter = filterEmpty.empty): F[List[StacItem]] = + def search: F[List[StacItem]] = search(None) + + def search(filter: Filter): F[List[StacItem]] = search(filter.asJson.some) + + private def search(filter: Option[Json]): F[List[StacItem]] = client - .send(basicRequest.post(baseUri.withPath("search")).body(filter.asJson.noSpaces).response(asJson[Json])) + .send { + filter + .fold(basicRequest)(f => basicRequest.body(f.asJson.noSpaces)) + .post(baseUri.withPath("search")) + .response(asJson[Json]) + } .map(_.body.flatMap(_.hcursor.downField("features").as[List[StacItem]])) .flatMap(MonadError[F, Throwable].fromEither) @@ -90,9 +98,8 @@ object BaseSttpStacClient { def instance[F[_]: MonadError[*[_], Throwable], S]( client: SttpBackend[F, Any], baseUri: Uri - )(implicit sencoder: Encoder[S], sempty: Empty[S]): Aux[F, S] = new BaseSttpStacClient[F](client, baseUri) { + )(implicit sencoder: Encoder[S]): Aux[F, S] = new BaseSttpStacClient[F](client, baseUri) { type Filter = S protected val filterEncoder: Encoder[Filter] = sencoder - protected val filterEmpty: Empty[Filter] = sempty } } diff --git a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala index 54154a82..4316f937 100644 --- a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala +++ b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala @@ -23,6 +23,7 @@ import eu.timepit.refined.types.string.NonEmptyString /** TODO: instead of returning F[List] we can return fs2.Stream */ trait StacClient[F[_]] { type Filter + def search: F[List[StacItem]] def search(filter: Filter): F[List[StacItem]] def collections: F[List[StacCollection]] def collection(collectionId: NonEmptyString): F[Option[StacCollection]] From d587f463b7d38e21eea91075d48a72c343389a32 Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Thu, 24 Dec 2020 13:25:34 -0500 Subject: [PATCH 20/27] Make JS tests more tiny --- .circleci/config.yml | 1 - modules/testing/js/src/main/scala/JsInstances.scala | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4eed8e62..ff350af5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -36,7 +36,6 @@ aliases: environment: # https://circleci.com/docs/2.0/java-oom/ _JAVA_OPTIONS: "-Xms128m -Xmx1536m" - TOOL_NODE_FLAGS: "--max_old_space_size=512" version: 2 workflows: diff --git a/modules/testing/js/src/main/scala/JsInstances.scala b/modules/testing/js/src/main/scala/JsInstances.scala index 077a6e95..234d597e 100644 --- a/modules/testing/js/src/main/scala/JsInstances.scala +++ b/modules/testing/js/src/main/scala/JsInstances.scala @@ -96,7 +96,7 @@ trait JsInstances { Gen.const("FeatureCollection"), Gen.const(StacVersion.unsafeFrom("0.9.0")), Gen.const(Nil), - Gen.listOf[StacItem](stacItemGen), + Gen.listOfN[StacItem](2, stacItemGen), Gen.const(Nil), Gen.const(().asJsonObject) ).mapN(ItemCollection.apply) From fdee5433870e65c78493cd0b09524e0fc6f7bf6c Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Thu, 24 Dec 2020 13:30:31 -0500 Subject: [PATCH 21/27] Give 2g to the JVM --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ff350af5..04c71a13 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -35,7 +35,7 @@ aliases: - image: circleci/openjdk:8-jdk-node environment: # https://circleci.com/docs/2.0/java-oom/ - _JAVA_OPTIONS: "-Xms128m -Xmx1536m" + _JAVA_OPTIONS: "-Xms128m -Xmx2g" version: 2 workflows: From 7f6abceeebea59dfc9a605891c73fc121116610b Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Thu, 24 Dec 2020 16:40:04 -0500 Subject: [PATCH 22/27] polygons => multipolygons --- .../js/src/main/scala/com/azavea/stac4s/geometry/Geometry.scala | 2 +- project/Versions.scala | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/core/js/src/main/scala/com/azavea/stac4s/geometry/Geometry.scala b/modules/core/js/src/main/scala/com/azavea/stac4s/geometry/Geometry.scala index e5db4584..449cdb03 100644 --- a/modules/core/js/src/main/scala/com/azavea/stac4s/geometry/Geometry.scala +++ b/modules/core/js/src/main/scala/com/azavea/stac4s/geometry/Geometry.scala @@ -66,7 +66,7 @@ object Geometry { } // for now, I'm ignoring multi polygons with holes - // however polygons still store coordinates as List[List[List[List[Double]]]] + // however multipolygons still store coordinates as List[List[List[List[Double]]]] implicit val encMultiPolygon: Encoder[MultiPolygon] = { mpolygon => Map( "type" -> "MultiPolygon".asJson, diff --git a/project/Versions.scala b/project/Versions.scala index 642e44e9..b45d45e4 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -15,5 +15,4 @@ object Versions { val Sttp = "3.0.0-RC13" val SttpModel = "1.2.0-RC9" val SttpShared = "1.0.0-RC11" - val Log4Cats = "1.1.1" } From 1d838803fb35642cfbd9285482b3e3a1722cd82c Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Thu, 24 Dec 2020 17:17:53 -0500 Subject: [PATCH 23/27] Address review comments --- build.sbt | 5 +--- .../stac4s/api/client/StacClientSpec.scala | 24 +------------------ .../azavea/stac4s/api/client/StacClient.scala | 17 ------------- 3 files changed, 2 insertions(+), 44 deletions(-) diff --git a/build.sbt b/build.sbt index 65fb1578..c8f166d6 100644 --- a/build.sbt +++ b/build.sbt @@ -225,10 +225,7 @@ lazy val client = crossProject(JSPlatform, JVMPlatform) .jsSettings(libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % "2.1.0") .jvmSettings(libraryDependencies ++= coreDependenciesJVM) .jvmSettings( - libraryDependencies ++= Seq( - "com.softwaremill.sttp.client3" %% "http4s-backend" % Versions.Sttp % Test, - "com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % Versions.Sttp % Test - ) + libraryDependencies += "com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % Versions.Sttp % Test ) lazy val clientJVM = client.jvm diff --git a/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala b/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala index 010dd3de..554d59a5 100644 --- a/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala +++ b/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala @@ -2,17 +2,14 @@ package com.azavea.stac4s.api.client import com.azavea.stac4s.testing.JvmInstances -import cats.effect.{Blocker, IO} +import cats.effect.IO import cats.syntax.applicative._ import cats.syntax.either._ import com.azavea.IOSpec -import eu.timepit.refined.collection.Empty import eu.timepit.refined.types.all.NonEmptyString import io.circe.JsonObject import io.circe.syntax._ import org.scalatest.BeforeAndAfterAll -import sttp.client3.asynchttpclient.cats.AsyncHttpClientCatsBackend -import sttp.client3.http4s.Http4sBackend import sttp.client3.impl.cats.CatsMonadAsyncError import sttp.client3.testing.SttpBackendStub import sttp.client3.{Response, UriContext} @@ -107,24 +104,5 @@ class StacClientSpec extends IOSpec with JvmInstances with BeforeAndAfterAll { } } - describe("STAC Client Examples") { - ignore("AsyncHttpClientCatsBackend") { - - val res = AsyncHttpClientCatsBackend[IO]().flatMap { backend => - SttpStacClient(backend, uri"http://localhost:9090").collections - } - - res map (_ shouldNot be(Empty)) - } - - ignore("Http4sBackend") { - val res = Blocker[IO].flatMap(Http4sBackend.usingDefaultClientBuilder[IO](_)).use { backend => - SttpStacClient(backend, uri"http://localhost:9090").collections - } - - res map (_ shouldNot be(Empty)) - } - } - override def afterAll(): Unit = backend.close().unsafeRunSync() } diff --git a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala index 4316f937..602c48c0 100644 --- a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala +++ b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/StacClient.scala @@ -1,26 +1,9 @@ -/* - * Copyright 2020 Azavea - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.azavea.stac4s.api.client import com.azavea.stac4s._ import eu.timepit.refined.types.string.NonEmptyString -/** TODO: instead of returning F[List] we can return fs2.Stream */ trait StacClient[F[_]] { type Filter def search: F[List[StacItem]] From baa5c6c19b7b6424793a6586607f155e96c2e6d8 Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Thu, 24 Dec 2020 19:30:55 -0500 Subject: [PATCH 24/27] Simplify and unify client specs --- build.sbt | 3 -- .../src/test/scala/com/azavea/IOSpec.scala | 19 ---------- .../stac4s/api/client/StacClientSpec.scala | 37 ++++++++++--------- 3 files changed, 20 insertions(+), 39 deletions(-) delete mode 100644 modules/client/jvm/src/test/scala/com/azavea/IOSpec.scala diff --git a/build.sbt b/build.sbt index c8f166d6..032ba79b 100644 --- a/build.sbt +++ b/build.sbt @@ -224,9 +224,6 @@ lazy val client = crossProject(JSPlatform, JVMPlatform) ) .jsSettings(libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % "2.1.0") .jvmSettings(libraryDependencies ++= coreDependenciesJVM) - .jvmSettings( - libraryDependencies += "com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % Versions.Sttp % Test - ) lazy val clientJVM = client.jvm lazy val clientJS = client.js diff --git a/modules/client/jvm/src/test/scala/com/azavea/IOSpec.scala b/modules/client/jvm/src/test/scala/com/azavea/IOSpec.scala deleted file mode 100644 index 1fd967f8..00000000 --- a/modules/client/jvm/src/test/scala/com/azavea/IOSpec.scala +++ /dev/null @@ -1,19 +0,0 @@ -package com.azavea - -import cats.effect.{ContextShift, IO, Timer} -import org.scalatest.funspec.AsyncFunSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.{Assertion, Assertions} - -trait IOSpec extends AsyncFunSpec with Assertions with Matchers { - implicit val contextShift: ContextShift[IO] = IO.contextShift(executionContext) - implicit val timer: Timer[IO] = IO.timer(executionContext) - - private val itWord = new ItWord - - def it(name: String)(test: => IO[Assertion]): Unit = itWord.apply(name)(test.unsafeToFuture()) - - def ignore(name: String)(test: => IO[Assertion]): Unit = super.ignore(name)(test.unsafeToFuture()) - - def describe(description: String)(fun: => Unit): Unit = super.describe(description)(fun) -} diff --git a/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala b/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala index 554d59a5..97c11196 100644 --- a/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala +++ b/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala @@ -2,28 +2,27 @@ package com.azavea.stac4s.api.client import com.azavea.stac4s.testing.JvmInstances -import cats.effect.IO -import cats.syntax.applicative._ import cats.syntax.either._ -import com.azavea.IOSpec import eu.timepit.refined.types.all.NonEmptyString import io.circe.JsonObject import io.circe.syntax._ import org.scalatest.BeforeAndAfterAll -import sttp.client3.impl.cats.CatsMonadAsyncError +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import sttp.client3.testing.SttpBackendStub import sttp.client3.{Response, UriContext} import sttp.model.Method +import sttp.monad.EitherMonad -class StacClientSpec extends IOSpec with JvmInstances with BeforeAndAfterAll { +class StacClientSpec extends AnyFunSpec with Matchers with JvmInstances with BeforeAndAfterAll { lazy val backend = - SttpBackendStub(new CatsMonadAsyncError[IO]()) + SttpBackendStub(EitherMonad) .whenRequestMatches(_.uri.path == Seq("search")) .thenRespondF { _ => Response .ok(arbItemCollectionShort.arbitrary.sample.asJson.asRight) - .pure[IO] + .asRight } .whenRequestMatches { case req if req.method == Method.GET => req.uri.path == Seq("collections") @@ -32,7 +31,7 @@ class StacClientSpec extends IOSpec with JvmInstances with BeforeAndAfterAll { .thenRespondF { _ => Response .ok(JsonObject("collections" -> arbCollectionShort.arbitrary.sample.toList.asJson).asJson.asRight) - .pure[IO] + .asRight } .whenRequestMatches { case req if req.method == Method.GET => req.uri.path == Seq("collections", "collection_id", "items") @@ -41,13 +40,13 @@ class StacClientSpec extends IOSpec with JvmInstances with BeforeAndAfterAll { .thenRespondF { _ => Response .ok(arbItemCollectionShort.arbitrary.sample.asJson.asRight) - .pure[IO] + .asRight } .whenRequestMatches(_.uri.path == Seq("collections", "collection_id", "items", "item_id")) .thenRespondF { _ => Response .ok(arbItemShort.arbitrary.sample.asRight) - .pure[IO] + .asRight } .whenRequestMatches { case req if req.method == Method.POST => req.uri.path == Seq("collections", "collection_id", "items") @@ -56,7 +55,7 @@ class StacClientSpec extends IOSpec with JvmInstances with BeforeAndAfterAll { .thenRespondF { _ => Response .ok(arbItemShort.arbitrary.sample.get.asRight) - .pure[IO] + .asRight } .whenRequestMatches { case req if req.method == Method.POST => req.uri.path == Seq("collections") @@ -65,30 +64,34 @@ class StacClientSpec extends IOSpec with JvmInstances with BeforeAndAfterAll { .thenRespondF { _ => Response .ok(arbCollectionShort.arbitrary.sample.get.asRight) - .pure[IO] + .asRight } describe("StacClientSpec") { it("search") { SttpStacClient(backend, uri"http://localhost:9090").search - .map(_.size should be > 0) + .valueOr(throw _) + .size should be > 0 } it("collections") { SttpStacClient(backend, uri"http://localhost:9090").collections - .map(_.size should be > 0) + .valueOr(throw _) + .size should be > 0 } it("items") { SttpStacClient(backend, uri"http://localhost:9090") .items(NonEmptyString.unsafeFrom("collection_id")) - .map(_.size should be > 0) + .valueOr(throw _) + .size should be > 0 } it("item") { SttpStacClient(backend, uri"http://localhost:9090") .item(NonEmptyString.unsafeFrom("collection_id"), NonEmptyString.unsafeFrom("item_id")) - .map(_.size should be > 0) + .valueOr(throw _) + .size should be > 0 } it("itemCreate") { @@ -104,5 +107,5 @@ class StacClientSpec extends IOSpec with JvmInstances with BeforeAndAfterAll { } } - override def afterAll(): Unit = backend.close().unsafeRunSync() + override def afterAll(): Unit = backend.close().valueOr(throw _) } From 30123e3e07cde4f826bb30e4d4fbdfd067d9e58c Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Thu, 24 Dec 2020 19:43:14 -0500 Subject: [PATCH 25/27] Add convenient types --- .../scala/com/azavea/stac4s/api/client/SttpStacClient.scala | 2 +- .../main/scala/com/azavea/stac4s/api/client/package.scala | 5 +++++ .../scala/com/azavea/stac4s/api/client/SttpStacClient.scala | 2 +- .../src/main/scala/com/azavea/stac4s/api/client/client.scala | 5 +++++ 4 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 modules/client/js/src/main/scala/com/azavea/stac4s/api/client/package.scala create mode 100644 modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/client.scala diff --git a/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala index e5e9b3b9..c4dc3abe 100644 --- a/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala +++ b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala @@ -9,6 +9,6 @@ object SttpStacClient { def apply[F[_]: MonadError[*[_], Throwable]]( client: SttpBackend[F, Any], baseUri: Uri - ): BaseSttpStacClient.Aux[F, SearchFilters] = + ): SttpStacClient[F] = BaseSttpStacClient.instance[F, SearchFilters](client, baseUri) } diff --git a/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/package.scala b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/package.scala new file mode 100644 index 00000000..e13eaae9 --- /dev/null +++ b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/package.scala @@ -0,0 +1,5 @@ +package com.azavea.stac4s.api + +package object client { + type SttpStacClient[F[_]] = BaseSttpStacClient.Aux[F, SearchFilters] +} diff --git a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala index e5e9b3b9..c4dc3abe 100644 --- a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala +++ b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala @@ -9,6 +9,6 @@ object SttpStacClient { def apply[F[_]: MonadError[*[_], Throwable]]( client: SttpBackend[F, Any], baseUri: Uri - ): BaseSttpStacClient.Aux[F, SearchFilters] = + ): SttpStacClient[F] = BaseSttpStacClient.instance[F, SearchFilters](client, baseUri) } diff --git a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/client.scala b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/client.scala new file mode 100644 index 00000000..e13eaae9 --- /dev/null +++ b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/client.scala @@ -0,0 +1,5 @@ +package com.azavea.stac4s.api + +package object client { + type SttpStacClient[F[_]] = BaseSttpStacClient.Aux[F, SearchFilters] +} From 6d2ab013b2b7941375476d95509a2250717f9313 Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Thu, 24 Dec 2020 21:07:28 -0500 Subject: [PATCH 26/27] Consolidate tests --- .../api/client/SttpStacClientSpec.scala | 9 ++ .../stac4s/api/client/StacClientSpec.scala | 111 ------------------ .../api/client/SttpStacClientSpec.scala | 9 ++ .../api/client/BaseSttpStacClientSpec.scala} | 27 +++-- 4 files changed, 35 insertions(+), 121 deletions(-) create mode 100644 modules/client/js/src/test/scala/com/azavea/stac4s/api/client/SttpStacClientSpec.scala delete mode 100644 modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala create mode 100644 modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/SttpStacClientSpec.scala rename modules/client/{js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala => shared/src/test/scala/com/azavea/stac4s/api/client/BaseSttpStacClientSpec.scala} (84%) diff --git a/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/SttpStacClientSpec.scala b/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/SttpStacClientSpec.scala new file mode 100644 index 00000000..fb7e65a1 --- /dev/null +++ b/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/SttpStacClientSpec.scala @@ -0,0 +1,9 @@ +package com.azavea.stac4s.api.client + +import com.azavea.stac4s.testing.JsInstances + +import sttp.client3.UriContext + +class SttpStacClientSpec extends BaseSttpStacClientSpec with JsInstances { + lazy val client = SttpStacClient(backend, uri"http://localhost:9090") +} diff --git a/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala b/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala deleted file mode 100644 index 97c11196..00000000 --- a/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala +++ /dev/null @@ -1,111 +0,0 @@ -package com.azavea.stac4s.api.client - -import com.azavea.stac4s.testing.JvmInstances - -import cats.syntax.either._ -import eu.timepit.refined.types.all.NonEmptyString -import io.circe.JsonObject -import io.circe.syntax._ -import org.scalatest.BeforeAndAfterAll -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers -import sttp.client3.testing.SttpBackendStub -import sttp.client3.{Response, UriContext} -import sttp.model.Method -import sttp.monad.EitherMonad - -class StacClientSpec extends AnyFunSpec with Matchers with JvmInstances with BeforeAndAfterAll { - - lazy val backend = - SttpBackendStub(EitherMonad) - .whenRequestMatches(_.uri.path == Seq("search")) - .thenRespondF { _ => - Response - .ok(arbItemCollectionShort.arbitrary.sample.asJson.asRight) - .asRight - } - .whenRequestMatches { - case req if req.method == Method.GET => req.uri.path == Seq("collections") - case _ => false - } - .thenRespondF { _ => - Response - .ok(JsonObject("collections" -> arbCollectionShort.arbitrary.sample.toList.asJson).asJson.asRight) - .asRight - } - .whenRequestMatches { - case req if req.method == Method.GET => req.uri.path == Seq("collections", "collection_id", "items") - case _ => false - } - .thenRespondF { _ => - Response - .ok(arbItemCollectionShort.arbitrary.sample.asJson.asRight) - .asRight - } - .whenRequestMatches(_.uri.path == Seq("collections", "collection_id", "items", "item_id")) - .thenRespondF { _ => - Response - .ok(arbItemShort.arbitrary.sample.asRight) - .asRight - } - .whenRequestMatches { - case req if req.method == Method.POST => req.uri.path == Seq("collections", "collection_id", "items") - case _ => false - } - .thenRespondF { _ => - Response - .ok(arbItemShort.arbitrary.sample.get.asRight) - .asRight - } - .whenRequestMatches { - case req if req.method == Method.POST => req.uri.path == Seq("collections") - case _ => false - } - .thenRespondF { _ => - Response - .ok(arbCollectionShort.arbitrary.sample.get.asRight) - .asRight - } - - describe("StacClientSpec") { - it("search") { - SttpStacClient(backend, uri"http://localhost:9090").search - .valueOr(throw _) - .size should be > 0 - } - - it("collections") { - SttpStacClient(backend, uri"http://localhost:9090").collections - .valueOr(throw _) - .size should be > 0 - } - - it("items") { - SttpStacClient(backend, uri"http://localhost:9090") - .items(NonEmptyString.unsafeFrom("collection_id")) - .valueOr(throw _) - .size should be > 0 - } - - it("item") { - SttpStacClient(backend, uri"http://localhost:9090") - .item(NonEmptyString.unsafeFrom("collection_id"), NonEmptyString.unsafeFrom("item_id")) - .valueOr(throw _) - .size should be > 0 - } - - it("itemCreate") { - SttpStacClient(backend, uri"http://localhost:9090") - .itemCreate(NonEmptyString.unsafeFrom("collection_id"), arbItemShort.arbitrary.sample.get) - .map(_.id should not be empty) - } - - it("collectionCreate") { - SttpStacClient(backend, uri"http://localhost:9090") - .collectionCreate(arbCollectionShort.arbitrary.sample.get) - .map(_.id should not be empty) - } - } - - override def afterAll(): Unit = backend.close().valueOr(throw _) -} diff --git a/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/SttpStacClientSpec.scala b/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/SttpStacClientSpec.scala new file mode 100644 index 00000000..80b7bef6 --- /dev/null +++ b/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/SttpStacClientSpec.scala @@ -0,0 +1,9 @@ +package com.azavea.stac4s.api.client + +import com.azavea.stac4s.testing.JvmInstances + +import sttp.client3.UriContext + +class SttpStacClientSpec extends BaseSttpStacClientSpec with JvmInstances { + lazy val client = SttpStacClient(backend, uri"http://localhost:9090") +} diff --git a/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala b/modules/client/shared/src/test/scala/com/azavea/stac4s/api/client/BaseSttpStacClientSpec.scala similarity index 84% rename from modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala rename to modules/client/shared/src/test/scala/com/azavea/stac4s/api/client/BaseSttpStacClientSpec.scala index b0c1b8b6..0463182a 100644 --- a/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/StacClientSpec.scala +++ b/modules/client/shared/src/test/scala/com/azavea/stac4s/api/client/BaseSttpStacClientSpec.scala @@ -1,20 +1,27 @@ package com.azavea.stac4s.api.client -import com.azavea.stac4s.testing.JsInstances +import com.azavea.stac4s.{ItemCollection, StacCollection, StacItem} import cats.syntax.either._ import eu.timepit.refined.types.all.NonEmptyString import io.circe.JsonObject import io.circe.syntax._ +import org.scalacheck.Arbitrary import org.scalatest.BeforeAndAfterAll import org.scalatest.funspec.AnyFunSpec import org.scalatest.matchers.should.Matchers +import sttp.client3.Response import sttp.client3.testing.SttpBackendStub -import sttp.client3.{Response, UriContext} import sttp.model.Method import sttp.monad.EitherMonad -class StacClientSpec extends AnyFunSpec with Matchers with JsInstances with BeforeAndAfterAll { +trait BaseSttpStacClientSpec extends AnyFunSpec with Matchers with BeforeAndAfterAll { + + def arbCollectionShort: Arbitrary[StacCollection] + def arbItemCollectionShort: Arbitrary[ItemCollection] + def arbItemShort: Arbitrary[StacItem] + + def client: BaseSttpStacClient[Either[Throwable, *]] lazy val backend = SttpBackendStub(EitherMonad) @@ -67,41 +74,41 @@ class StacClientSpec extends AnyFunSpec with Matchers with JsInstances with Befo .asRight } - describe("StacClientSpec") { + describe("SttpStacClientSpec") { it("search") { - SttpStacClient(backend, uri"http://localhost:9090").search + client.search .valueOr(throw _) .size should be > 0 } it("collections") { - SttpStacClient(backend, uri"http://localhost:9090").collections + client.collections .valueOr(throw _) .size should be > 0 } it("items") { - SttpStacClient(backend, uri"http://localhost:9090") + client .items(NonEmptyString.unsafeFrom("collection_id")) .valueOr(throw _) .size should be > 0 } it("item") { - SttpStacClient(backend, uri"http://localhost:9090") + client .item(NonEmptyString.unsafeFrom("collection_id"), NonEmptyString.unsafeFrom("item_id")) .valueOr(throw _) .size should be > 0 } it("itemCreate") { - SttpStacClient(backend, uri"http://localhost:9090") + client .itemCreate(NonEmptyString.unsafeFrom("collection_id"), arbItemShort.arbitrary.sample.get) .map(_.id should not be empty) } it("collectionCreate") { - SttpStacClient(backend, uri"http://localhost:9090") + client .collectionCreate(arbCollectionShort.arbitrary.sample.get) .map(_.id should not be empty) } From c7080098563200f79055c79bb127d2c2b9fd032b Mon Sep 17 00:00:00 2001 From: Grigory Pomadchin Date: Tue, 29 Dec 2020 12:35:14 -0500 Subject: [PATCH 27/27] Rename BasetSttpClient => SttpStacClientF; modify pagination token codecs --- build.sbt | 1 + .../stac4s/api/client/SttpStacClient.scala | 2 +- .../azavea/stac4s/api/client/package.scala | 2 +- .../api/client/SttpStacClientSpec.scala | 2 +- .../stac4s/api/client/SttpStacClient.scala | 2 +- .../com/azavea/stac4s/api/client/client.scala | 2 +- .../api/client/SttpStacClientSpec.scala | 2 +- .../stac4s/api/client/PaginationToken.scala | 32 +++++++++++++++++-- ...StacClient.scala => SttpStacClientF.scala} | 8 ++--- ...ntSpec.scala => SttpStacClientFSpec.scala} | 4 +-- 10 files changed, 43 insertions(+), 14 deletions(-) rename modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/{BaseSttpStacClient.scala => SttpStacClientF.scala} (92%) rename modules/client/shared/src/test/scala/com/azavea/stac4s/api/client/{BaseSttpStacClientSpec.scala => SttpStacClientFSpec.scala} (96%) diff --git a/build.sbt b/build.sbt index 032ba79b..db16134b 100644 --- a/build.sbt +++ b/build.sbt @@ -211,6 +211,7 @@ lazy val client = crossProject(JSPlatform, JVMPlatform) "io.circe" %%% "circe-core" % Versions.Circe, "io.circe" %%% "circe-generic" % Versions.Circe, "io.circe" %%% "circe-refined" % Versions.Circe, + "io.circe" %%% "circe-parser" % Versions.Circe, "com.chuusai" %%% "shapeless" % Versions.Shapeless, "eu.timepit" %%% "refined" % Versions.Refined, "org.typelevel" %%% "cats-core" % Versions.Cats, diff --git a/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala index c4dc3abe..426c79c9 100644 --- a/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala +++ b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala @@ -10,5 +10,5 @@ object SttpStacClient { client: SttpBackend[F, Any], baseUri: Uri ): SttpStacClient[F] = - BaseSttpStacClient.instance[F, SearchFilters](client, baseUri) + SttpStacClientF.instance[F, SearchFilters](client, baseUri) } diff --git a/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/package.scala b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/package.scala index e13eaae9..50cc09a1 100644 --- a/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/package.scala +++ b/modules/client/js/src/main/scala/com/azavea/stac4s/api/client/package.scala @@ -1,5 +1,5 @@ package com.azavea.stac4s.api package object client { - type SttpStacClient[F[_]] = BaseSttpStacClient.Aux[F, SearchFilters] + type SttpStacClient[F[_]] = SttpStacClientF.Aux[F, SearchFilters] } diff --git a/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/SttpStacClientSpec.scala b/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/SttpStacClientSpec.scala index fb7e65a1..a977f902 100644 --- a/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/SttpStacClientSpec.scala +++ b/modules/client/js/src/test/scala/com/azavea/stac4s/api/client/SttpStacClientSpec.scala @@ -4,6 +4,6 @@ import com.azavea.stac4s.testing.JsInstances import sttp.client3.UriContext -class SttpStacClientSpec extends BaseSttpStacClientSpec with JsInstances { +class SttpStacClientSpec extends SttpStacClientFSpec with JsInstances { lazy val client = SttpStacClient(backend, uri"http://localhost:9090") } diff --git a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala index c4dc3abe..426c79c9 100644 --- a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala +++ b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/SttpStacClient.scala @@ -10,5 +10,5 @@ object SttpStacClient { client: SttpBackend[F, Any], baseUri: Uri ): SttpStacClient[F] = - BaseSttpStacClient.instance[F, SearchFilters](client, baseUri) + SttpStacClientF.instance[F, SearchFilters](client, baseUri) } diff --git a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/client.scala b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/client.scala index e13eaae9..50cc09a1 100644 --- a/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/client.scala +++ b/modules/client/jvm/src/main/scala/com/azavea/stac4s/api/client/client.scala @@ -1,5 +1,5 @@ package com.azavea.stac4s.api package object client { - type SttpStacClient[F[_]] = BaseSttpStacClient.Aux[F, SearchFilters] + type SttpStacClient[F[_]] = SttpStacClientF.Aux[F, SearchFilters] } diff --git a/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/SttpStacClientSpec.scala b/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/SttpStacClientSpec.scala index 80b7bef6..0f5648a5 100644 --- a/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/SttpStacClientSpec.scala +++ b/modules/client/jvm/src/test/scala/com/azavea/stac4s/api/client/SttpStacClientSpec.scala @@ -4,6 +4,6 @@ import com.azavea.stac4s.testing.JvmInstances import sttp.client3.UriContext -class SttpStacClientSpec extends BaseSttpStacClientSpec with JvmInstances { +class SttpStacClientSpec extends SttpStacClientFSpec with JvmInstances { lazy val client = SttpStacClient(backend, uri"http://localhost:9090") } diff --git a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/PaginationToken.scala b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/PaginationToken.scala index 714087ea..a91c04f1 100644 --- a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/PaginationToken.scala +++ b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/PaginationToken.scala @@ -1,15 +1,43 @@ package com.azavea.stac4s.api.client +import cats.syntax.either._ import eu.timepit.refined.types.numeric.PosInt +import io.circe import io.circe.generic.semiauto._ +import io.circe.parser.parse import io.circe.refined._ +import io.circe.syntax._ import io.circe.{Decoder, Encoder} import java.time.Instant +import java.util.Base64 final case class PaginationToken(timestampAtLeast: Instant, serialIdGreaterThan: PosInt) +/** Circe codecs should encode token into a base64 string + * https://github.com/azavea/franklin/blob/f5be8ddf48661c5bc43cbd22cb7277e961641803/application/src/main/scala/com/azavea/franklin/api/schemas/package.scala#L84-L85 + */ object PaginationToken { - implicit val dec: Decoder[PaginationToken] = deriveDecoder - implicit val enc: Encoder[PaginationToken] = deriveEncoder + val b64Encoder = Base64.getEncoder + val b64Decoder = Base64.getDecoder + + val defaultDecoder: Decoder[PaginationToken] = deriveDecoder + val defaultEncoder: Encoder[PaginationToken] = deriveEncoder + + def encPaginationToken(token: PaginationToken): String = b64Encoder.encodeToString( + token.asJson(defaultEncoder).noSpaces.getBytes + ) + + def decPaginationToken(encoded: String): Either[circe.Error, PaginationToken] = { + val jsonString = new String(b64Decoder.decode(encoded)) + for { + js <- parse(jsonString) + decoded <- js.as[PaginationToken](defaultDecoder) + } yield decoded + } + + implicit val dec: Decoder[PaginationToken] = + Decoder.decodeString.emap(str => decPaginationToken(str).leftMap(_.getMessage)) + + implicit val enc: Encoder[PaginationToken] = { encPaginationToken(_).asJson } } diff --git a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/BaseSttpStacClient.scala b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/SttpStacClientF.scala similarity index 92% rename from modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/BaseSttpStacClient.scala rename to modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/SttpStacClientF.scala index 60d6ebc9..5bf2421d 100644 --- a/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/BaseSttpStacClient.scala +++ b/modules/client/shared/src/main/scala/com/azavea/stac4s/api/client/SttpStacClientF.scala @@ -13,7 +13,7 @@ import sttp.client3.circe.asJson import sttp.client3.{SttpBackend, basicRequest} import sttp.model.Uri -abstract class BaseSttpStacClient[F[_]: MonadError[*[_], Throwable]]( +abstract class SttpStacClientF[F[_]: MonadError[*[_], Throwable]]( client: SttpBackend[F, Any], baseUri: Uri ) extends StacClient[F] { @@ -92,13 +92,13 @@ abstract class BaseSttpStacClient[F[_]: MonadError[*[_], Throwable]]( .flatMap(MonadError[F, Throwable].fromEither) } -object BaseSttpStacClient { - type Aux[F[_], S] = BaseSttpStacClient[F] { type Filter = S } +object SttpStacClientF { + type Aux[F[_], S] = SttpStacClientF[F] { type Filter = S } def instance[F[_]: MonadError[*[_], Throwable], S]( client: SttpBackend[F, Any], baseUri: Uri - )(implicit sencoder: Encoder[S]): Aux[F, S] = new BaseSttpStacClient[F](client, baseUri) { + )(implicit sencoder: Encoder[S]): Aux[F, S] = new SttpStacClientF[F](client, baseUri) { type Filter = S protected val filterEncoder: Encoder[Filter] = sencoder } diff --git a/modules/client/shared/src/test/scala/com/azavea/stac4s/api/client/BaseSttpStacClientSpec.scala b/modules/client/shared/src/test/scala/com/azavea/stac4s/api/client/SttpStacClientFSpec.scala similarity index 96% rename from modules/client/shared/src/test/scala/com/azavea/stac4s/api/client/BaseSttpStacClientSpec.scala rename to modules/client/shared/src/test/scala/com/azavea/stac4s/api/client/SttpStacClientFSpec.scala index 0463182a..dc95a1e2 100644 --- a/modules/client/shared/src/test/scala/com/azavea/stac4s/api/client/BaseSttpStacClientSpec.scala +++ b/modules/client/shared/src/test/scala/com/azavea/stac4s/api/client/SttpStacClientFSpec.scala @@ -15,13 +15,13 @@ import sttp.client3.testing.SttpBackendStub import sttp.model.Method import sttp.monad.EitherMonad -trait BaseSttpStacClientSpec extends AnyFunSpec with Matchers with BeforeAndAfterAll { +trait SttpStacClientFSpec extends AnyFunSpec with Matchers with BeforeAndAfterAll { def arbCollectionShort: Arbitrary[StacCollection] def arbItemCollectionShort: Arbitrary[ItemCollection] def arbItemShort: Arbitrary[StacItem] - def client: BaseSttpStacClient[Either[Throwable, *]] + def client: SttpStacClientF[Either[Throwable, *]] lazy val backend = SttpBackendStub(EitherMonad)