diff --git a/application/src/main/scala/com/azavea/franklin/api/Server.scala b/application/src/main/scala/com/azavea/franklin/api/Server.scala index 70d03ba4f..199aca9d7 100644 --- a/application/src/main/scala/com/azavea/franklin/api/Server.scala +++ b/application/src/main/scala/com/azavea/franklin/api/Server.scala @@ -22,14 +22,16 @@ import io.chrisdavenport.log4cats import io.chrisdavenport.log4cats.SelfAwareStructuredLogger import io.chrisdavenport.log4cats.slf4j.Slf4jLogger import org.http4s._ +import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.dsl.Http4sDsl import org.http4s.implicits._ -import org.http4s.server.blaze._ import org.http4s.server.middleware._ import org.http4s.server.{Router, Server => HTTP4sServer} import sttp.client.asynchttpclient.cats.AsyncHttpClientCatsBackend import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter +import sttp.tapir.docs.openapi.OpenAPIDocsOptions import sttp.tapir.openapi.circe.yaml._ +import sttp.tapir.server.http4s.Http4sServerInterpreter import sttp.tapir.swagger.http4s.SwaggerHttp4s import scala.concurrent.ExecutionContext @@ -70,12 +72,11 @@ $$$$$ $$/ $$/ $$$$$$$/ $$/ $$/ $$/ $$/ $$/ $$/ $$/ $$/ $$$$ """.split("\n").toList - implicit val serverOptions = ServerOptions.defaultServerOptions[IO] - private def createServer( apiConfig: ApiConfig, dbConfig: DatabaseConfig ) = { + val interpreter = Http4sServerInterpreter[IO] val rootLink = StacLink( apiConfig.apiHost, StacLinkType.StacRoot, @@ -115,38 +116,48 @@ $$$$ apiConfig.enableTiles, apiConfig.path ).endpoints ++ landingPage.endpoints - docs = OpenAPIDocsInterpreter.toOpenAPI(allEndpoints, "Franklin", "0.0.1") - docRoutes = new SwaggerHttp4s(docs.toYaml, "open-api", "spec.yaml").routes[IO] + docs = OpenAPIDocsInterpreter().toOpenAPI(allEndpoints, "Franklin", "0.0.1") + docRoutes = new SwaggerHttp4s(docs.toYaml, List("open-api", "spec.yaml")).routes[IO] searchRoutes = new SearchService[IO]( apiConfig, apiConfig.defaultLimit, apiConfig.enableTiles, xa, - rootLink + rootLink, + interpreter ).routes tileRoutes = new TileService[IO]( apiConfig.apiHost, apiConfig.enableTiles, apiConfig.path, - xa + xa, + interpreter ).routes itemExtensions <- Resource.eval { itemExtensionsRef[IO] } collectionExtensions <- Resource.eval { collectionExtensionsRef[IO] } - collectionRoutes = new CollectionsService[IO](xa, apiConfig, collectionExtensions).routes <+> new CollectionItemsService[ + collectionRoutes = new CollectionsService[IO]( + xa, + apiConfig, + collectionExtensions, + interpreter + ).routes <+> new CollectionItemsService[ IO ]( xa, apiConfig, itemExtensions, - rootLink + rootLink, + interpreter ).routes - landingPageRoutes = new LandingPageService[IO](apiConfig).routes - router = CORS( - new AccessLoggingMiddleware( - collectionRoutes <+> searchRoutes <+> tileRoutes <+> landingPageRoutes <+> docRoutes, - logger - ).withLogging(true) - ).orNotFound + landingPageRoutes = new LandingPageService[IO](apiConfig, interpreter).routes + router = CORS.policy + .withAllowOriginAll( + new AccessLoggingMiddleware( + collectionRoutes <+> searchRoutes <+> tileRoutes <+> landingPageRoutes <+> docRoutes, + logger + ).withLogging(true) + ) + .orNotFound serverBuilderBlocker <- Blocker[IO] server <- { BlazeServerBuilder[IO](serverBuilderBlocker.blockingContext) diff --git a/application/src/main/scala/com/azavea/franklin/api/ServerOptions.scala b/application/src/main/scala/com/azavea/franklin/api/ServerOptions.scala deleted file mode 100644 index db70ebbc7..000000000 --- a/application/src/main/scala/com/azavea/franklin/api/ServerOptions.scala +++ /dev/null @@ -1,46 +0,0 @@ -package com.azavea.franklin.api - -import cats.effect.{ContextShift, Sync} -import io.circe.{CursorOp, DecodingFailure} -import sttp.tapir.DecodeResult -import sttp.tapir.server.http4s.Http4sServerOptions -import sttp.tapir.server.{DecodeFailureContext, ServerDefaults} - -object ServerOptions { - - private def handleDecodingErr(err: Throwable): Option[String] = { - err match { - case DecodeResult.Error.JsonDecodeException(_, underlying) => - Some( - underlying.getMessage() - ) - case _ => None - } - } - - private def failureMessage(ctx: DecodeFailureContext): String = { - ctx.failure match { - case DecodeResult.Mismatch(expected, actual) => s"Expected: $expected. Received: $actual" - case DecodeResult.Error(original, err) => handleDecodingErr(err) getOrElse original - case _ => ServerDefaults.FailureMessages.failureMessage(ctx) - } - } - - private val failureHandling = - ServerDefaults.decodeFailureHandler.copy(failureMessage = failureMessage) - - /** Override the default decodeFailureHandler to produce nicer strings. - * - * Other overrideable values allow specifying how to produce the response message - * and how to go from decoding failures in different parts of a request (headers, - * query params, etc.) to a status code. For now, only the - * DecodeFailureContext => String - * has been overridden. - */ - def defaultServerOptions[F[_]: Sync: ContextShift]: Http4sServerOptions[F] = - Http4sServerOptions - .default[F] - .copy( - decodeFailureHandler = failureHandling - ) -} diff --git a/application/src/main/scala/com/azavea/franklin/api/endpoints/TileEndpoints.scala b/application/src/main/scala/com/azavea/franklin/api/endpoints/TileEndpoints.scala index a0a10acea..776372cac 100644 --- a/application/src/main/scala/com/azavea/franklin/api/endpoints/TileEndpoints.scala +++ b/application/src/main/scala/com/azavea/franklin/api/endpoints/TileEndpoints.scala @@ -50,7 +50,7 @@ class TileEndpoints[F[_]: Concurrent](enableTiles: Boolean, pathPrefix: Option[S .and(query[Option[Quantile]]("upperQuantile")) .and(query[Option[Quantile]]("lowerQuantile")) .and(query[Option[NonNegInt]]("singleBand")) - .mapTo(ItemRasterTileRequest) + .mapTo[ItemRasterTileRequest] val collectionRasterTileParameters: EndpointInput[CollectionMosaicRequest] = collectionMosaicTilePath @@ -60,7 +60,7 @@ class TileEndpoints[F[_]: Concurrent](enableTiles: Boolean, pathPrefix: Option[S .and(query[Option[Quantile]]("upperQuantile")) .and(query[Option[Quantile]]("lowerQuantile")) .and(query[Option[NonNegInt]]("singleBand")) - .mapTo(CollectionMosaicRequest) + .mapTo[CollectionMosaicRequest] val itemRasterTileEndpoint : Endpoint[ItemRasterTileRequest, NotFound, Array[Byte], Fs2Streams[F]] = @@ -75,7 +75,7 @@ class TileEndpoints[F[_]: Concurrent](enableTiles: Boolean, pathPrefix: Option[S val collectionFootprintTileEndpoint : Endpoint[MapboxVectorTileFootprintRequest, NotFound, Array[Byte], Fs2Streams[F]] = endpoint.get - .in(collectionFootprintTileParameters.mapTo(MapboxVectorTileFootprintRequest)) + .in(collectionFootprintTileParameters.mapTo[MapboxVectorTileFootprintRequest]) .out(rawBinaryBody[Array[Byte]]) .out(header("content-type", "application/vnd.mapbox-vector-tile")) .errorOut(oneOf(statusMapping(NF, jsonBody[NotFound].description("not found")))) diff --git a/application/src/main/scala/com/azavea/franklin/api/middleware/AccessLoggingMiddleware.scala b/application/src/main/scala/com/azavea/franklin/api/middleware/AccessLoggingMiddleware.scala index 0c45f7c0c..cc6a43966 100644 --- a/application/src/main/scala/com/azavea/franklin/api/middleware/AccessLoggingMiddleware.scala +++ b/application/src/main/scala/com/azavea/franklin/api/middleware/AccessLoggingMiddleware.scala @@ -6,8 +6,8 @@ import cats.effect.Sync import cats.syntax.functor._ import io.chrisdavenport.log4cats.Logger import io.circe.syntax._ -import org.http4s.util.CaseInsensitiveString import org.http4s.{HttpRoutes, Request} +import org.typelevel.ci.CIString import java.time.Instant @@ -21,17 +21,17 @@ class AccessLoggingMiddleware[F[_]: Sync]( else { Kleisli { (request: Request[F]) => val requestStart = Instant.now - val headerWhitelist: Set[CaseInsensitiveString] = + val headerWhitelist: Set[CIString] = Set( - CaseInsensitiveString("user-agent"), - CaseInsensitiveString("accept-encoding"), - CaseInsensitiveString("referer"), - CaseInsensitiveString("origin"), - CaseInsensitiveString("X-Amzn-Trace-Id") + CIString("user-agent"), + CIString("accept-encoding"), + CIString("referer"), + CIString("origin"), + CIString("X-Amzn-Trace-Id") ) val headers = Map( - request.headers.toList.filter(header => headerWhitelist.contains(header.name)) map { + request.headers.headers.filter(header => headerWhitelist.contains(header.name)) map { header => header.name.toString.toLowerCase -> header.value.asJson }: _* ) diff --git a/application/src/main/scala/com/azavea/franklin/api/schemas/package.scala b/application/src/main/scala/com/azavea/franklin/api/schemas/package.scala index cb9ba123d..8668d979e 100644 --- a/application/src/main/scala/com/azavea/franklin/api/schemas/package.scala +++ b/application/src/main/scala/com/azavea/franklin/api/schemas/package.scala @@ -9,28 +9,67 @@ import com.azavea.franklin.datamodel.IfMatchMode import com.azavea.franklin.datamodel.PaginationToken import com.azavea.franklin.error.InvalidPatch import com.azavea.stac4s._ +import com.azavea.stac4s.types._ import eu.timepit.refined.types.string.NonEmptyString import geotrellis.vector.Geometry import io.circe.syntax._ import io.circe.{Encoder, Json} import sttp.tapir.Codec.PlainCodec +import sttp.tapir.codec.enumeratum._ +import sttp.tapir.codec.refined._ import sttp.tapir.generic.auto._ import sttp.tapir.json.circe._ -import sttp.tapir.{Codec, DecodeResult, Schema} +import sttp.tapir.{Codec, DecodeResult, Schema, SchemaType} import scala.util.Try package object schemas { - implicit val schemaStacCollection: Schema[StacCollection] = Schema(schemaForCirceJson.schemaType) + implicit lazy val schemaStacLicense: Schema[StacLicense] = Schema.derived - implicit val schemaForTemporalExtent: Schema[TemporalExtent] = Schema( - schemaForCirceJson.schemaType + implicit lazy val schemaSpatialExtent: Schema[SpatialExtent] = Schema.derived + + implicit lazy val schemaTemporalExtent: Schema[List[TemporalExtent]] = Schema.derived + + implicit lazy val schemaInterval: Schema[Interval] = Schema.derived + + implicit lazy val schemaStacExtent: Schema[StacExtent] = Schema.derived + + implicit lazy val schemaSummaryValue: Schema[SummaryValue] = Schema.derived + + // We can fill in a product schema without any fields as a placeholder, which is + // no worse than the schema for circe json that we used to have + implicit val schemaSummaries: Schema[Map[NonEmptyString, SummaryValue]] = Schema.apply( + SchemaType.SProduct[Map[NonEmptyString, SummaryValue]](Nil), + Some(Schema.SName("summaries")) ) - implicit val schemaForGeometry: Schema[Geometry] = Schema(schemaForCirceJson.schemaType) - implicit val schemaForStacItem: Schema[StacItem] = Schema(schemaForCirceJson.schemaType) - implicit val schemaForInvalidPatch: Schema[InvalidPatch] = Schema(schemaForCirceJson.schemaType) + implicit lazy val schemaForStacLink: Schema[StacLinkType] = Schema.derived + + implicit lazy val schemaStacCollection: Schema[StacCollection] = Schema.derived + + implicit lazy val schemaForTemporalExtent: Schema[TemporalExtent] = Schema.derived + + // We can fill in a product schema without any fields as a placeholder, which is + // no worse than the schema for circe json that we used to have + implicit val schemaForGeometry: Schema[Geometry] = Schema( + SchemaType.SProduct[Geometry](Nil), + Some(Schema.SName("geometry")) + ) + + implicit lazy val schemaForBbox: Schema[TwoDimBbox] = Schema.derived + + // We can fill in a product schema without any fields as a placeholder, which is + // no worse than the schema for circe json that we used to have + implicit val schemaForItemDatetime: Schema[ItemDatetime] = Schema( + SchemaType.SProduct[ItemDatetime](Nil), + Some(Schema.SName("datetime")) + ) + + implicit lazy val schemaForItemProperties: Schema[ItemProperties] = Schema.derived + + implicit lazy val schemaForStacItem: Schema[StacItem] = Schema.derived + implicit lazy val schemaForInvalidPatch: Schema[InvalidPatch] = Schema.derived def decode(s: String): DecodeResult[TemporalExtent] = { temporalExtentFromString(s) match { @@ -89,9 +128,6 @@ package object schemas { implicit val codecPaginationToken: Codec.PlainCodec[PaginationToken] = Codec.string.mapDecode(PaginationToken.decPaginationToken)(PaginationToken.encPaginationToken) - implicit val schemaForStacLink: Schema[StacLinkType] = - Schema.schemaForString.map(s => s.asJson.as[StacLinkType].toOption)(_.repr) - implicit val codecIfMatchMode: Codec.PlainCodec[IfMatchMode] = Codec.string.mapDecode(s => DecodeResult.Value(IfMatchMode.fromString(s)))(_.toString) } diff --git a/application/src/main/scala/com/azavea/franklin/api/services/CollectionItemsService.scala b/application/src/main/scala/com/azavea/franklin/api/services/CollectionItemsService.scala index ae84a52bb..a20f0e4e0 100644 --- a/application/src/main/scala/com/azavea/franklin/api/services/CollectionItemsService.scala +++ b/application/src/main/scala/com/azavea/franklin/api/services/CollectionItemsService.scala @@ -34,8 +34,6 @@ import org.http4s.HttpRoutes import org.http4s.dsl.Http4sDsl import sttp.client.{NothingT, SttpBackend} import sttp.tapir.DecodeResult -import sttp.tapir.server.DecodeFailureContext -import sttp.tapir.server.ServerDefaults import sttp.tapir.server.http4s._ import java.net.URLDecoder @@ -45,12 +43,10 @@ class CollectionItemsService[F[_]: Concurrent]( xa: Transactor[F], apiConfig: ApiConfig, itemExtensionsRef: ExtensionRef[F, StacItem], - rootLink: StacLink + rootLink: StacLink, + interpreter: Http4sServerInterpreter[F] )( - implicit contextShift: ContextShift[F], - timer: Timer[F], - serverOptions: Http4sServerOptions[F], - backend: SttpBackend[F, Nothing, NothingT], + implicit backend: SttpBackend[F, Nothing, NothingT], logger: Logger[F] ) extends Http4sDsl[F] { @@ -345,31 +341,31 @@ class CollectionItemsService[F[_]: Concurrent]( new CollectionItemEndpoints(defaultLimit, enableTransactions, enableTiles, apiConfig.path) val collectionItemTileRoutes = - Http4sServerInterpreter.toRoutes(collectionItemEndpoints.collectionItemTiles)({ + interpreter.toRoutes(collectionItemEndpoints.collectionItemTiles)({ case (collectionId, itemId) => getCollectionItemTileInfo(collectionId, itemId) }) val transactionRoutes: List[HttpRoutes[F]] = List( - Http4sServerInterpreter.toRoutes(collectionItemEndpoints.postItem)({ + interpreter.toRoutes(collectionItemEndpoints.postItem)({ case (collectionId, stacItem) => postItem(collectionId, stacItem) }), - Http4sServerInterpreter.toRoutes(collectionItemEndpoints.putItem)({ + interpreter.toRoutes(collectionItemEndpoints.putItem)({ case (collectionId, itemId, stacItem, etag) => putItem(collectionId, itemId, stacItem, etag) }), - Http4sServerInterpreter.toRoutes(collectionItemEndpoints.deleteItem)({ + interpreter.toRoutes(collectionItemEndpoints.deleteItem)({ case (collectionId, itemId) => deleteItem(collectionId, itemId) }), - Http4sServerInterpreter.toRoutes(collectionItemEndpoints.patchItem)({ + interpreter.toRoutes(collectionItemEndpoints.patchItem)({ case (collectionId, itemId, jsonPatch, etag) => patchItem(collectionId, itemId, jsonPatch, etag) }) ) val routesList: NonEmptyList[HttpRoutes[F]] = NonEmptyList.of( - Http4sServerInterpreter.toRoutes(collectionItemEndpoints.collectionItemsList)({ query => + interpreter.toRoutes(collectionItemEndpoints.collectionItemsList)({ query => Function.tupled(listCollectionItems _)(query) }), - Http4sServerInterpreter.toRoutes(collectionItemEndpoints.collectionItemsUnique)({ + interpreter.toRoutes(collectionItemEndpoints.collectionItemsUnique)({ case (collectionId, itemId) => getCollectionItemUnique(collectionId, itemId) }) ) ++ (if (enableTransactions) { diff --git a/application/src/main/scala/com/azavea/franklin/api/services/CollectionsService.scala b/application/src/main/scala/com/azavea/franklin/api/services/CollectionsService.scala index f0539521c..65f82b8b1 100644 --- a/application/src/main/scala/com/azavea/franklin/api/services/CollectionsService.scala +++ b/application/src/main/scala/com/azavea/franklin/api/services/CollectionsService.scala @@ -34,12 +34,10 @@ import java.util.UUID class CollectionsService[F[_]: Concurrent]( xa: Transactor[F], apiConfig: ApiConfig, - collectionExtensionsRef: ExtensionRef[F, StacCollection] + collectionExtensionsRef: ExtensionRef[F, StacCollection], + interpreter: Http4sServerInterpreter[F] )( - implicit contextShift: ContextShift[F], - timer: Timer[F], - serverOptions: Http4sServerOptions[F], - backend: SttpBackend[F, Nothing, NothingT], + implicit backend: SttpBackend[F, Nothing, NothingT], logger: Logger[F] ) extends Http4sDsl[F] { @@ -229,29 +227,30 @@ class CollectionsService[F[_]: Concurrent]( new CollectionEndpoints[F](enableTransactions, enableTiles, apiConfig.path) val routesList = List( - Http4sServerInterpreter.toRoutes(collectionEndpoints.collectionsList)(_ => listCollections()), - Http4sServerInterpreter.toRoutes(collectionEndpoints.collectionUnique)({ + interpreter.toRoutes(collectionEndpoints.collectionsList)(_ => listCollections()), + interpreter.toRoutes(collectionEndpoints.collectionUnique)({ case collectionId => getCollectionUnique(collectionId) }) ) ++ (if (enableTiles) { List( - Http4sServerInterpreter.toRoutes(collectionEndpoints.collectionTiles)(getCollectionTiles), - Http4sServerInterpreter + interpreter + .toRoutes(collectionEndpoints.collectionTiles)(getCollectionTiles), + interpreter .toRoutes(collectionEndpoints.createMosaic)(Function.tupled(createMosaic)), - Http4sServerInterpreter + interpreter .toRoutes(collectionEndpoints.getMosaic)(Function.tupled(getMosaic)), - Http4sServerInterpreter + interpreter .toRoutes(collectionEndpoints.deleteMosaic)(Function.tupled(deleteMosaic)), - Http4sServerInterpreter.toRoutes(collectionEndpoints.listMosaics)(listMosaics) + interpreter.toRoutes(collectionEndpoints.listMosaics)(listMosaics) ) } else Nil) ++ (if (enableTransactions) { List( - Http4sServerInterpreter.toRoutes(collectionEndpoints.createCollection)(collection => + interpreter.toRoutes(collectionEndpoints.createCollection)(collection => createCollection(collection) ), - Http4sServerInterpreter.toRoutes(collectionEndpoints.deleteCollection)(rawCollectionId => + interpreter.toRoutes(collectionEndpoints.deleteCollection)(rawCollectionId => deleteCollection(rawCollectionId) ) ) diff --git a/application/src/main/scala/com/azavea/franklin/api/services/LandingPageService.scala b/application/src/main/scala/com/azavea/franklin/api/services/LandingPageService.scala index 00d5492dd..739f77fbe 100644 --- a/application/src/main/scala/com/azavea/franklin/api/services/LandingPageService.scala +++ b/application/src/main/scala/com/azavea/franklin/api/services/LandingPageService.scala @@ -21,10 +21,9 @@ import org.http4s._ import org.http4s.dsl.Http4sDsl import sttp.tapir.server.http4s._ -class LandingPageService[F[_]: Concurrent](apiConfig: ApiConfig)( - implicit contextShift: ContextShift[F], - timer: Timer[F], - serverOptions: Http4sServerOptions[F] +class LandingPageService[F[_]: Concurrent]( + apiConfig: ApiConfig, + interpreter: Http4sServerInterpreter[F] ) extends Http4sDsl[F] { val links = List( @@ -101,8 +100,8 @@ class LandingPageService[F[_]: Concurrent](apiConfig: ApiConfig)( val endpoints = new LandingPageEndpoints[F](apiConfig.path) val routesList = List( - Http4sServerInterpreter.toRoutes(endpoints.conformanceEndpoint)(_ => conformancePage), - Http4sServerInterpreter.toRoutes(endpoints.landingPageEndpoint)(_ => landingPage) + interpreter.toRoutes(endpoints.conformanceEndpoint)(_ => conformancePage), + interpreter.toRoutes(endpoints.landingPageEndpoint)(_ => landingPage) ) val routes: HttpRoutes[F] = routesList.foldK diff --git a/application/src/main/scala/com/azavea/franklin/api/services/SearchService.scala b/application/src/main/scala/com/azavea/franklin/api/services/SearchService.scala index 0b30ec92e..faf489e49 100644 --- a/application/src/main/scala/com/azavea/franklin/api/services/SearchService.scala +++ b/application/src/main/scala/com/azavea/franklin/api/services/SearchService.scala @@ -24,11 +24,8 @@ class SearchService[F[_]: Concurrent]( defaultLimit: NonNegInt, enableTiles: Boolean, xa: Transactor[F], - rootLink: StacLink -)( - implicit contextShift: ContextShift[F], - timerF: Timer[F], - serverOptions: Http4sServerOptions[F] + rootLink: StacLink, + interpreter: Http4sServerInterpreter[F] ) extends Http4sDsl[F] { val searchEndpoints = new SearchEndpoints[F](apiConfig.path) @@ -67,9 +64,9 @@ class SearchService[F[_]: Concurrent]( } val routes: HttpRoutes[F] = - Http4sServerInterpreter.toRoutes(searchEndpoints.searchGet)(searchFilters => + interpreter.toRoutes(searchEndpoints.searchGet)(searchFilters => search(searchFilters, SearchMethod.Get) - ) <+> Http4sServerInterpreter.toRoutes(searchEndpoints.searchPost)({ + ) <+> interpreter.toRoutes(searchEndpoints.searchPost)({ case searchFilters => search(searchFilters, SearchMethod.Post) }) } diff --git a/application/src/main/scala/com/azavea/franklin/api/services/TileService.scala b/application/src/main/scala/com/azavea/franklin/api/services/TileService.scala index 7867b3d46..cda578a9a 100644 --- a/application/src/main/scala/com/azavea/franklin/api/services/TileService.scala +++ b/application/src/main/scala/com/azavea/franklin/api/services/TileService.scala @@ -55,7 +55,8 @@ class TileService[F[_]: Async: Concurrent: Parallel: Logger: Timer: ContextShift serverHost: NonEmptyString, enableTiles: Boolean, path: Option[String], - xa: Transactor[F] + xa: Transactor[F], + interpreter: Http4sServerInterpreter[F] ) extends Http4sDsl[F] with RenderImplicits { @@ -251,12 +252,12 @@ class TileService[F[_]: Async: Concurrent: Parallel: Logger: Timer: ContextShift } val routes: HttpRoutes[F] = - Http4sServerInterpreter.toRoutes(tileEndpoints.itemRasterTileEndpoint)(getItemRasterTile) <+> - Http4sServerInterpreter.toRoutes(tileEndpoints.collectionFootprintTileEndpoint)( + interpreter.toRoutes(tileEndpoints.itemRasterTileEndpoint)(getItemRasterTile) <+> + interpreter.toRoutes(tileEndpoints.collectionFootprintTileEndpoint)( getCollectionFootprintTile - ) <+> Http4sServerInterpreter.toRoutes(tileEndpoints.collectionFootprintTileJson)( + ) <+> interpreter.toRoutes(tileEndpoints.collectionFootprintTileJson)( getCollectionFootprintTileJson - ) <+> Http4sServerInterpreter.toRoutes(tileEndpoints.collectionMosaicEndpoint)( + ) <+> interpreter.toRoutes(tileEndpoints.collectionMosaicEndpoint)( getCollectionMosaicTile ) diff --git a/application/src/main/scala/com/azavea/franklin/database/StacItemDao.scala b/application/src/main/scala/com/azavea/franklin/database/StacItemDao.scala index d4ea29575..e251a1e93 100644 --- a/application/src/main/scala/com/azavea/franklin/database/StacItemDao.scala +++ b/application/src/main/scala/com/azavea/franklin/database/StacItemDao.scala @@ -143,11 +143,11 @@ object StacItemDao extends Dao[StacItem] { } private def getTimeData(item: StacItem): (Option[Instant], Option[Instant], Option[Instant]) = { - val timeRangeO = StacItem.timeRangePrism.getOption(item) - val startO = timeRangeO map { _.start } - val endO = timeRangeO map { _.end } - val datetimeO = StacItem.datetimePrism.getOption(item) map { _.when } - (startO, endO, datetimeO) + item.properties.datetime.fold( + { case PointInTime(dt) => (Some(dt), None, None) }, + { case TimeRange(start, end) => (None, Some(start), Some(end)) }, + { case (PointInTime(dt), TimeRange(start, end)) => (Some(dt), Some(start), Some(end)) } + ) } def getItemCount(): ConnectionIO[Int] = { diff --git a/application/src/test/scala/com/azavea/franklin/api/TestClient.scala b/application/src/test/scala/com/azavea/franklin/api/TestClient.scala index 3f3b20217..88e1aa7c0 100644 --- a/application/src/test/scala/com/azavea/franklin/api/TestClient.scala +++ b/application/src/test/scala/com/azavea/franklin/api/TestClient.scala @@ -11,8 +11,8 @@ import io.circe.syntax._ import org.http4s.circe.CirceEntityDecoder._ import org.http4s.circe.CirceEntityEncoder._ import org.http4s.implicits._ -import org.http4s.util.CaseInsensitiveString import org.http4s.{Method, Request, Uri} +import org.typelevel.ci.CIString import java.net.URLEncoder import java.nio.charset.StandardCharsets @@ -59,8 +59,8 @@ class TestClient[F[_]: Sync]( ).withEntity(item.copy(collection = None)) ) item <- resp.as[StacItem] - etag <- resp.headers - .find(h => h.name == CaseInsensitiveString("etag")) + etag <- resp.headers.headers + .find(h => h.name == CIString("etag")) .map(h => h.value.pure[F]) getOrElse { MonadError[F, Throwable].raiseError(new Exception("No etag in response!")) } diff --git a/application/src/test/scala/com/azavea/franklin/api/TestServices.scala b/application/src/test/scala/com/azavea/franklin/api/TestServices.scala index 8d98e0207..7b000c4bd 100644 --- a/application/src/test/scala/com/azavea/franklin/api/TestServices.scala +++ b/application/src/test/scala/com/azavea/franklin/api/TestServices.scala @@ -11,6 +11,7 @@ import doobie.Transactor import eu.timepit.refined.types.numeric.{NonNegInt, PosInt} import io.chrisdavenport.log4cats.noop.NoOpLogger import sttp.client.asynchttpclient.cats.AsyncHttpClientCatsBackend +import sttp.tapir.server.http4s.Http4sServerInterpreter class TestServices[F[_]: Concurrent](xa: Transactor[F])( implicit cs: ContextShift[F], @@ -37,14 +38,17 @@ class TestServices[F[_]: Concurrent](xa: Transactor[F])( false ) + val interpreter = Http4sServerInterpreter[F] + val searchService: SearchService[F] = - new SearchService[F](apiConfig, NonNegInt(30), apiConfig.enableTiles, xa, rootLink) + new SearchService[F](apiConfig, NonNegInt(30), apiConfig.enableTiles, xa, rootLink, interpreter) val collectionsService: F[CollectionsService[F]] = collectionExtensionsRef[F] map { ref => new CollectionsService[F]( xa, apiConfig, - ref + ref, + interpreter ) } @@ -53,7 +57,8 @@ class TestServices[F[_]: Concurrent](xa: Transactor[F])( xa, apiConfig, ref, - rootLink + rootLink, + interpreter ) } diff --git a/application/src/test/scala/com/azavea/franklin/api/services/CollectionItemsServiceSpec.scala b/application/src/test/scala/com/azavea/franklin/api/services/CollectionItemsServiceSpec.scala index 26435acf1..168232bae 100644 --- a/application/src/test/scala/com/azavea/franklin/api/services/CollectionItemsServiceSpec.scala +++ b/application/src/test/scala/com/azavea/franklin/api/services/CollectionItemsServiceSpec.scala @@ -154,8 +154,8 @@ class CollectionItemsServiceSpec val request = Request[IO]( method = Method.PUT, Uri.unsafeFromString(s"/collections/$encodedCollectionId/items/$encodedItemId"), - headers = Headers.of(Header("If-Match", (if (mode == IfMatchMode.YOLO) { "*" } - else { s"$etag" }))) + headers = Headers("If-Match" -> (if (mode == IfMatchMode.YOLO) { "*" } + else { s"$etag" })) ).withEntity(toUpdate) (for { response <- collectionItemsService.routes.run(request) @@ -196,8 +196,8 @@ class CollectionItemsServiceSpec val request = Request[IO]( method = Method.PATCH, Uri.unsafeFromString(s"/collections/$encodedCollectionId/items/$encodedItemId"), - headers = Headers.of(Header("If-Match", (if (mode == IfMatchMode.YOLO) { "*" } - else { s"$etag" }))) + headers = Headers("If-Match" -> (if (mode == IfMatchMode.YOLO) { "*" } + else { s"$etag" })) ).withEntity(patch) (for { diff --git a/application/src/test/scala/com/azavea/franklin/api/services/FiltersFor.scala b/application/src/test/scala/com/azavea/franklin/api/services/FiltersFor.scala index 2ef3fccbd..84b64b70e 100644 --- a/application/src/test/scala/com/azavea/franklin/api/services/FiltersFor.scala +++ b/application/src/test/scala/com/azavea/franklin/api/services/FiltersFor.scala @@ -4,7 +4,14 @@ import cats.data.NonEmptyList import cats.syntax.all._ import cats.{Monoid, Semigroup} import com.azavea.franklin.database.SearchFilters -import com.azavea.stac4s.{ItemDatetime, StacCollection, StacItem, TemporalExtent, TwoDimBbox} +import com.azavea.stac4s.{ + PointInTime, + StacCollection, + StacItem, + TemporalExtent, + TimeRange, + TwoDimBbox +} import geotrellis.vector.Extent import io.circe.syntax._ @@ -59,22 +66,38 @@ object FiltersFor { } def timeFilterFor(item: StacItem): SearchFilters = { - val temporalExtent = item.properties.datetime match { - case ItemDatetime.PointInTime(instant) => - TemporalExtent(instant.minusSeconds(60), Some(instant.plusSeconds(60))) - case ItemDatetime.TimeRange(start, end) => - val milli = start.toEpochMilli % 3 - if (milli == 0) { - // test start and end with full overlap - TemporalExtent(start.minusSeconds(60), Some(end.plusSeconds(60))) - } else if (milli == 1) { - // test start before the range start with open end - TemporalExtent(start.minusSeconds(60), None) - } else { - // test end after the range end with open start - TemporalExtent(None, end.plusSeconds(60)) - } - } + val temporalExtent = item.properties.datetime.fold( + { + case PointInTime(instant) => + TemporalExtent(instant.minusSeconds(60), Some(instant.plusSeconds(60))) + }, { + case TimeRange(start, end) => + val milli = start.toEpochMilli % 3 + if (milli == 0) { + // test start and end with full overlap + TemporalExtent(start.minusSeconds(60), Some(end.plusSeconds(60))) + } else if (milli == 1) { + // test start before the range start with open end + TemporalExtent(start.minusSeconds(60), None) + } else { + // test end after the range end with open start + TemporalExtent(None, end.plusSeconds(60)) + } + }, { + case (_, TimeRange(start, end)) => + val milli = start.toEpochMilli % 3 + if (milli == 0) { + // test start and end with full overlap + TemporalExtent(start.minusSeconds(60), Some(end.plusSeconds(60))) + } else if (milli == 1) { + // test start before the range start with open end + TemporalExtent(start.minusSeconds(60), None) + } else { + // test end after the range end with open start + TemporalExtent(None, end.plusSeconds(60)) + } + } + ) SearchFilters( None, Some(temporalExtent), @@ -139,22 +162,38 @@ object FiltersFor { } def timeFilterExcluding(item: StacItem): SearchFilters = { - val temporalExtent = item.properties.datetime match { - case ItemDatetime.PointInTime(instant) => - TemporalExtent(instant.minusSeconds(60), Some(instant.minusSeconds(30))) - case ItemDatetime.TimeRange(start, end) => - val milli = start.toEpochMilli % 3 - if (milli == 0) { - // test no intersection with range - TemporalExtent(start.minusSeconds(60), Some(start.minusSeconds(30))) - } else if (milli == 1) { - // test start after the range end with open end - TemporalExtent(end.plusSeconds(60), None) - } else { - // test end before the range start with open start - TemporalExtent(None, start.minusSeconds(60)) - } - } + val temporalExtent = item.properties.datetime.fold( + { + case PointInTime(instant) => + TemporalExtent(instant.minusSeconds(60), Some(instant.minusSeconds(30))) + }, { + case TimeRange(start, end) => + val milli = start.toEpochMilli % 3 + if (milli == 0) { + // test no intersection with range + TemporalExtent(start.minusSeconds(60), Some(start.minusSeconds(30))) + } else if (milli == 1) { + // test start after the range end with open end + TemporalExtent(end.plusSeconds(60), None) + } else { + // test end before the range start with open start + TemporalExtent(None, start.minusSeconds(60)) + } + }, { + case (_, TimeRange(start, end)) => + val milli = start.toEpochMilli % 3 + if (milli == 0) { + // test no intersection with range + TemporalExtent(start.minusSeconds(60), Some(start.minusSeconds(30))) + } else if (milli == 1) { + // test start after the range end with open end + TemporalExtent(end.plusSeconds(60), None) + } else { + // test end before the range start with open start + TemporalExtent(None, start.minusSeconds(60)) + } + } + ) SearchFilters( None, Some(temporalExtent), diff --git a/build.sbt b/build.sbt index c2fcf5c01..d3a15c883 100644 --- a/build.sbt +++ b/build.sbt @@ -79,13 +79,14 @@ lazy val applicationSettings = commonSettings ++ Seq( ) lazy val applicationDependencies = Seq( - "software.amazon.awssdk" % "sdk-core" % Versions.AWSSdk2Version, + "co.fs2" %% "fs2-core" % Versions.Fs2Version, "com.amazonaws" % "aws-java-sdk-core" % Versions.AWSVersion, "com.amazonaws" % "aws-java-sdk-s3" % Versions.AWSVersion, - "co.fs2" %% "fs2-core" % Versions.Fs2Version, "com.azavea.stac4s" %% "core" % Versions.Stac4SVersion, "com.azavea.stac4s" %% "testing" % Versions.Stac4SVersion % Test, + "com.beachape" %% "enumeratum" % Versions.Enumeratum, "com.chuusai" %% "shapeless" % Versions.ShapelessVersion, + "com.comcast" %% "ip4s-core" % Versions.Ip4s, "com.github.cb372" %% "scalacache-caffeine" % Versions.ScalacacheVersion, "com.github.cb372" %% "scalacache-cats-effect" % Versions.ScalacacheVersion, "com.github.cb372" %% "scalacache-core" % Versions.ScalacacheVersion, @@ -96,15 +97,16 @@ lazy val applicationDependencies = Seq( "com.monovore" %% "decline-refined" % Versions.DeclineVersion, "com.monovore" %% "decline" % Versions.DeclineVersion, "com.propensive" %% "magnolia" % Versions.MagnoliaVersion, - "com.softwaremill.sttp.client" %% "async-http-client-backend" % Versions.SttpClientVersion, "com.softwaremill.sttp.client" %% "async-http-client-backend-cats" % Versions.SttpClientVersion, + "com.softwaremill.sttp.client" %% "async-http-client-backend" % Versions.SttpClientVersion, "com.softwaremill.sttp.client" %% "circe" % Versions.SttpClientVersion, "com.softwaremill.sttp.client" %% "core" % Versions.SttpClientVersion, "com.softwaremill.sttp.client" %% "json-common" % Versions.SttpClientVersion, + "com.softwaremill.sttp.model" %% "core" % Versions.SttpModelVersion, "com.softwaremill.sttp.shared" %% "core" % Versions.SttpShared, "com.softwaremill.sttp.shared" %% "fs2-ce2" % Versions.SttpShared, - "com.softwaremill.sttp.model" %% "core" % Versions.SttpModelVersion, "com.softwaremill.sttp.tapir" %% "tapir-core" % Versions.TapirVersion, + "com.softwaremill.sttp.tapir" %% "tapir-enumeratum" % Versions.TapirVersion, "com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % Versions.TapirVersion, "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % Versions.TapirVersion, "com.softwaremill.sttp.tapir" %% "tapir-openapi-circe-yaml" % Versions.TapirOpenAPIVersion, @@ -158,11 +160,13 @@ lazy val applicationDependencies = Seq( "org.tpolecat" %% "doobie-scalatest" % Versions.DoobieVersion % Test, "org.tpolecat" %% "doobie-specs2" % Versions.DoobieVersion % Test, "org.tpolecat" %% "typename" % Versions.TypenameVersion, + "org.typelevel" %% "case-insensitive" % Versions.CaseInsensitive, "org.typelevel" %% "cats-core" % Versions.CatsVersion, "org.typelevel" %% "cats-effect" % Versions.CatsEffectVersion, "org.typelevel" %% "cats-free" % Versions.CatsVersion, "org.typelevel" %% "cats-kernel" % Versions.CatsVersion, - "org.typelevel" %% "discipline-scalatest" % Versions.DisciplineScalatest % Test + "org.typelevel" %% "discipline-scalatest" % Versions.DisciplineScalatest % Test, + "software.amazon.awssdk" % "sdk-core" % Versions.AWSSdk2Version ) lazy val application = (project in file("application")) diff --git a/project/Versions.scala b/project/Versions.scala index 2a38454b7..b151ff095 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -13,12 +13,14 @@ object Versions { val DisciplineScalatest = "2.1.5" val DoobieVersion = "0.13.4" val EmojiVersion = "1.2.3" + val Enumeratum = "1.7.0" val Flyway = "7.15.0" val Fs2Version = "2.5.9" val GeoTrellisVersion = "3.6.0" val GuavaVersion = "30.1.1-jre" val HikariVersion = "4.0.3" - val Http4sVersion = "0.21.29" + val Http4sVersion = "0.22.4" + val Ip4s = "2.0.3" val JtsVersion = "1.16.1" val LogbackVersion = "1.2.5" val Log4CatsVersion = "1.1.1" @@ -33,12 +35,12 @@ object Versions { val ShapelessVersion = "2.3.7" val Slf4jVersion = "1.7.32" val Specs2Version = "4.12.12" - val Stac4SVersion = "0.6.2" + val Stac4SVersion = "0.7.1" val SttpClientVersion = "2.2.10" val SttpShared = "1.2.6" val SttpModelVersion = "1.4.11" - val TapirVersion = "0.17.20" - val TapirOpenAPIVersion = "0.17.20" + val TapirVersion = "0.18.3" + val TapirOpenAPIVersion = "0.18.3" val ThreeTenExtra = "1.7.0" val TypenameVersion = "1.0.0" }