Skip to content

Commit

Permalink
Migrate schema module
Browse files Browse the repository at this point in the history
  • Loading branch information
Simon Dumas committed Aug 25, 2023
1 parent 90e7eb7 commit 02ca26d
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 126 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package ch.epfl.bluebrain.nexus.delta.routes

import akka.http.scaladsl.model.StatusCode
import akka.http.scaladsl.model.StatusCodes.{Created, OK}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import cats.effect.IO
import cats.implicits._
import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.contexts
import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.schemas.shacl
Expand All @@ -27,6 +29,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.{Schema, SchemaRejection}
import io.circe.{Json, Printer}
import kamon.instrumentation.akka.http.TracingDirectives.operationName
import monix.execution.Scheduler
import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._

/**
* The schemas routes
Expand Down Expand Up @@ -64,6 +67,17 @@ final class SchemasRoutes(
implicit private def resourceFAJsonLdEncoder[A: JsonLdEncoder]: JsonLdEncoder[ResourceF[A]] =
ResourceF.resourceFAJsonLdEncoder(ContextValue(contexts.schemasMetadata))

private def triggerIndexing(io: IO[SchemaResource], indexingMode: IndexingMode) =
io.toBIO[SchemaRejection].tapEval { schema => index(schema.value.project, schema, indexingMode) }.map(_.void)

private def emitIndex(status: StatusCode, io: IO[SchemaResource], indexingMode: IndexingMode): Route =
emit(status, triggerIndexing(io, indexingMode))

private def emitIndex(io: IO[SchemaResource], indexingMode: IndexingMode): Route = emitIndex(OK, io, indexingMode)

private def emitIndexRejectOnNotFound(io: IO[SchemaResource], indexingMode: IndexingMode): Route =
emit(triggerIndexing(io, indexingMode).rejectOn[SchemaNotFound])

def routes: Route =
(baseUriPrefix(baseUri.prefix) & replaceUri("schemas", shacl)) {
pathPrefix("schemas") {
Expand All @@ -74,7 +88,7 @@ final class SchemasRoutes(
(post & pathEndOrSingleSlash & noParameter("rev") & entity(as[Json]) & indexingMode) { (source, mode) =>
operationName(s"$prefixSegment/schemas/{org}/{project}") {
authorizeFor(ref, Write).apply {
emit(Created, schemas.create(ref, source).tapEval(index(ref, _, mode)).map(_.void))
emitIndex(Created, schemas.create(ref, source), mode)
}
}
},
Expand All @@ -89,26 +103,17 @@ final class SchemasRoutes(
(parameter("rev".as[Int].?) & pathEndOrSingleSlash & entity(as[Json])) {
case (None, source) =>
// Create a schema with id segment
emit(
Created,
schemas.create(id, ref, source).tapEval(index(ref, _, mode)).map(_.void)
)
emitIndex(Created, schemas.create(id, ref, source), mode)
case (Some(rev), source) =>
// Update a schema
emit(schemas.update(id, ref, rev, source).tapEval(index(ref, _, mode)).map(_.void))
emitIndex(schemas.update(id, ref, rev, source), mode)
}
}
},
// Deprecate a schema
(delete & parameter("rev".as[Int])) { rev =>
authorizeFor(ref, Write).apply {
emit(
schemas
.deprecate(id, ref, rev)
.tapEval(index(ref, _, mode))
.map(_.void)
.rejectOn[SchemaNotFound]
)
emitIndexRejectOnNotFound(schemas.deprecate(id, ref, rev), mode)
}
},
// Fetch a schema
Expand All @@ -117,7 +122,7 @@ final class SchemasRoutes(
ref,
id,
authorizeFor(ref, Read).apply {
emit(schemas.fetch(id, ref).leftWiden[SchemaRejection].rejectOn[SchemaNotFound])
emit(schemas.fetch(id, ref).toBIO[SchemaRejection].rejectOn[SchemaNotFound])
}
)
}
Expand All @@ -129,7 +134,7 @@ final class SchemasRoutes(
authorizeFor(ref, Write).apply {
emit(
OK,
schemas.refresh(id, ref).tapEval(index(ref, _, mode)).map(_.void)
schemas.refresh(id, ref).toBIO[SchemaRejection].tapEval(index(ref, _, mode)).map(_.void)
)
}
}
Expand All @@ -140,7 +145,7 @@ final class SchemasRoutes(
authorizeFor(ref, Read).apply {
implicit val source: Printer = sourcePrinter
val sourceIO = schemas.fetch(id, ref).map(_.value.source)
emit(sourceIO.leftWiden[SchemaRejection].rejectOn[SchemaNotFound])
emit(sourceIO.toBIO[SchemaRejection].rejectOn[SchemaNotFound])
}
}
},
Expand All @@ -150,15 +155,19 @@ final class SchemasRoutes(
// Fetch a schema tags
(get & idSegmentRef(id) & pathEndOrSingleSlash & authorizeFor(ref, Read)) { id =>
val tagsIO = schemas.fetch(id, ref).map(_.value.tags)
emit(tagsIO.leftWiden[SchemaRejection].rejectOn[SchemaNotFound])
emit(tagsIO.toBIO[SchemaRejection].rejectOn[SchemaNotFound])
},
// Tag a schema
(post & parameter("rev".as[Int]) & pathEndOrSingleSlash) { rev =>
authorizeFor(ref, Write).apply {
entity(as[Tag]) { case Tag(tagRev, tag) =>
emit(
Created,
schemas.tag(id, ref, tag, tagRev, rev).tapEval(index(ref, _, mode)).map(_.void)
schemas
.tag(id, ref, tag, tagRev, rev)
.toBIO[SchemaRejection]
.tapEval(index(ref, _, mode))
.map(_.void)
)
}
}
Expand All @@ -168,13 +177,7 @@ final class SchemasRoutes(
ref,
Write
)) { (tag, rev) =>
emit(
schemas
.deleteTag(id, ref, tag, rev)
.tapEval(index(ref, _, mode))
.map(_.void)
.rejectOn[SchemaNotFound]
)
emitIndexRejectOnNotFound(schemas.deleteTag(id, ref, tag, rev), mode)
}
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package ch.epfl.bluebrain.nexus.delta.wiring

import cats.effect.Clock
import cats.effect.{Clock, IO}
import ch.epfl.bluebrain.nexus.delta.Main.pluginsMaxPriority
import ch.epfl.bluebrain.nexus.delta.config.AppConfig
import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF
Expand All @@ -27,7 +27,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.schemas.{SchemaImports, Schemas, Schema
import ch.epfl.bluebrain.nexus.delta.sdk.sse.SseEncoder
import ch.epfl.bluebrain.nexus.delta.sourcing.Transactors
import izumi.distage.model.definition.{Id, ModuleDef}
import monix.bio.UIO
import monix.execution.Scheduler

/**
Expand All @@ -44,7 +43,7 @@ object SchemasModule extends ModuleDef {
resolverContextResolution: ResolverContextResolution,
config: AppConfig,
xas: Transactors,
clock: Clock[UIO],
clock: Clock[IO],
uuidF: UUIDF
) =>
SchemasImpl(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package ch.epfl.bluebrain.nexus.delta.kernel.utils

import cats.effect.IO
import cats.syntax.all._
import ch.epfl.bluebrain.nexus.delta.kernel.utils.CatsEffectsClasspathResourceUtilsStatic.handleBars
import ch.epfl.bluebrain.nexus.delta.kernel.utils.ClasspathResourceError.{InvalidJson, InvalidJsonObject, ResourcePathNotFound}
import com.github.jknack.handlebars.{EscapingStrategy, Handlebars}
import io.circe.parser.parse
import io.circe.{Json, JsonObject}

import java.io.InputStream
import java.util.Properties
import scala.io.{Codec, Source}
import scala.jdk.CollectionConverters._

trait CatsEffectsClasspathResourceUtils {

final def absolutePath(resourcePath: String)(implicit classLoader: ClassLoader): IO[String] = {
val fromResourceOrClassLoader =
Option(getClass.getResource(resourcePath)) orElse Option(classLoader.getResource(resourcePath))
IO.fromOption(fromResourceOrClassLoader)(ResourcePathNotFound(resourcePath)).map(_.getPath)
}

/**
* Loads the content of the argument classpath resource as an [[InputStream]].
*
* @param resourcePath
* the path of a resource available on the classpath
* @return
* the content of the referenced resource as an [[InputStream]] or a [[ClasspathResourceError]] when the resource
* is not found
*/
def ioStreamOf(resourcePath: String)(implicit classLoader: ClassLoader): IO[InputStream] =
IO.defer {
lazy val fromClass = Option(getClass.getResourceAsStream(resourcePath))
val fromClassLoader = Option(classLoader.getResourceAsStream(resourcePath))
IO.fromOption(fromClass orElse fromClassLoader)(ResourcePathNotFound(resourcePath))
}

/**
* Loads the content of the argument classpath resource as a string and replaces all the key matches of the
* ''replacements'' with their values.
*
* @param resourcePath
* the path of a resource available on the classpath
* @return
* the content of the referenced resource as a string or a [[ClasspathResourceError]] when the resource is not
* found
*/
final def ioContentOf(
resourcePath: String,
attributes: (String, Any)*
)(implicit classLoader: ClassLoader): IO[String] =
resourceAsTextFrom(resourcePath).map {
case text if attributes.isEmpty => text
case text => handleBars.compileInline(text).apply(attributes.toMap.asJava)
}

/**
* Loads the content of the argument classpath resource as a java Properties and transforms it into a Map of key
* property and property value.
*
* @param resourcePath
* the path of a resource available on the classpath
* @return
* the content of the referenced resource as a map of properties or a [[ClasspathResourceError]] when the resource
* is not found
*/
final def ioPropertiesOf(resourcePath: String)(implicit
classLoader: ClassLoader
): IO[Map[String, String]] =
ioStreamOf(resourcePath).map { is =>
val props = new Properties()
props.load(is)
props.asScala.toMap
}

/**
* Loads the content of the argument classpath resource as a string and replaces all the key matches of the
* ''replacements'' with their values. The resulting string is parsed into a json value.
*
* @param resourcePath
* the path of a resource available on the classpath
* @return
* the content of the referenced resource as a json value or an [[ClasspathResourceError]] when the resource is not
* found or is not a Json
*/
final def ioJsonContentOf(
resourcePath: String,
attributes: (String, Any)*
)(implicit classLoader: ClassLoader): IO[Json] =
for {
text <- ioContentOf(resourcePath, attributes: _*)
json <- IO.fromEither(parse(text).leftMap(InvalidJson(resourcePath, text, _)))
} yield json

/**
* Loads the content of the argument classpath resource as a string and replaces all the key matches of the
* ''replacements'' with their values. The resulting string is parsed into a json object.
*
* @param resourcePath
* the path of a resource available on the classpath
* @return
* the content of the referenced resource as a json value or an [[ClasspathResourceError]] when the resource is not
* found or is not a Json
*/
final def ioJsonObjectContentOf(resourcePath: String, attributes: (String, Any)*)(implicit
classLoader: ClassLoader
): IO[JsonObject] =
for {
json <- ioJsonContentOf(resourcePath, attributes: _*)
jsonObj <- IO.fromOption(json.asObject)(InvalidJsonObject(resourcePath))
} yield jsonObj

private def resourceAsTextFrom(resourcePath: String)(implicit
classLoader: ClassLoader
): IO[String] =
ioStreamOf(resourcePath).map(is => Source.fromInputStream(is)(Codec.UTF8).mkString)
}

object CatsEffectsClasspathResourceUtilsStatic {
private[utils] val handleBars = new Handlebars().`with`(EscapingStrategy.NOOP)
}

object CatsEffectsClasspathResourceUtils extends CatsEffectsClasspathResourceUtils
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package ch.epfl.bluebrain.nexus.delta.kernel.utils

import io.circe.ParsingFailure

/**
* Enumeration of possible errors when retrieving resources from the classpath
*/
sealed abstract class ClasspathResourceError(reason: String) extends Exception with Product with Serializable {
override def fillInStackTrace(): ClasspathResourceError = this
override def getMessage: String = reason
override def toString: String = reason
}

object ClasspathResourceError {

/**
* A retrieved resource from the classpath is not a Json
*/
final case class InvalidJson(resourcePath: String, raw: String, failure: ParsingFailure)
extends ClasspathResourceError(
s"The resource path '$resourcePath' could not be converted to Json because of failure: $failure.\nResource content is:\n$raw"
)

/**
* A retrieved resource from the classpath is not a Json object
*/
final case class InvalidJsonObject(resourcePath: String)
extends ClasspathResourceError(s"The resource path '$resourcePath' could not be converted to Json object")

/**
* The resource cannot be found on the classpath
*/
final case class ResourcePathNotFound(resourcePath: String)
extends ClasspathResourceError(s"The resource path '$resourcePath' could not be found")

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import ch.epfl.bluebrain.nexus.delta.kernel.utils.ClassPathResourceUtilsStatic.h
import ch.epfl.bluebrain.nexus.delta.kernel.utils.ClasspathResourceError.{InvalidJson, InvalidJsonObject, ResourcePathNotFound}
import com.github.jknack.handlebars.{EscapingStrategy, Handlebars}
import io.circe.parser.parse
import io.circe.{Json, JsonObject, ParsingFailure}
import io.circe.{Json, JsonObject}
import monix.bio.IO

import java.io.InputStream
Expand Down Expand Up @@ -122,36 +122,3 @@ object ClassPathResourceUtilsStatic {
}

object ClasspathResourceUtils extends ClasspathResourceUtils

/**
* Enumeration of possible errors when retrieving resources from the classpath
*/
sealed abstract class ClasspathResourceError(reason: String) extends Exception with Product with Serializable {
override def fillInStackTrace(): ClasspathResourceError = this
override def getMessage: String = reason
override def toString: String = reason
}

object ClasspathResourceError {

/**
* A retrieved resource from the classpath is not a Json
*/
final case class InvalidJson(resourcePath: String, raw: String, failure: ParsingFailure)
extends ClasspathResourceError(
s"The resource path '$resourcePath' could not be converted to Json because of failure: $failure.\nResource content is:\n$raw"
)

/**
* A retrieved resource from the classpath is not a Json object
*/
final case class InvalidJsonObject(resourcePath: String)
extends ClasspathResourceError(s"The resource path '$resourcePath' could not be converted to Json object")

/**
* The resource cannot be found on the classpath
*/
final case class ResourcePathNotFound(resourcePath: String)
extends ClasspathResourceError(s"The resource path '$resourcePath' could not be found")

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{Resolver, ResolverReje
import ch.epfl.bluebrain.nexus.delta.sdk.resources.Resources
import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.Resource
import ch.epfl.bluebrain.nexus.delta.sdk.schemas.Schemas
import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema
import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.{Schema, SchemaRejection}
import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Identity, ProjectRef, ResourceRef}
import monix.bio.{IO, UIO}
import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._

object ResourceResolution {

Expand Down Expand Up @@ -83,7 +84,8 @@ object ResourceResolution {
apply(
aclCheck,
resolvers,
(ref: ResourceRef, project: ProjectRef) => schemas.fetch(ref, project).redeem(_ => None, Some(_)),
(ref: ResourceRef, project: ProjectRef) =>
schemas.fetch(ref, project).toBIO[SchemaRejection].redeem(_ => None, Some(_)),
Permissions.schemas.read
)

Expand Down
Loading

0 comments on commit 02ca26d

Please sign in to comment.