Skip to content

Commit

Permalink
Сlient module (stac-utils#140)
Browse files Browse the repository at this point in the history
* Add a client module

* Consolidate tests
  • Loading branch information
pomadchin authored Jan 4, 2021
1 parent e612e91 commit c0bcfa2
Show file tree
Hide file tree
Showing 22 changed files with 874 additions and 119 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ project/plugins/project/
/project/.sbtboot
/project/.boot/
/project/.ivy/
/.sbtopts

# Molecule
.molecule/
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
- С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)

Expand Down
102 changes: 65 additions & 37 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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
)

Expand All @@ -125,57 +125,56 @@ 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"))
.settings(commonSettings)
.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(
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

lazy val testing = (crossProject(JSPlatform, JVMPlatform))
lazy val testing = crossProject(JSPlatform, JVMPlatform)
.in(file("modules/testing"))
.dependsOn(core)
.settings(commonSettings)
.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)
.jsSettings(
libraryDependencies ++= Seq(
"io.github.cquiroz" %%% "scala-java-time" % "2.1.0" % Test
)
)

lazy val testingJVM = testing.jvm
lazy val testingJS = testing.js
Expand All @@ -187,8 +186,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
)
)
Expand All @@ -200,3 +199,32 @@ 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 = crossProject(JSPlatform, JVMPlatform)
.in(file("modules/client"))
.dependsOn(core, testing % Test)
.settings(commonSettings)
.settings(publishSettings)
.settings(
libraryDependencies ++= Seq(
"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,
"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)

lazy val clientJVM = client.jvm
lazy val clientJS = client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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 eu.timepit.refined.types.numeric.NonNegInt
import io.circe._
import io.circe.generic.semiauto._
import io.circe.refined._

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 extends ClientCodecs {

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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.azavea.stac4s.api.client

import cats.MonadError
import sttp.client3.SttpBackend
import sttp.model.Uri

object SttpStacClient {

def apply[F[_]: MonadError[*[_], Throwable]](
client: SttpBackend[F, Any],
baseUri: Uri
): SttpStacClient[F] =
SttpStacClientF.instance[F, SearchFilters](client, baseUri)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.azavea.stac4s.api

package object client {
type SttpStacClient[F[_]] = SttpStacClientF.Aux[F, SearchFilters]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.azavea.stac4s.api.client

import com.azavea.stac4s.testing.JsInstances

import sttp.client3.UriContext

class SttpStacClientSpec extends SttpStacClientFSpec with JsInstances {
lazy val client = SttpStacClient(backend, uri"http://localhost:9090")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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 eu.timepit.refined.types.numeric.NonNegInt
import geotrellis.vector.{io => _, _}
import io.circe._
import io.circe.generic.semiauto._
import io.circe.refined._

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 extends ClientCodecs {

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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.azavea.stac4s.api.client

import cats.MonadError
import sttp.client3.SttpBackend
import sttp.model.Uri

object SttpStacClient {

def apply[F[_]: MonadError[*[_], Throwable]](
client: SttpBackend[F, Any],
baseUri: Uri
): SttpStacClient[F] =
SttpStacClientF.instance[F, SearchFilters](client, baseUri)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.azavea.stac4s.api

package object client {
type SttpStacClient[F[_]] = SttpStacClientF.Aux[F, SearchFilters]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.azavea.stac4s.api.client

import com.azavea.stac4s.testing.JvmInstances

import sttp.client3.UriContext

class SttpStacClientSpec extends SttpStacClientFSpec with JvmInstances {
lazy val client = SttpStacClient(backend, uri"http://localhost:9090")
}
Original file line number Diff line number Diff line change
@@ -0,0 +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 {
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 }
}
Loading

0 comments on commit c0bcfa2

Please sign in to comment.