From 79e719421958d465887d5b92aab3184afc2e8100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 30 Oct 2024 10:08:40 +0100 Subject: [PATCH] refactor: Start replacing Json-LD parsing with using an RDF model (#3401) --- .../webapi/e2e/v2/ValuesRouteV2E2ESpec.scala | 5 +- .../valuemessages/ValueMessagesV2.scala | 164 +++++++++--------- .../knora/webapi/slice/common/ModelOps.scala | 122 +++++++++++++ .../valuemessages/CreateValueV2Spec.scala | 56 +++--- .../slice/common/JenaModelOpsSpec.scala | 58 +++++++ .../webapi/slice/common/JsonLdTestUtil.scala | 59 +++++++ .../slice/common/KnoraApiValueModelSpec.scala | 74 ++++++++ 7 files changed, 429 insertions(+), 109 deletions(-) create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/common/ModelOps.scala create mode 100644 webapi/src/test/scala/org/knora/webapi/slice/common/JenaModelOpsSpec.scala create mode 100644 webapi/src/test/scala/org/knora/webapi/slice/common/JsonLdTestUtil.scala create mode 100644 webapi/src/test/scala/org/knora/webapi/slice/common/KnoraApiValueModelSpec.scala diff --git a/integration/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala b/integration/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala index b88e55d2b9..e8f7b2b9f6 100644 --- a/integration/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala +++ b/integration/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala @@ -2836,7 +2836,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec { val jsonLDEntity = s"""{ - | "@id" : "$resourceIri", + | "@id" : "$resourceIri", | "@type" : "anything:Thing", | "anything:hasOtherThingValue" : { | "@id" : "$customValueIri", @@ -2862,7 +2862,8 @@ class ValuesRouteV2E2ESpec extends E2ESpec { HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity), ) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) val response: HttpResponse = singleAwaitingRequest(request) - assert(response.status == StatusCodes.OK, response.toString) + val bodyStr = Await.result(Unmarshal(response.entity).to[String], 3.seconds) + assert(response.status == StatusCodes.OK, bodyStr) val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) val valueIri: IRI = responseJsonDoc.body.requireStringWithValidation(JsonLDKeywords.ID, validationFun) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala index 2c6bceed2a..5fe082a04e 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala @@ -4,7 +4,6 @@ */ package org.knora.webapi.messages.v2.responder.valuemessages - import zio.ZIO import java.net.URI @@ -45,6 +44,7 @@ import org.knora.webapi.slice.admin.api.model.Project import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode import org.knora.webapi.slice.admin.domain.model.Permission import org.knora.webapi.slice.admin.domain.model.User +import org.knora.webapi.slice.common.KnoraApiValueModel import org.knora.webapi.slice.resourceinfo.domain.InternalIri import org.knora.webapi.slice.resourceinfo.domain.IriConverter import org.knora.webapi.slice.resources.IiifImageRequestUrl @@ -569,89 +569,89 @@ object CreateValueV2 { ingestState: AssetIngestState, jsonLdString: String, requestingUser: User, - ): ZIO[SipiService & StringFormatter & IriConverter & MessageRelay, Throwable, CreateValueV2] = - ZIO.serviceWithZIO[StringFormatter] { implicit stringFormatter => - for { - // Get the IRI of the resource that the value is to be created in. - jsonLDDocument <- ZIO.attempt(JsonLDUtil.parseJsonLD(jsonLdString)) - resourceIri <- jsonLDDocument.body.getRequiredIdValueAsKnoraDataIri - .mapError(BadRequestException(_)) - .flatMap(RouteUtilZ.ensureIsKnoraResourceIri) - - shortcode <- ZIO - .fromEither(resourceIri.getProjectShortcode) - .mapError(msg => NotFoundException(s"Shortcode not found. $msg")) - - // Get the resource class. - resourceClassIri <- - jsonLDDocument.body.getRequiredTypeAsKnoraApiV2ComplexTypeIri.mapError(BadRequestException(_)) - - // Get the resource property and the value to be created. - createValue <- - jsonLDDocument.body.getRequiredResourcePropertyApiV2ComplexValue.mapError(BadRequestException(_)).flatMap { - case (propertyIri: SmartIri, jsonLdObject: JsonLDObject) => - for { - fileInfo <- ValueContentV2.getFileInfo(shortcode, ingestState, jsonLdObject) - valueContent <- ValueContentV2.fromJsonLdObject(jsonLdObject, requestingUser, fileInfo) - - // Get and validate the custom value IRI if provided. - maybeCustomValueIri <- jsonLdObject.getIdValueAsKnoraDataIri - .mapError(BadRequestException(_)) - .mapAttempt { definedNewIri => - definedNewIri.foreach( - stringFormatter.validateCustomValueIri( - _, - shortcode.value, - resourceIri.getResourceID.get, - ), + ): ZIO[SipiService & StringFormatter & IriConverter & MessageRelay, Throwable, CreateValueV2] = ZIO.scoped { + ZIO.serviceWithZIO[IriConverter] { converter => + ZIO.serviceWithZIO[StringFormatter] { implicit sf => + for { + // Get the IRI of the resource that the value is to be created in. + model <- KnoraApiValueModel.fromJsonLd(jsonLdString, converter).mapError(e => BadRequestException(e.msg)) + shortcode <- ZIO + .fromEither(model.rootResourceIri.getProjectShortcode) + .mapError(msg => NotFoundException(s"Shortcode not found. $msg")) + resourceClassIri <- model.rootResourceClassIri.mapError { + case Some(e) => BadRequestException(e.msg) + case None => BadRequestException("No resource class found") + } + + // Get the resource property and the value to be created. + jsonLDDocument <- ZIO.attempt(JsonLDUtil.parseJsonLD(jsonLdString)) + createValue <- + jsonLDDocument.body.getRequiredResourcePropertyApiV2ComplexValue.mapError(BadRequestException(_)).flatMap { + case (propertyIri: SmartIri, jsonLdObject: JsonLDObject) => + for { + fileInfo <- ValueContentV2.getFileInfo(shortcode, ingestState, jsonLdObject) + valueContent <- ValueContentV2.fromJsonLdObject(jsonLdObject, requestingUser, fileInfo) + + // Get and validate the custom value IRI if provided. + maybeCustomValueIri <- jsonLdObject.getIdValueAsKnoraDataIri + .mapError(BadRequestException(_)) + .mapAttempt { definedNewIri => + definedNewIri.foreach( + sf.validateCustomValueIri( + _, + shortcode.value, + model.rootResourceIri.getResourceID.get, + ), + ) + definedNewIri + } + + // Get the custom value UUID if provided. + maybeCustomUUID <- jsonLdObject.getUuid(ValueHasUUID).mapError(BadRequestException(_)) + + // Get the value's creation date. + // TODO: creationDate for values is a bug, and will not be supported in future. Use valueCreationDate instead. + maybeCreationDate <- ZIO.attempt( + jsonLdObject + .maybeDatatypeValueInObject( + key = ValueCreationDate, + expectedDatatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri, + validationFun = (s, errorFun) => + ValuesValidator.xsdDateTimeStampToInstant(s).getOrElse(errorFun), ) - definedNewIri - } - - // Get the custom value UUID if provided. - maybeCustomUUID <- jsonLdObject.getUuid(ValueHasUUID).mapError(BadRequestException(_)) - - // Get the value's creation date. - // TODO: creationDate for values is a bug, and will not be supported in future. Use valueCreationDate instead. - maybeCreationDate <- ZIO.attempt( - jsonLdObject - .maybeDatatypeValueInObject( - key = ValueCreationDate, - expectedDatatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri, - validationFun = (s, errorFun) => - ValuesValidator.xsdDateTimeStampToInstant(s).getOrElse(errorFun), - ) - .orElse( - jsonLdObject - .maybeDatatypeValueInObject( - key = CreationDate, - expectedDatatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri, - validationFun = (s, errorFun) => - ValuesValidator.xsdDateTimeStampToInstant(s).getOrElse(errorFun), - ), - ), - ) - - maybePermissions <- - ZIO.attempt { - val validationFun: (String, => Nothing) => String = - (s, errorFun) => Iri.toSparqlEncodedString(s).getOrElse(errorFun) - jsonLdObject.maybeStringWithValidation(HasPermissions, validationFun) - } - } yield CreateValueV2( - resourceIri = resourceIri.toString, - resourceClassIri = resourceClassIri, - propertyIri = propertyIri, - valueContent = valueContent, - valueIri = maybeCustomValueIri, - valueUUID = maybeCustomUUID, - valueCreationDate = maybeCreationDate, - permissions = maybePermissions, - ingestState = ingestState, - ) - } - } yield createValue + .orElse( + jsonLdObject + .maybeDatatypeValueInObject( + key = CreationDate, + expectedDatatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri, + validationFun = (s, errorFun) => + ValuesValidator.xsdDateTimeStampToInstant(s).getOrElse(errorFun), + ), + ), + ) + + maybePermissions <- + ZIO.attempt { + val validationFun: (String, => Nothing) => String = + (s, errorFun) => Iri.toSparqlEncodedString(s).getOrElse(errorFun) + jsonLdObject.maybeStringWithValidation(HasPermissions, validationFun) + } + } yield CreateValueV2( + resourceIri = model.rootResourceIri.toString, + resourceClassIri = resourceClassIri, + propertyIri = propertyIri, + valueContent = valueContent, + valueIri = maybeCustomValueIri, + valueUUID = maybeCustomUUID, + valueCreationDate = maybeCreationDate, + permissions = maybePermissions, + ingestState = ingestState, + ) + } + } yield createValue + } } + } } /** A trait for classes representing information to be updated in a value. */ diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/ModelOps.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/ModelOps.scala new file mode 100644 index 0000000000..394770f659 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/ModelOps.scala @@ -0,0 +1,122 @@ +/* + * Copyright © 2021 - 2024 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.common + +import org.apache.jena.rdf.model.* +import org.apache.jena.riot.Lang +import org.apache.jena.riot.RDFDataMgr +import org.apache.jena.vocabulary.RDF +import zio.* + +import java.io.ByteArrayInputStream +import java.nio.charset.StandardCharsets +import scala.util.Try + +import org.knora.webapi.ApiV2Complex +import org.knora.webapi.messages.SmartIri +import org.knora.webapi.slice.common.ModelError.IsNoResourceIri +import org.knora.webapi.slice.common.ModelError.MoreThanOneRootResource +import org.knora.webapi.slice.common.ModelError.ParseError +import org.knora.webapi.slice.resourceinfo.domain.IriConverter + +enum ModelError(val msg: String) { + case ParseError(override val msg: String) extends ModelError(msg) + case IsNoResourceIri(override val msg: String, iri: String) extends ModelError(msg) + case InvalidResourceClassIri(override val msg: String, iri: String) extends ModelError(msg) + case MoreThanOneRootResource(override val msg: String) extends ModelError(msg) + case NoRootResource(override val msg: String) extends ModelError(msg) +} +object ModelError { + def parseError(ex: Throwable): ParseError = ParseError(ex.getMessage) + def noResourceIri(iri: SmartIri): IsNoResourceIri = + IsNoResourceIri(s"This is not a resource IRI $iri", iri.toOntologySchema(ApiV2Complex).toIri) + def moreThanOneRootResource: MoreThanOneRootResource = MoreThanOneRootResource("More than one root resource found") + def noRootResource: NoRootResource = NoRootResource("No root resource found") + def invalidResourceClassIri(iri: SmartIri): InvalidResourceClassIri = + InvalidResourceClassIri("Invalid resource class IRI", iri.toIri) +} + +/* + * The KnoraApiModel represents any incoming value models from our v2 API. + */ +final case class KnoraApiValueModel(model: Model, rootResourceIri: SmartIri, convert: IriConverter) { self => + import ResourceOps.* + import StatementOps.* + + def rootResource: Resource = model.getResource(rootResourceIri.toString) + + def rootResourceClassIri: IO[Option[ModelError], SmartIri] = ZIO + .fromOption(rootResource.rdfsType()) + .flatMap(convert.asSmartIri(_).mapError(ModelError.parseError).asSomeError) + .filterOrElseWith(iri => iri.isKnoraEntityIri && iri.isApiV2ComplexSchema)(iri => + ZIO.fail(ModelError.invalidResourceClassIri(iri)).asSomeError, + ) +} + +object KnoraApiValueModel { self => + import StatementOps.* + + // available for ease of use in tests + def fromJsonLd(str: String): ZIO[Scope & IriConverter, ModelError, KnoraApiValueModel] = + ZIO.service[IriConverter].flatMap(self.fromJsonLd(str, _)) + + def fromJsonLd(str: String, converter: IriConverter): ZIO[Scope & IriConverter, ModelError, KnoraApiValueModel] = + for { + model <- ModelOps.fromJsonLd(str) + root <- getRootResourceIri(model, converter) + } yield KnoraApiValueModel(model, root, converter) + + private def getRootResourceIri(model: Model, convert: IriConverter): IO[ModelError, SmartIri] = + val iter = model.listStatements() + var objSeen = Set.empty[String] + var subSeen = Set.empty[String] + while (iter.hasNext) { + val stmt = iter.nextStatement() + val _ = stmt.objectUri().foreach(iri => objSeen += iri) + val _ = stmt.subjectUri().foreach(iri => subSeen += iri) + } + val result: IO[ModelError, SmartIri] = (subSeen -- objSeen) match { + case result if result.size == 1 => + convert + .asSmartIri(result.head) + .mapError(ModelError.parseError) + .filterOrElseWith(_.isKnoraResourceIri)(iri => ZIO.fail(ModelError.noResourceIri(iri))) + case result if result.isEmpty => ZIO.fail(ModelError.noRootResource) + case _ => ZIO.fail(ModelError.moreThanOneRootResource) + } + result +} + +object ResourceOps { + extension (res: Resource) { + def property(p: Property): Option[Statement] = Option(res.getProperty(p)) + def rdfsType(): Option[String] = Option(res.getPropertyResourceValue(RDF.`type`)).flatMap(_.uri) + def uri: Option[String] = Option(res.getURI) + } +} + +object StatementOps { + extension (stmt: Statement) { + def subjectUri(): Option[String] = Option(stmt.getSubject.getURI) + def objectUri(): Option[String] = Try(stmt.getObject.asResource()).toOption.flatMap(r => Option(r.getURI)) + } +} + +object ModelOps { self => + + def fromJsonLd(str: String): ZIO[Scope, ParseError, Model] = from(str, Lang.JSONLD) + + private val createModel = + ZIO.acquireRelease(ZIO.succeed(ModelFactory.createDefaultModel()))(m => ZIO.succeed(m.close())) + + def from(str: String, lang: Lang): ZIO[Scope, ParseError, Model] = + for { + m <- createModel + _ <- ZIO + .attempt(RDFDataMgr.read(m, ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)), lang)) + .mapError(ModelError.parseError) + } yield m +} diff --git a/webapi/src/test/scala/org/knora/webapi/messages/v2/responder/valuemessages/CreateValueV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/messages/v2/responder/valuemessages/CreateValueV2Spec.scala index 99b0149775..5408b81a40 100644 --- a/webapi/src/test/scala/org/knora/webapi/messages/v2/responder/valuemessages/CreateValueV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/messages/v2/responder/valuemessages/CreateValueV2Spec.scala @@ -6,10 +6,12 @@ package org.knora.webapi.messages.v2.responder.valuemessages import zio.ZIO +import zio.test.Gen import zio.test.Spec import zio.test.TestResult import zio.test.ZIOSpecDefault import zio.test.assertTrue +import zio.test.check import org.knora.webapi.ApiV2Complex import org.knora.webapi.IRI @@ -25,6 +27,8 @@ import org.knora.webapi.slice.admin.domain.model.Group import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.admin.domain.service.KnoraGroupRepo import org.knora.webapi.slice.admin.domain.service.KnoraProjectRepo +import org.knora.webapi.slice.common.JsonLdTestUtil +import org.knora.webapi.slice.common.JsonLdTestUtil.JsonLdTransformations import org.knora.webapi.slice.resourceinfo.domain.IriConverter import org.knora.webapi.store.iiif.api.SipiService import org.knora.webapi.store.iiif.impl.SipiServiceMock @@ -65,32 +69,34 @@ object CreateValueV2Spec extends ZIOSpecDefault { override def spec: Spec[Any, Throwable] = suite("CreateValueV2Spec")(test("UnformattedText TextValue fromJsonLd should contain the language") { - for { - sf <- ZIO.service[StringFormatter] - value <- CreateValueV2.fromJsonLd(AssetIngested, unformattedTextValueWithLanguage, rootUser) - } yield assertTrue( - value == CreateValueV2( - resourceIri = "http://rdfh.ch/0001/a-thing", - resourceClassIri = sf.toSmartIri("http://0.0.0.0:3333/ontology/0001/anything/v2#Thing"), - propertyIri = sf.toSmartIri("http://0.0.0.0:3333/ontology/0001/anything/v2#hasText"), - valueContent = TextValueContentV2( - ontologySchema = ApiV2Complex, - maybeValueHasString = Some("This is English"), - textValueType = UnformattedText, - valueHasLanguage = Some("en"), - standoff = Nil, - mappingIri = None, - mapping = None, - xslt = None, - comment = None, + check(Gen.fromIterable(Seq(JsonLdTransformations.noOp))) { f => + for { + sf <- ZIO.service[StringFormatter] + value <- CreateValueV2.fromJsonLd(AssetIngested, f(unformattedTextValueWithLanguage), rootUser) + } yield assertTrue( + value == CreateValueV2( + resourceIri = "http://rdfh.ch/0001/a-thing", + resourceClassIri = sf.toSmartIri("http://0.0.0.0:3333/ontology/0001/anything/v2#Thing"), + propertyIri = sf.toSmartIri("http://0.0.0.0:3333/ontology/0001/anything/v2#hasText"), + valueContent = TextValueContentV2( + ontologySchema = ApiV2Complex, + maybeValueHasString = Some("This is English"), + textValueType = UnformattedText, + valueHasLanguage = Some("en"), + standoff = Nil, + mappingIri = None, + mapping = None, + xslt = None, + comment = None, + ), + valueIri = None, + valueUUID = None, + valueCreationDate = None, + permissions = None, + ingestState = AssetIngested, ), - valueIri = None, - valueUUID = None, - valueCreationDate = None, - permissions = None, - ingestState = AssetIngested, - ), - ) + ) + } }).provide(StringFormatter.test, MessageRelayLive.layer, IriConverter.layer, SipiServiceMock.layer) } diff --git a/webapi/src/test/scala/org/knora/webapi/slice/common/JenaModelOpsSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/common/JenaModelOpsSpec.scala new file mode 100644 index 0000000000..ae25438764 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/slice/common/JenaModelOpsSpec.scala @@ -0,0 +1,58 @@ +/* + * Copyright © 2021 - 2024 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.common + +import zio.* +import zio.test.* + +object JenaModelOpsSpec extends ZIOSpecDefault { + + private val jsonLd = """{ + "@id" : "http://rdfh.ch/0001/a-thing", + "@type" : "anything:Thing", + "anything:hasInteger" : { + "@type" : "knora-api:IntValue", + "knora-api:intValueAsInt" : 4 + }, + "@context" : { + "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", + "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#" + } + } + """.stripMargin + private val jsonLd2 = """{ + "@id" : "http://rdfh.ch/0001/a-thing", + "@type" : "anything:Thing", + "anything:hasInteger" : { + "@type" : "knora-api:IntValue", + "knora-api:intValueAsInt" : { + "@type" : "xsd", + "@value" : "4" + } + }, + "@context" : { + "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", + "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#", + "xsd" : "http://www.w3.org/2001/XMLSchema#integer" + } + } + """.stripMargin + val spec = suite("JenaModelOps")( + suite("fromJsonLd")( + test("should parse the json ld") { + ModelOps.fromJsonLd(jsonLd).flatMap { model => + assertTrue(model.size() == 4) + } + }, + test("should produce isomorphic models") { + for { + model1 <- ModelOps.fromJsonLd(jsonLd) + model2 <- ModelOps.fromJsonLd(jsonLd2) + } yield assertTrue(model1.isIsomorphicWith(model2)) + }, + ), + ) +} diff --git a/webapi/src/test/scala/org/knora/webapi/slice/common/JsonLdTestUtil.scala b/webapi/src/test/scala/org/knora/webapi/slice/common/JsonLdTestUtil.scala new file mode 100644 index 0000000000..3731dbcf48 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/slice/common/JsonLdTestUtil.scala @@ -0,0 +1,59 @@ +/* + * Copyright © 2021 - 2024 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.common +import com.apicatalog.jsonld.JsonLd +import com.apicatalog.jsonld.api.CompactionApi +import com.apicatalog.jsonld.api.ExpansionApi +import com.apicatalog.jsonld.api.FlatteningApi +import com.apicatalog.jsonld.document.JsonDocument +import zio.test.Gen + +import java.io.ByteArrayInputStream +import java.nio.charset.StandardCharsets.UTF_8 + +object JsonLdTestUtil { + + def jsonLdDocumentFrom(str: String): JsonDocument = JsonDocument.of(ByteArrayInputStream(str.getBytes(UTF_8))) + + object JsonLdTransformations { + + val expand: String => String = (jsonLd: String) => { + val d: JsonDocument = jsonLdDocumentFrom(jsonLd) + val api: ExpansionApi = JsonLd.expand(d) + api.get().toString + } + + val flatten: String => String = (jsonLd: String) => { + val d: JsonDocument = jsonLdDocumentFrom(jsonLd) + val api: FlatteningApi = JsonLd.flatten(d) + api.get().toString + } + + val compact: String => String = (jsonLd: String) => { + val d: JsonDocument = jsonLdDocumentFrom(jsonLd) + val api: CompactionApi = + JsonLd.compact( + d, + jsonLdDocumentFrom("""{ + | "api": "http://api.knora.org/ontology/knora-api/v2#", + | "anything": "http://0.0.0.0:3333/ontology/0001/anything/v2#" + |}""".stripMargin), + ) + api.get().toString + } + + val noOp: String => String = (jsonLd: String) => jsonLd + + val all: Seq[String => String] = Seq( + expand, + compact, + flatten, + noOp, + ) + + val allGen: Gen[Any, String => String] = Gen.fromIterable(JsonLdTransformations.all) + } +} diff --git a/webapi/src/test/scala/org/knora/webapi/slice/common/KnoraApiValueModelSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/common/KnoraApiValueModelSpec.scala new file mode 100644 index 0000000000..7eff61e49d --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/slice/common/KnoraApiValueModelSpec.scala @@ -0,0 +1,74 @@ +/* + * Copyright © 2021 - 2024 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.common + +import zio.Scope +import zio.json.DecoderOps +import zio.json.EncoderOps +import zio.json.ast.Json +import zio.test.* + +import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.slice.resourceinfo.domain.IriConverter + +object KnoraApiValueModelSpec extends ZIOSpecDefault { + + private val createIntegerValue = """ + { + "@id": "http://rdfh.ch/0001/a-thing", + "@type": "anything:Thing", + "anything:hasInteger": { + "@type": "knora-api:IntValue", + "knora-api:intValueAsInt": 4 + }, + "@context": { + "knora-api": "http://api.knora.org/ontology/knora-api/v2#", + "anything": "http://0.0.0.0:3333/ontology/0001/anything/v2#" + } + } + """.fromJson[Json].getOrElse(throw new Exception("Invalid JSON")) + + private val createLinkValue = + s"""{ + "@id" : "http://rdfh.ch/0001/a-thing", + "@type" : "anything:Thing", + "anything:hasOtherThingValue" : { + "@id" : "http://rdfh.ch/0001/a-thing/values/mr9i2aUUJolv64V_9hYdTw", + "@type" : "knora-api:LinkValue", + "knora-api:valueHasUUID": "mr9i2aUUJolv64V_9hYdTw", + "knora-api:linkValueHasTargetIri" : { + "@id" : "http://rdfh.ch/0001/CNhWoNGGT7iWOrIwxsEqvA" + }, + "knora-api:valueCreationDate" : { + "@type" : "xsd:dateTimeStamp", + "@value" : "2020-06-04T11:36:54.502951Z" + } + }, + "@context" : { + "xsd" : "http://www.w3.org/2001/XMLSchema#", + "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", + "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#" + } + }""".fromJson[Json].getOrElse(throw new Exception("Invalid JSON")) + + val spec = suite("KnoraApiValueModel")( + test("getResourceIri should get the id") { + check(Gen.fromIterable(Seq(createIntegerValue, createLinkValue).map(_.toJsonPretty))) { json => + for { + model <- KnoraApiValueModel.fromJsonLd(json) + } yield assertTrue(model.rootResourceIri.toString == "http://rdfh.ch/0001/a-thing") + } + }, + test("rootResourceClassIri should get the rdfs:type") { + check(Gen.fromIterable(Seq(createIntegerValue, createLinkValue).map(_.toJsonPretty))) { json => + for { + model <- KnoraApiValueModel.fromJsonLd(json) + resourceClassIri <- model.rootResourceClassIri + } yield assertTrue(resourceClassIri.toString == "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing") + } + }, + ).provideSome[Scope](IriConverter.layer, StringFormatter.test) +}