diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/JsonLdValue.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/JsonLdValue.scala index d1277fa9d3..aacffff40b 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/JsonLdValue.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/JsonLdValue.scala @@ -1,6 +1,11 @@ package ch.epfl.bluebrain.nexus.delta.sdk +import ch.epfl.bluebrain.nexus.delta.rdf.RdfError +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.{JsonLdApi, JsonLdOptions} +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.{CompactedJsonLd, ExpandedJsonLd} +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, RemoteContextResolution} import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder +import monix.bio.IO /** * A definition of a value that can be converted to JSONLD @@ -24,4 +29,18 @@ object JsonLdValue { override val value: A = v override val encoder: JsonLdEncoder[A] = implicitly[JsonLdEncoder[A]] } + + implicit val jsonLdEncoder: JsonLdEncoder[JsonLdValue] = { + new JsonLdEncoder[JsonLdValue] { + override def context(value: JsonLdValue): ContextValue = value.encoder.context(value.value) + override def expand( + value: JsonLdValue + )(implicit opts: JsonLdOptions, api: JsonLdApi, rcr: RemoteContextResolution): IO[RdfError, ExpandedJsonLd] = + value.encoder.expand(value.value) + override def compact( + value: JsonLdValue + )(implicit opts: JsonLdOptions, api: JsonLdApi, rcr: RemoteContextResolution): IO[RdfError, CompactedJsonLd] = + value.encoder.compact(value.value) + } + } } diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/ce/CatsResponseToJsonLd.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/ce/CatsResponseToJsonLd.scala index fcbbde44b2..de2f33fd3d 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/ce/CatsResponseToJsonLd.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/ce/CatsResponseToJsonLd.scala @@ -116,12 +116,9 @@ object CatsResponseToJsonLd extends FileBytesInstances { } onSuccess(flattened.unsafeToFuture()) { - case Left(complete: Complete[E]) => emit(complete) - case Left(reject: Reject[E]) => emit(reject) - case Right(Left(c)) => - implicit val valueEncoder = c.value.encoder - emit(c.value.value) - + case Left(complete: Complete[E]) => emit(complete) + case Left(reject: Reject[E]) => emit(reject) + case Right(Left(c)) => emit(c) case Right(Right((metadata, content))) => headerValueByType(Accept) { accept => if (accept.mediaRanges.exists(_.matches(metadata.contentType.mediaType))) { diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/ResponseToJsonLd.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/ResponseToJsonLd.scala index 6dfde427e9..1b28a3a957 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/ResponseToJsonLd.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/ResponseToJsonLd.scala @@ -106,12 +106,9 @@ object ResponseToJsonLd extends FileBytesInstances { override def apply(statusOverride: Option[StatusCode]): Route = { val flattened = io.flatMap { fr => fr.content.attempt.map(_.map { s => fr.metadata -> s }) }.attempt onSuccess(flattened.runToFuture) { - case Left(complete: Complete[E]) => emit(complete) - case Left(reject: Reject[E]) => emit(reject) - case Right(Left(c)) => - implicit val valueEncoder = c.value.encoder - emit(c.value.value) - + case Left(complete: Complete[E]) => emit(complete) + case Left(reject: Reject[E]) => emit(reject) + case Right(Left(c)) => emit(c) case Right(Right((metadata, content))) => headerValueByType(Accept) { accept => if (accept.mediaRanges.exists(_.matches(metadata.contentType.mediaType))) { diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/error/AuthTokenError.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/error/AuthTokenError.scala index 2f1bd3e3f8..6ef19a4c2e 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/error/AuthTokenError.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/error/AuthTokenError.scala @@ -27,12 +27,6 @@ object AuthTokenError { final case class AuthTokenNotFoundInResponse(failure: DecodingFailure) extends AuthTokenError(s"Auth token not found in auth response: ${failure.reason}") - /** - * Signals that the expiry was missing from the authentication response - */ - final case class ExpiryNotFoundInResponse(failure: DecodingFailure) - extends AuthTokenError(s"Expiry not found in auth response: ${failure.reason}") - /** * Signals that the realm specified for authentication is deprecated */ @@ -45,8 +39,6 @@ object AuthTokenError { JsonObject(keywords.tpe := "AuthTokenHttpError", "reason" := r.reason) case AuthTokenNotFoundInResponse(r) => JsonObject(keywords.tpe -> "AuthTokenNotFoundInResponse".asJson, "reason" := r.message) - case ExpiryNotFoundInResponse(r) => - JsonObject(keywords.tpe -> "ExpiryNotFoundInResponse".asJson, "reason" := r.message) case r: RealmIsDeprecated => JsonObject(keywords.tpe := "RealmIsDeprecated", "reason" := r.getMessage) } diff --git a/delta/sdk/src/test/resources/directives/blank-id.json b/delta/sdk/src/test/resources/directives/blank-id.json new file mode 100644 index 0000000000..66a095e9d3 --- /dev/null +++ b/delta/sdk/src/test/resources/directives/blank-id.json @@ -0,0 +1,5 @@ +{ + "@context" : "https://bluebrain.github.io/nexus/contexts/error.json", + "@type" : "BlankResourceId", + "reason" : "Resource identifier cannot be blank." +} diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/ce/CatsResponseToJsonLdSpec.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/ce/CatsResponseToJsonLdSpec.scala new file mode 100644 index 0000000000..6d2758a72e --- /dev/null +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/ce/CatsResponseToJsonLdSpec.scala @@ -0,0 +1,97 @@ +package ch.epfl.bluebrain.nexus.delta.sdk.ce + +import akka.http.scaladsl.model.ContentTypes.`text/plain(UTF-8)` +import akka.http.scaladsl.model.MediaRanges.`*/*` +import akka.http.scaladsl.model.headers.Accept +import akka.http.scaladsl.model.{ContentType, StatusCodes} +import akka.http.scaladsl.server.RouteConcatenation +import akka.stream.scaladsl.Source +import akka.util.ByteString +import ch.epfl.bluebrain.nexus.delta.rdf.RdfMediaTypes.`application/ld+json` +import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.contexts +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder +import ch.epfl.bluebrain.nexus.delta.rdf.syntax.JsonSyntax +import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering +import ch.epfl.bluebrain.nexus.delta.sdk.ce.DeltaDirectives._ +import ch.epfl.bluebrain.nexus.delta.sdk.directives.FileResponse +import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.HttpResponseFields +import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection +import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection.BlankResourceId +import ch.epfl.bluebrain.nexus.delta.sdk.utils.RouteHelpers +import ch.epfl.bluebrain.nexus.delta.sdk.{AkkaSource, SimpleRejection, SimpleResource} +import ch.epfl.bluebrain.nexus.testkit.ShouldMatchers.convertToAnyShouldWrapper +import ch.epfl.bluebrain.nexus.testkit.TestHelpers.jsonContentOf +import monix.bio.IO +import cats.effect.{IO => CatsIO} +import monix.execution.Scheduler + +class CatsResponseToJsonLdSpec extends RouteHelpers with JsonSyntax with RouteConcatenation { + + implicit val s: Scheduler = Scheduler.global + implicit val rcr: RemoteContextResolution = + RemoteContextResolution.fixed( + SimpleResource.contextIri -> SimpleResource.context, + SimpleRejection.contextIri -> SimpleRejection.context, + contexts.error -> jsonContentOf("/contexts/error.json").topContextValueOrEmpty + ) + implicit val jo: JsonKeyOrdering = JsonKeyOrdering.default() + + private def responseWithSourceError[E: JsonLdEncoder: HttpResponseFields](error: E) = { + responseWith( + `text/plain(UTF-8)`, + IO.raiseError(error) + ) + } + + private val expectedBlankIdErrorResponse = jsonContentOf( + "/directives/blank-id.json" + ) + + private val FileContents = "hello" + + private def fileSourceOfString(value: String) = { + IO.pure(Source.single(ByteString(value))) + } + + private def responseWith[E: JsonLdEncoder: HttpResponseFields]( + contentType: ContentType, + contents: IO[E, AkkaSource] + ) = { + CatsIO.pure( + Right( + FileResponse( + "file.name", + contentType, + 1024, + contents + ) + ) + ) + } + + private def request = { + Get() ~> Accept(`*/*`) + } + + "ResponseToJsonLd file handling" should { + + "Return the contents of a file" in { + request ~> emit( + responseWith(`text/plain(UTF-8)`, fileSourceOfString(FileContents)) + ) ~> check { + status shouldEqual StatusCodes.OK + contentType shouldEqual `text/plain(UTF-8)` + response.asString shouldEqual FileContents + } + } + + "Return an error from a file content IO" in { + request ~> emit(responseWithSourceError[ResourceRejection](BlankResourceId)) ~> check { + status shouldEqual StatusCodes.BadRequest // BlankResourceId is supposed to result in BadRequest + contentType.mediaType shouldEqual `application/ld+json` + response.asJson shouldEqual expectedBlankIdErrorResponse + } + } + } +} diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/ResponseToJsonLdSpec.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/ResponseToJsonLdSpec.scala new file mode 100644 index 0000000000..05c8b40b1a --- /dev/null +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/ResponseToJsonLdSpec.scala @@ -0,0 +1,93 @@ +package ch.epfl.bluebrain.nexus.delta.sdk.directives + +import akka.http.scaladsl.model.ContentTypes.`text/plain(UTF-8)` +import akka.http.scaladsl.model.MediaRanges.`*/*` +import akka.http.scaladsl.model.headers.Accept +import akka.http.scaladsl.model.{ContentType, StatusCodes} +import akka.http.scaladsl.server.RouteConcatenation +import akka.stream.scaladsl.Source +import akka.util.ByteString +import ch.epfl.bluebrain.nexus.delta.rdf.RdfMediaTypes.`application/ld+json` +import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.contexts +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder +import ch.epfl.bluebrain.nexus.delta.rdf.syntax.JsonSyntax +import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering +import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives._ +import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.HttpResponseFields +import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection +import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection.BlankResourceId +import ch.epfl.bluebrain.nexus.delta.sdk.utils.RouteHelpers +import ch.epfl.bluebrain.nexus.delta.sdk.{AkkaSource, SimpleRejection, SimpleResource} +import ch.epfl.bluebrain.nexus.testkit.ShouldMatchers.convertToAnyShouldWrapper +import ch.epfl.bluebrain.nexus.testkit.TestHelpers.jsonContentOf +import monix.bio.IO +import monix.execution.Scheduler + +class ResponseToJsonLdSpec extends RouteHelpers with JsonSyntax with RouteConcatenation { + + implicit val s: Scheduler = Scheduler.global + implicit val rcr: RemoteContextResolution = + RemoteContextResolution.fixed( + SimpleResource.contextIri -> SimpleResource.context, + SimpleRejection.contextIri -> SimpleRejection.context, + contexts.error -> jsonContentOf("/contexts/error.json").topContextValueOrEmpty + ) + implicit val jo: JsonKeyOrdering = JsonKeyOrdering.default() + + private def responseWithSourceError[E: JsonLdEncoder: HttpResponseFields](error: E) = { + responseWith( + `text/plain(UTF-8)`, + IO.raiseError(error) + ) + } + + private val expectedBlankIdErrorResponse = jsonContentOf( + "/directives/blank-id.json" + ) + + private val FileContents = "hello" + + private def fileSourceOfString(value: String) = { + IO.pure(Source.single(ByteString(value))) + } + + private def responseWith[E: JsonLdEncoder: HttpResponseFields]( + contentType: ContentType, + contents: IO[E, AkkaSource] + ) = { + IO.pure( + FileResponse( + "file.name", + contentType, + 1024, + contents + ) + ) + } + + private def request = { + Get() ~> Accept(`*/*`) + } + + "ResponseToJsonLd file handling" should { + + "Return the contents of a file" in { + request ~> emit( + responseWith(`text/plain(UTF-8)`, fileSourceOfString(FileContents)) + ) ~> check { + status shouldEqual StatusCodes.OK + contentType shouldEqual `text/plain(UTF-8)` + response.asString shouldEqual FileContents + } + } + + "Return an error from a file content IO" in { + request ~> emit(responseWithSourceError[ResourceRejection](BlankResourceId)) ~> check { + status shouldEqual StatusCodes.BadRequest // BlankResourceId is supposed to result in BadRequest + contentType.mediaType shouldEqual `application/ld+json` + response.asJson shouldEqual expectedBlankIdErrorResponse + } + } + } +}