Skip to content

Commit

Permalink
Add endpoint for remote contexts + documentation (#4244)
Browse files Browse the repository at this point in the history
* Add endpoint + documentation

---------

Co-authored-by: Simon Dumas <simon.dumas@epfl.ch>
  • Loading branch information
imsdu and Simon Dumas authored Sep 5, 2023
1 parent 8c083a9 commit e97ab4f
Show file tree
Hide file tree
Showing 22 changed files with 407 additions and 242 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -77,22 +77,24 @@ class DeltaModule(appCfg: AppConfig, config: Config)(implicit classLoader: Class

make[RemoteContextResolution].named("aggregate").fromEffect { (otherCtxResolutions: Set[RemoteContextResolution]) =>
for {
errorCtx <- ContextValue.fromFile("contexts/error.json")
metadataCtx <- ContextValue.fromFile("contexts/metadata.json")
searchCtx <- ContextValue.fromFile("contexts/search.json")
pipelineCtx <- ContextValue.fromFile("contexts/pipeline.json")
tagsCtx <- ContextValue.fromFile("contexts/tags.json")
versionCtx <- ContextValue.fromFile("contexts/version.json")
validationCtx <- ContextValue.fromFile("contexts/validation.json")
errorCtx <- ContextValue.fromFile("contexts/error.json")
metadataCtx <- ContextValue.fromFile("contexts/metadata.json")
searchCtx <- ContextValue.fromFile("contexts/search.json")
pipelineCtx <- ContextValue.fromFile("contexts/pipeline.json")
remoteContextsCtx <- ContextValue.fromFile("contexts/remote-contexts.json")
tagsCtx <- ContextValue.fromFile("contexts/tags.json")
versionCtx <- ContextValue.fromFile("contexts/version.json")
validationCtx <- ContextValue.fromFile("contexts/validation.json")
} yield RemoteContextResolution
.fixed(
contexts.error -> errorCtx,
contexts.metadata -> metadataCtx,
contexts.search -> searchCtx,
contexts.pipeline -> pipelineCtx,
contexts.tags -> tagsCtx,
contexts.version -> versionCtx,
contexts.validation -> validationCtx
contexts.error -> errorCtx,
contexts.metadata -> metadataCtx,
contexts.search -> searchCtx,
contexts.pipeline -> pipelineCtx,
contexts.remoteContexts -> remoteContextsCtx,
contexts.tags -> tagsCtx,
contexts.version -> versionCtx,
contexts.validation -> validationCtx
)
.merge(otherCtxResolutions.toSeq: _*)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@ import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.RdfExceptionHandler
import ch.epfl.bluebrain.nexus.delta.sdk.utils.{RouteFixtures, RouteHelpers}
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group}
import ch.epfl.bluebrain.nexus.testkit.{CirceEq, IOValues}
import ch.epfl.bluebrain.nexus.testkit.{CirceEq, IOValues, TestHelpers}
import org.scalatest.matchers.should.Matchers

class IdentitiesRoutesSpec extends RouteHelpers with Matchers with CirceEq with RouteFixtures with IOValues {
class IdentitiesRoutesSpec
extends RouteHelpers
with Matchers
with CirceEq
with RouteFixtures
with IOValues
with TestHelpers {

private val caller = Caller(alice, Set(alice, Anonymous, Authenticated(realm), Group("group", realm)))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,24 +323,19 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap {
}
}

"fail fetching a resource without resources/read permission" in {
"fail fetching a resource information without resources/read permission" in {
val endpoints = List(
"/v1/resources/myorg/myproject/_/myid2",
s"/v1/resources/myorg/myproject/myschema/$myId2Encoded"
"/v1/resources/myorg/myproject/_/myid2?rev=1",
"/v1/resources/myorg/myproject/_/myid2?tag=mytag",
s"/v1/resources/myorg/myproject/myschema/$myId2Encoded",
"/v1/resources/myorg/myproject/_/myid2/source",
"/v1/resources/myorg/myproject/_/myid2/source?annotate=true",
"/v1/resources/myorg/myproject/_/myid2/remote-contexts",
"/v1/resources/myorg/myproject/_/myid2/tags"
)
forAll(endpoints) { endpoint =>
forAll(List("", "?rev=1", "?tag=mytag")) { suffix =>
Get(s"$endpoint$suffix") ~> routes ~> check {
response.status shouldEqual StatusCodes.Forbidden
response.asJson shouldEqual jsonContentOf("errors/authorization-failed.json")
}
}
}
}

"fail fetching a resource original payload without resources/read permission" in {
forAll(List("", "?annotate=true")) { suffix =>
Get(s"/v1/resources/myorg/myproject/_/myid2/source$suffix") ~> routes ~> check {
Get(endpoint) ~> routes ~> check {
response.status shouldEqual StatusCodes.Forbidden
response.asJson shouldEqual jsonContentOf("errors/authorization-failed.json")
}
Expand Down Expand Up @@ -383,14 +378,60 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap {
}
}

"return not found if fetching a resource original payload that does not exist" in {
Get("/v1/resources/myorg/myproject/_/wrongid/source") ~> routes ~> check {
status shouldEqual StatusCodes.NotFound
response.asJson shouldEqual jsonContentOf(
"/resources/errors/not-found.json",
"id" -> "https://bluebrain.github.io/nexus/vocabulary/wrongid",
"proj" -> "myorg/myproject"
)
"fetch a resource remote contexts" in {
val suffix = genString()
val idWithRemoteContext = nxv + suffix
val payload = jsonContentOf("resources/resource.json", "id" -> idWithRemoteContext).deepMerge(resourceCtx)
Post("/v1/resources/myorg/myproject", payload.toEntity) ~> routes ~> check {
status shouldEqual StatusCodes.Created
}

val tag = "mytag"
val tagPayload = json"""{"tag": "$tag", "rev": 1}"""
Post(s"/v1/resources/myorg/myproject/_/$suffix/tags?rev=1", tagPayload.toEntity) ~> routes ~> check {
status shouldEqual StatusCodes.Created
}

val endpoints = List(
s"/v1/resources/myorg/myproject/_/$suffix/remote-contexts",
s"/v1/resources/myorg/myproject/_/$suffix/remote-contexts?rev=1",
s"/v1/resources/myorg/myproject/_/$suffix/remote-contexts?tag=$tag"
)

forAll(endpoints) { endpoint =>
Get(endpoint) ~> routes ~> check {
status shouldEqual StatusCodes.OK
response.asJson shouldEqual
json"""{
"@context" : "https://bluebrain.github.io/nexus/contexts/remote-contexts.json",
"remoteContexts" : [
{ "@type": "StaticContextRef", "iri": "https://bluebrain.github.io/nexus/contexts/metadata.json" }
]
}"""
}
}

}

"return not found when a resource does not exist" in {
val endpoints = List(
"/v1/resources/myorg/myproject/_/wrongid",
"/v1/resources/myorg/myproject/_/wrongid?rev=1",
"/v1/resources/myorg/myproject/_/wrongid?tag=mytag",
"/v1/resources/myorg/myproject/_/wrongid/source",
"/v1/resources/myorg/myproject/_/wrongid/source?annotate=true",
"/v1/resources/myorg/myproject/_/wrongid/remote-contexts",
"/v1/resources/myorg/myproject/_/wrongid/tags"
)
forAll(endpoints) { endpoint =>
Get(endpoint) ~> routes ~> check {
status shouldEqual StatusCodes.NotFound
response.asJson shouldEqual jsonContentOf(
"/resources/errors/not-found.json",
"id" -> "https://bluebrain.github.io/nexus/vocabulary/wrongid",
"proj" -> "myorg/myproject"
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ object Vocabulary {
val quotas = contexts + "quotas.json"
val realms = contexts + "realms.json"
val realmsMetadata = contexts + "realms-metadata.json"
val remoteContexts = contexts + "remote-contexts.json"
val resolvers = contexts + "resolvers.json"
val resolversMetadata = contexts + "resolvers-metadata.json"
val search = contexts + "search.json"
Expand Down
9 changes: 9 additions & 0 deletions delta/sdk/src/main/resources/contexts/remote-contexts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"@context": {
"@vocab": "https://bluebrain.github.io/nexus/vocabulary/",
"remoteContexts": {
"@container": "@set"
}
},
"@id": "https://bluebrain.github.io/nexus/contexts/remote-contexts.json"
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package ch.epfl.bluebrain.nexus.delta.sdk.model.jsonld

import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContext
import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.{BNode, Iri}
import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.contexts
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContext.StaticContext
import ch.epfl.bluebrain.nexus.delta.sdk.model.jsonld.RemoteContextRef.NexusContextRef.ResourceContext
import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution.NexusContext
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, RemoteContext}
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder
import ch.epfl.bluebrain.nexus.delta.sdk.model.jsonld.RemoteContextRef.ProjectRemoteContextRef.ResourceContext
import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution.ProjectRemoteContext
import ch.epfl.bluebrain.nexus.delta.sourcing.Serializer
import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef
import io.circe.Codec
import io.circe.generic.extras.Configuration
import io.circe.generic.extras.semiauto.deriveConfiguredCodec
import io.circe.syntax.EncoderOps
import io.circe.{Codec, Encoder, Json, JsonObject}

import scala.annotation.nowarn

Expand All @@ -28,10 +31,10 @@ object RemoteContextRef {

def apply(remoteContexts: Map[Iri, RemoteContext]): Set[RemoteContextRef] =
remoteContexts.foldLeft(Set.empty[RemoteContextRef]) {
case (acc, (input, _: StaticContext)) => acc + StaticContextRef(input)
case (acc, (input, context: NexusContext)) =>
acc + NexusContextRef(input, ResourceContext(context.iri, context.project, context.rev))
case (_, (_, context)) =>
case (acc, (input, _: StaticContext)) => acc + StaticContextRef(input)
case (acc, (input, context: ProjectRemoteContext)) =>
acc + ProjectRemoteContextRef(input, ResourceContext(context.iri, context.project, context.rev))
case (_, (_, context)) =>
throw new NotImplementedError(s"Case for '${context.getClass.getSimpleName}' has not been implemented.")
}

Expand All @@ -41,15 +44,15 @@ object RemoteContextRef {
final case class StaticContextRef(iri: Iri) extends RemoteContextRef

/**
* A reference to a context registered in Nexus
* @param id
* the identifier it has been resolved to
* A reference to a context registered in a Nexus project
* @param iri
* the resolved iri
* @param resource
* the qualified reference to the revisioned Nexus resource
* the qualified reference to the Nexus resource
*/
final case class NexusContextRef(iri: Iri, resource: ResourceContext) extends RemoteContextRef
final case class ProjectRemoteContextRef(iri: Iri, resource: ResourceContext) extends RemoteContextRef

object NexusContextRef {
object ProjectRemoteContextRef {
final case class ResourceContext(id: Iri, project: ProjectRef, rev: Int)
}

Expand All @@ -59,4 +62,12 @@ object RemoteContextRef {
implicit val resourceContextRefCoder = deriveConfiguredCodec[ResourceContext]
deriveConfiguredCodec[RemoteContextRef]
}

implicit final val remoteContextRefsJsonLdEncoder: JsonLdEncoder[Set[RemoteContextRef]] = {
implicit val remoteContextsEncoder: Encoder.AsObject[Set[RemoteContextRef]] = Encoder.AsObject.instance {
remoteContexts =>
JsonObject("remoteContexts" -> Json.arr(remoteContexts.map(_.asJson).toSeq: _*))
}
JsonLdEncoder.computeFromCirce(id = BNode.random, ctx = ContextValue(contexts.remoteContexts))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, RemoteCon
import ch.epfl.bluebrain.nexus.delta.sdk._
import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck
import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller
import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution.{logger, NexusContext}
import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution.{logger, ProjectRemoteContext}
import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverResolution.ResourceResolution
import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport
import ch.epfl.bluebrain.nexus.delta.sdk.resources.Resources
Expand Down Expand Up @@ -51,7 +51,7 @@ final class ResolverContextResolution(val rcr: RemoteContextResolution, resolveR
s"Resolution via static resolution and via resolvers failed in '$projectRef'",
Some(report.asJson)
),
NexusContext.fromResource
ProjectRemoteContext.fromResource
)
)
.tapEval { context =>
Expand All @@ -70,11 +70,17 @@ object ResolverContextResolution {
/**
* A remote context defined in Nexus as a resource
*/
final case class NexusContext(iri: Iri, project: ProjectRef, rev: Int, value: ContextValue) extends RemoteContext
final case class ProjectRemoteContext(iri: Iri, project: ProjectRef, rev: Int, value: ContextValue)
extends RemoteContext

object NexusContext {
def fromResource(resource: DataResource): NexusContext =
NexusContext(resource.id, resource.value.project, resource.rev, resource.value.source.topContextValueOrEmpty)
object ProjectRemoteContext {
def fromResource(resource: DataResource): ProjectRemoteContext =
ProjectRemoteContext(
resource.id,
resource.value.project,
resource.rev,
resource.value.source.topContextValueOrEmpty
)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,23 @@ trait Resources {
rev: Int
)(implicit caller: Subject): IO[ResourceRejection, DataResource]

/**
* Fetches a resource state.
*
* @param id
* the identifier that will be expanded to the Iri of the resource with its optional rev/tag
* @param projectRef
* the project reference where the resource belongs
* @param schemaOpt
* the optional identifier that will be expanded to the schema reference of the resource. A None value uses the
* currently available resource schema reference.
*/
def fetchState(
id: IdSegmentRef,
projectRef: ProjectRef,
schemaOpt: Option[IdSegment]
): IO[ResourceFetchRejection, ResourceState]

/**
* Fetches a resource.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,11 @@ final class ResourcesImpl private (
res <- eval(DeprecateResource(iri, projectRef, schemeRefOpt, rev, caller))
} yield res).span("deprecateResource")

override def fetch(
def fetchState(
id: IdSegmentRef,
projectRef: ProjectRef,
schemaOpt: Option[IdSegment]
): IO[ResourceFetchRejection, DataResource] = {
): IO[ResourceFetchRejection, ResourceState] = {
for {
pc <- fetchContext.onRead(projectRef)
iri <- expandIri(id.value, pc)
Expand All @@ -175,9 +175,15 @@ final class ResourcesImpl private (
log.stateOr(projectRef, iri, tag, notFound, TagNotFound(tag))
}
_ <- IO.raiseWhen(schemaRefOpt.exists(_.iri != state.schema.iri))(notFound)
} yield state.toResource
} yield state
}.span("fetchResource")

override def fetch(
id: IdSegmentRef,
projectRef: ProjectRef,
schemaOpt: Option[IdSegment]
): IO[ResourceFetchRejection, DataResource] = fetchState(id, projectRef, schemaOpt).map(_.toResource)

private def eval(cmd: ResourceCommand): IO[ResourceRejection, DataResource] =
log.evaluate(cmd.project, cmd.id, cmd).map(_._2.toResource)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"project": "myorg/myproj",
"rev": 5
},
"@type": "NexusContextRef"
"@type": "ProjectRemoteContextRef"
}
],
"rev": 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"project": "myorg/myproj",
"rev": 5
},
"@type": "NexusContextRef"
"@type": "ProjectRemoteContextRef"
}
],
"rev": 2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"project": "myorg/myproj",
"rev": 5
},
"@type": "NexusContextRef"
"@type": "ProjectRemoteContextRef"
}
],
"rev": 2,
Expand Down
2 changes: 1 addition & 1 deletion delta/sdk/src/test/resources/resources/resource-state.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"project": "myorg/myproj",
"rev": 5
},
"@type": "NexusContextRef"
"@type": "ProjectRemoteContextRef"
}
],
"rev": 2,
Expand Down
Loading

0 comments on commit e97ab4f

Please sign in to comment.