diff --git a/conversions/shacl2shex/Dockerfile b/conversions/shacl2shex/Dockerfile index fc7c21a..296009a 100644 --- a/conversions/shacl2shex/Dockerfile +++ b/conversions/shacl2shex/Dockerfile @@ -1,7 +1,10 @@ FROM sbtscala/scala-sbt:eclipse-temurin-17.0.4_1.7.1_3.2.0 - + + + RUN git clone --depth 1 --branch v0.2.2 https://github.com/weso/shaclex WORKDIR /root/shaclex +COPY ./patch.scala ./modules/converter/src/main/scala/es/weso/shacl/converter/Shacl2ShEx.scala RUN sbt compile CMD [ "sbt", "run --schema /shape.ttl \ --schemaFormat Turtle \ diff --git a/conversions/shacl2shex/convert.sh b/conversions/shacl2shex/convert.sh index eca51d2..dee63d4 100644 --- a/conversions/shacl2shex/convert.sh +++ b/conversions/shacl2shex/convert.sh @@ -3,7 +3,7 @@ docker create --name shacl2shex shacl2shex for file in ./shapes/*.ttl; do filename=$(basename "$file") filename="${filename%.*}" - docker run -v "$file":/"$filename".ttl shacl2shex > ./shapes/"$filename".shex + docker run -v "$file":/shape.ttl shacl2shex > ./shapes/"$filename".shex sed -i '1,22d;$d' ./shapes/"$filename".shex sed -i 's/^.\{7\}//' ./shapes/"$filename".shex done diff --git a/conversions/shacl2shex/patch.scala b/conversions/shacl2shex/patch.scala new file mode 100644 index 0000000..f9c5190 --- /dev/null +++ b/conversions/shacl2shex/patch.scala @@ -0,0 +1,383 @@ +package es.weso.shacl.converter + +import cats.Id +import cats.data._ +import cats.implicits._ +import com.typesafe.scalalogging.LazyLogging +import es.weso._ +import es.weso.rdf.PREFIXES._ +import es.weso.rdf.PrefixMap +import es.weso.rdf.nodes._ +import es.weso.rdf.path._ +import es.weso.shex.implicits.showShEx._ +import es.weso.shex.linter.ShExLinter + +object Shacl2ShEx extends LazyLogging { + + def shacl2ShEx( + schema: shacl.Schema, + nodesPrefixMap: Option[PrefixMap] = None + ): Either[String, (shex.Schema, shapemaps.QueryShapeMap)] = { + val (state, eitherSchema) = cnvSchema(schema).value.run(initialState) + val e = for { + shexSchema <- eitherSchema + schema1 = shexSchema.addTripleExprMap(state.tripleExprMap) + queryMap <- cnvShapeMap(schema, nodesPrefixMap) + lintedSchema <- ShExLinter.inlineInclusions(schema1) + } yield (lintedSchema, queryMap) + // priprontln(s"Result of conversion: \n$e") + e + } + + def cnvShapeMap( + schema: shacl.Schema, + nodesPrefixMap: Option[PrefixMap] = None + ): Either[String, shapemaps.QueryShapeMap] = + for { + associations <- schema.shapesMap.values.toList.map(shape2Associations).sequence + } yield { + val as: List[shapemaps.Association] = associations.flatten + shapemaps.QueryShapeMap(as, nodesPrefixMap.getOrElse(schema.pm), schema.pm) + } + + private def shape2Associations(shape: shacl.Shape): Either[String, List[shapemaps.Association]] = + shape.targets.toList.map(target2Association(shape)).sequence + + private def rdfTypeShacl: SHACLPath = + SequencePath(List(PredicatePath(`rdf:type`), ZeroOrMorePath(PredicatePath(`rdfs:subClassOf`)))) + + private def shape2ShapeMapLabel(shape: shacl.Shape): Either[String, shapemaps.ShapeMapLabel] = shape.id match { + case bnode: BNode => Right(shapemaps.BNodeLabel(bnode)) + case iri: IRI => Right(shapemaps.IRILabel(iri)) + case _ => Left(s"Cannot convert shape identifier ${shape.id} to shape map label") + } + + private def target2Association(shape: shacl.Shape)(target: shacl.Target): Either[String, shapemaps.Association] = + for { + lbl <- shape2ShapeMapLabel(shape) + nodeSelector <- target2NodeSelector(target) + } yield shapemaps.Association(nodeSelector, lbl, shapemaps.Info.undefined("Generated by Shacl2ShEx converter")) + + private def target2NodeSelector(target: shacl.Target): Either[String, shapemaps.NodeSelector] = target match { + case shacl.TargetClass(node) => + Right(shapemaps.TriplePattern(shapemaps.Focus, rdfTypeShacl, shapemaps.NodePattern(node))) + case shacl.TargetNode(node) => Right(shapemaps.RDFNodeSelector(node)) + case shacl.TargetObjectsOf(pred) => + Right(shapemaps.TriplePattern(shapemaps.WildCard, PredicatePath(pred), shapemaps.Focus)) + case shacl.TargetSubjectsOf(pred) => + Right(shapemaps.TriplePattern(shapemaps.Focus, PredicatePath(pred), shapemaps.WildCard)) + case _ => Left(s"target2NodeSelector: Unsupported conversion of ${target}") + } + + case class State(tripleExprMap: TEMap) { + + def addLabelTripleExpr( + lbl: shex.ShapeLabel, + te: shex.TripleExpr + ): State = { + this.copy(tripleExprMap = tripleExprMap.updated(lbl, te)) + } + + } + + lazy val initialState: State = State(Map()) + + type TEMap = Map[shex.ShapeLabel, shex.TripleExpr] + + type S[A] = StateT[Id, State, A] + + private type Result[A] = EitherT[S, String, A] + + private def ok[A](x: A): Result[A] = + EitherT.pure(x) + + private def err[A](msg: String): Result[A] = { + EitherT.leftT[S, A](msg) + } + + private def modify(fn: State => State): Result[Unit] = + EitherT.liftF(StateT.modify(fn)) + + private def sequence[A](rs: List[Result[A]]): Result[List[A]] = rs.sequence[Result, A] + + private def cnvSchema(schema: shacl.Schema): Result[shex.Schema] = { + sequence(schema.shapes.map(cnvShape(_, schema)).toList).map(m => + shex.Schema.empty.copy( + prefixes = Some(schema.pm), + shapes = Some(m) + ) + ) + } + + private def cnvShape(c: shacl.Shape, schema: shacl.Schema): Result[shex.ShapeExpr] = + c match { + case nc: shacl.NodeShape => cnvNodeShape(nc, schema) + case ps: shacl.PropertyShape => + for { + id <- cnvId(c.id) + te <- cnvPropertyShape(ps) + } yield shex.Shape.empty.copy(expression = Some(te)).addId(id) + case s => err(s"cnvShape: Unimplemented conversion of $s") + } + +// private type ShExId = Option[shex.ShapeLabel] + + private def cnvId(node: RDFNode): Result[shex.ShapeLabel] = + shex.ShapeLabel.fromRDFNode(node).fold(e => err(e), lbl => ok(lbl)) + + /* private def cnvComponents(cs: List[shacl.Component]): Result[List[shex.ShapeExpr]] = + sequence(cs.map(cnvComponent(_))) */ + + private def cnvPropertyShapes(ps: List[shacl.RefNode], schema: shacl.Schema): Result[List[shex.ShapeExpr]] = + sequence(ps.map(cnvPropertyShapeRef(_, schema))) + + private def cnvNodeShape(ns: shacl.NodeShape, schema: shacl.Schema): Result[shex.ShapeExpr] = + for { + id <- cnvId(ns.id) + maybeSe <- cnvComponentsAsShapeExpr(ns.components) + ps <- cnvPropertyShapes(ns.propertyShapes.toList, schema) + se = maybeSe.getOrElse(shex.ShapeExpr.any) + se1 = ps.length match { + case 0 => // TODO: Check when there are more triple exprs... + se + case 1 => { + ps.head // addExpression(se, ps.head) + } + case n if (n > 1) => + // addExpression(se, shex.EachOf(None,ps,None,None,None,None)) + shex.ShapeAnd(None, ps, None, None) + } + se2 <- addId(se1, id) + } yield se2 + + private def addId(se: shex.ShapeExpr, id: shex.ShapeLabel): Result[shex.ShapeExpr] = { + ok(se.addId(id)) + } + + /* private def addExpression(se: shex.ShapeExpr, te: shex.TripleExpr): Result[shex.ShapeExpr] = se match { + case s: shex.Shape => ok(s.copy(expression = Some(te))) + case nc: shex.NodeConstraint => ok(shex.ShapeAnd(None, + List(nc, + shex.Shape.empty.copy(expression=Some(te)))) + ) + }*/ + + private def addLabelTripleExpr(lbl: shex.ShapeLabel, te: shex.TripleExpr): Result[Unit] = + for { + _ <- modify(_.addLabelTripleExpr(lbl, te)) + } yield (()) + + private def cnvPropertyShapeRef(sref: shacl.RefNode, schema: shacl.Schema): Result[shex.ShapeExpr] = + for { + shape <- getShape(sref, schema) + se <- shape match { + case ps: shacl.PropertyShape => + for { + id <- cnvId(ps.id) + s <- cnvPropertyShape(ps) + _ <- addLabelTripleExpr(id, s) + } yield shapeInclusion(id) + case _ => + err[shex.ShapeExpr](s"cnvPropertyShapeRef: reference $sref does not point to a property shape. Shape: $shape") + } + } yield se + + private def cnvPropertyShape(ps: shacl.PropertyShape): Result[shex.TripleExpr] = + for { + predicateInverse <- getPredicateInversePair(ps.path) + se <- cnvComponentsAsShapeExpr(ps.components) + min <- getMinComponent(ps.components) + max <- getMaxComponent(ps.components) + } yield shex.TripleConstraint( + None, + predicateInverse.inverse, + None, + predicateInverse.pred, + se, + min, + max, + None, + None, + None + ) + + case class PredicateInverse(pred: IRI, inverse: Option[Boolean]) + + private def getMinComponent(components: List[shacl.Component]): Result[Option[Int]] = { + ok(option(components.collect { case shacl.MinCount(m) => m }.headOption.getOrElse(0))) + } + + private def getMaxComponent(components: List[shacl.Component]): Result[Option[shex.Max]] = { + ok(option(components.collect { case shacl.MaxCount(m) => shex.IntMax(m) }.headOption.getOrElse(Infinity))) + } + + // TODO: Conversion of components like BlankNodeOrIRI is ignored by now + private def cnvComponentsAsShapeExpr(cs: List[shacl.Component]): Result[Option[shex.ShapeExpr]] = + for { + maybeNodeConstraint <- getNodeConstraint(cs) + } yield maybeNodeConstraint + + private def getNodeConstraint(cs: List[shacl.Component]): Result[Option[shex.NodeConstraint]] = + for { + nk <- getNodeKind(cs) + dt <- getDatatype(cs) + valueSet <- getValueSet(cs) + } yield + if (nk.isDefined || dt.isDefined || valueSet.isDefined) + shex + .NodeConstraint( + id = None, + nodeKind = nk, + datatype = dt, + xsFacets = List(), + values = valueSet, + annotations = None, + actions = None + ) + .some + else + none[shex.NodeConstraint] + + /* private def cnvComponentsAsShapeExpr(cs: List[shacl.Component]): Result[Option[shex.ShapeExpr]] = for { + components <- cnvComponents(cs) + } yield components length match { + case 1 => Some(components.head) + case n if (n > 1) => Some(shex.ShapeAnd(None,components.reverse, None,None)) + case 0 => None + } */ + + private def getNodeKind(cs: List[shacl.Component]): Result[Option[shex.NodeKind]] = { + val nks: List[shex.NodeKind] = cs.flatMap(component2NodeKind) + nks.length match { + case 1 => ok(Some(nks.head)) + case 0 => ok(None) + case _ => err(s"More than one component provides NodeKind constraints: ${nks.map(_.show).mkString(",")}") + } + } + + private def getDatatype(cs: List[shacl.Component]): Result[Option[IRI]] = { + val nks: List[IRI] = cs.flatMap(component2Datatype) + nks.length match { + case 1 => ok(Some(nks.head)) + case 0 => ok(None) + case _ => err(s"More than one component provides Datatype constraints: ${nks.map(_.show).mkString(",")}") + } + } + + private def getValueSet(cs: List[shacl.Component]): Result[Option[List[shex.ValueSetValue]]] = { + val vss: List[List[shex.ValueSetValue]] = cs.flatMap(component2ListValues) + vss.length match { + case 1 => ok(Some(vss.head)) + case 0 => ok(None) + case _ => err(s"More than one component provides ValueSet constraints: ${vss.map(_.show).mkString(",")}") + } + } + + private def component2NodeKind(c: shacl.Component): Option[shex.NodeKind] = c match { + case nk: shacl.NodeKind => + nk.value match { + case shacl.IRIKind => Some(shex.IRIKind) + case shacl.BlankNodeKind => Some(shex.BNodeKind) + case shacl.LiteralKind => Some(shex.LiteralKind) + case _ => + None // TODO: Here we are ignoring the SHACl constraints BlankNodeOrIRI, BlankNodeOrLiteral and IRIOrLiteral + // The conversion of those constraints should be done before... + } + case _ => None + } + + private def component2Datatype(c: shacl.Component): Option[IRI] = c match { + case dt: shacl.Datatype => Some(dt.value) + case _ => None + } + + private def component2ListValues(c: shacl.Component): Option[List[shex.ValueSetValue]] = c match { + case cin: shacl.In => Some(cnvIn(cin)) + case _ => None + } + + def getPredicateInversePair(path: SHACLPath): Result[PredicateInverse] = path match { + case PredicatePath(iri) => ok(PredicateInverse(iri, None)) + case InversePath(PredicatePath(iri)) => ok(PredicateInverse(iri, Some(true))) + case _ => err(s"Not supported complex paths in SHACL->ShEx conversion yet: path: ${path}") + } + /* val rs : Result[List[shex.ShapeExpr]] = sequence(c.components.map(cnvComponent(_))) + def next(se: List[shex.ShapeExpr]): Result[shex.ShapeExpr] = se.size match { + case 1 => { + shex.ShapeLabel + .fromRDFNode(c.id) + .fold( + e => err(e), + lbl => ok { + se.head.addId(lbl) + } + ) + } + case n if (n > 1) => { + shex.ShapeLabel.fromRDFNode(c.id).fold( + e => err(e), + lbl => ok(shex.ShapeAnd(Some(lbl), se)) + ) + } + case v => err(s"cnvShapeNode, size of components $v not supported") + } + rs andThen next */ + + /*private def cnvComponent(c: shacl.Component): Result[shex.ShapeExpr] = { + c match { + case nk: shacl.NodeKind => cnvNodeKind(nk) + case dt: shacl.Datatype => cnvDatatype(dt) + case in: shacl.In => cnvIn(in) + case _ => err(s"cnvComponent: Unimplemented $c") + } + }*/ + +// private def shexIri(): shex.ShapeExpr = shex.NodeConstraint.nodeKind(shex.IRIKind, List()) +// private def shexBNode(): shex.ShapeExpr = shex.NodeConstraint.nodeKind(shex.BNodeKind, List()) +// private def shexLiteral(): shex.ShapeExpr = shex.NodeConstraint.nodeKind(shex.LiteralKind, List()) + private def shapeInclusion(lbl: shex.ShapeLabel): shex.ShapeExpr = + shex.Shape.empty.copy(expression = Some(shex.Inclusion(lbl))) + + /* private def cnvNodeKind(nk: shacl.NodeKind): Result[shex.ShapeExpr] = + nk.value match { + case shacl.IRIKind => ok(shexIri) + case shacl.BlankNodeKind => ok(shexBNode) + case shacl.LiteralKind => ok(shexLiteral) + case shacl.BlankNodeOrIRI => ok(shex.ShapeOr.fromShapeExprs(List(shexBNode,shexIri))) + case shacl.BlankNodeOrLiteral => ok(shex.ShapeOr.fromShapeExprs(List(shexBNode,shexLiteral))) + case shacl.IRIOrLiteral => ok(shex.ShapeOr.fromShapeExprs(List(shexIri,shexLiteral))) + } */ + + /* private def cnvDatatype(dt: shacl.Datatype): Result[shex.ShapeExpr] = { + ok(shex.NodeConstraint.datatype(dt.value,List())) + } */ + + private def cnvIn(dt: shacl.In): List[shex.ValueSetValue] = + dt.list.map(cnvValue) + + private def cnvValue(v: shacl.Value): shex.ValueSetValue = + v match { + case shacl.IRIValue(iri) => shex.IRIValue(iri) + case shacl.LiteralValue(lit) => + lit match { + case StringLiteral(str) => shex.StringValue(str) + case DatatypeLiteral(str, dt) => shex.DatatypeString(str, dt) + case LangLiteral(str, lang) => shex.LangString(str, lang) + case _ => shex.DatatypeString(lit.getLexicalForm, lit.dataType) + } + } + + private def getShape(sref: shacl.RefNode, schema: shacl.Schema): Result[shacl.Shape] = { + fromEither(schema.shape(sref.id)) + } + + private def fromEither[A](e: Either[String, A]): Result[A] = + EitherT.fromEither(e) + + /* private def mkIRILabel(iri: IRI): shex.ShapeLabel = + shex.IRILabel(iri) + + private def mkBNodeLabel(n: Int): shex.ShapeLabel = + shex.BNodeLabel(BNode(n.toString)) + */ +} diff --git a/humanInterfaces/clientServer.ts b/humanInterfaces/clientServer.ts index b548db3..3e8dbef 100644 --- a/humanInterfaces/clientServer.ts +++ b/humanInterfaces/clientServer.ts @@ -5,6 +5,7 @@ import express from 'express'; import rdfHandler from '@rdfjs/express-handler'; import termSet from '@rdfjs/term-set'; import { getSubjects } from "../utils"; +import { ClientInterface } from "./clientInterface"; const shapesToHandle = [ UserShapeShapeType diff --git a/package.json b/package.json index 51e94e6..8934699 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "shacl2shex": "bash conversions/shacl2shex/convert.sh ./shapes", "shc2ttl": "node ./conversions/shaclc2ttl.js", "shc2shex": "npm run shc2ttl && npm run shacl2shex", - "shapes": "npm run shc2shex && npm run ldo && rm -rf ./shapes/*.ttl && rm -rf ./shapes/*.shex" + "shapes": "npm run shc2shex && npm run ldo" }, "author": "", "license": "ISC", diff --git a/shapes/accessRequest.shaclc b/shapes/accessRequest.shaclc new file mode 100644 index 0000000..651d2df --- /dev/null +++ b/shapes/accessRequest.shaclc @@ -0,0 +1,6 @@ +PREFIX ex: + +shape ex:TestShape -> ex:AccessRequest { + # This cap at 1000 + ex:requestedGraphs [0..1] xsd:string . +} diff --git a/shapes/shape.shaclc b/shapes/shape.shaclc deleted file mode 100644 index cfa6590..0000000 --- a/shapes/shape.shaclc +++ /dev/null @@ -1,5 +0,0 @@ -PREFIX ex: - -shape ex:TestShape -> ex:TestClass1 ex:TestClass2 { - targetNode=ex:TestNode targetSubjectsOf=ex:subjectProperty targetObjectsOf=ex:objectProperty . -}