Skip to content

Commit

Permalink
feat: Support propert json-ld for update value endpoint (DEV-4325) (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
seakayone authored Nov 11, 2024
1 parent 0e45091 commit 039dcb6
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 175 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3066,7 +3066,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec {
"not update an integer value with an invalid custom new value version IRI" in {
val resourceIri: IRI = AThing.iri
val intValue: Int = 8
val newValueVersionIri: IRI = "foo"
val newValueVersionIri: IRI = "http://example.com/foo"

val jsonLDEntity = updateIntValueWithCustomNewValueVersionIriRequest(
resourceIri = resourceIri,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -594,178 +594,6 @@ sealed trait UpdateValueV2 {
val valueCreationDate: Option[Instant]
}

object UpdateValueV2 {

/**
* Converts JSON-LD input to a [[UpdateValueV2]].
*
* @param jsonLdString the JSON-LD input as String.
* @param requestingUser the user making the request.
* @return a case class instance representing the input.
*/
def fromJsonLd(
ingestState: AssetIngestState,
jsonLdString: String,
requestingUser: User,
): ZIO[IriConverter & SipiService & StringFormatter & MessageRelay, Throwable, UpdateValueV2] =
ZIO.serviceWithZIO[StringFormatter] { implicit stringFormatter =>
def makeUpdateValueContentV2(
resourceIri: SmartIri,
resourceClassIri: SmartIri,
propertyIri: SmartIri,
jsonLDObject: JsonLDObject,
valueIri: SmartIri,
maybeValueCreationDate: Option[Instant],
maybeNewIri: Option[SmartIri],
) =
for {
maybePermissions <-
ZIO.attempt {
val validationFun: (String, => Nothing) => String =
(s, errorFun) => Iri.toSparqlEncodedString(s).getOrElse(errorFun)
jsonLDObject.maybeStringWithValidation(HasPermissions, validationFun)
}
shortcode <- ZIO
.fromEither(resourceIri.getProjectShortcode)
.mapError(msg => NotFoundException(s"Shortcode not found. $msg"))
fileInfo <- ValueContentV2.getFileInfo(shortcode, ingestState, jsonLDObject)
valueContent <- ValueContentV2.fromJsonLdObject(jsonLDObject, requestingUser, fileInfo)
} yield UpdateValueContentV2(
resourceIri = resourceIri.toString,
resourceClassIri = resourceClassIri,
propertyIri = propertyIri,
valueIri = valueIri.toString,
valueContent = valueContent,
permissions = maybePermissions,
valueCreationDate = maybeValueCreationDate,
newValueVersionIri = maybeNewIri,
ingestState = ingestState,
)

def makeUpdateValuePermissionsV2(
resourceIri: SmartIri,
resourceClassIri: SmartIri,
propertyIri: SmartIri,
jsonLDObject: JsonLDObject,
valueIri: SmartIri,
maybeValueCreationDate: Option[Instant],
maybeNewIri: Option[SmartIri],
) = ZIO.serviceWithZIO[StringFormatter] { implicit stringFormatter =>
// Yes. This is a request to change the value's permissions.
for {
valueType <- ZIO.attempt(
jsonLDObject.requireStringWithValidation(
JsonLDKeywords.TYPE,
stringFormatter.toSmartIriWithErr,
),
)
permissions <- ZIO.attempt {
val validationFun: (String, => Nothing) => String =
(s, errorFun) => Iri.toSparqlEncodedString(s).getOrElse(errorFun)
jsonLDObject.requireStringWithValidation(HasPermissions, validationFun)
}
} yield UpdateValuePermissionsV2(
resourceIri = resourceIri.toString,
resourceClassIri = resourceClassIri,
propertyIri = propertyIri,
valueIri = valueIri.toString,
valueType = valueType,
permissions = permissions,
valueCreationDate = maybeValueCreationDate,
newValueVersionIri = maybeNewIri,
)
}

for {
jsonLdDocument <- RouteUtilV2.parseJsonLd(jsonLdString)
// Get the IRI of the resource that the value is to be created in.
resourceIri <- jsonLdDocument.body.getRequiredIdValueAsKnoraDataIri
.mapError(BadRequestException(_))
.flatMap(RouteUtilZ.ensureIsKnoraResourceIri)
// Get the resource class.
resourceClassIri <-
jsonLdDocument.body.getRequiredTypeAsKnoraApiV2ComplexTypeIri.mapError(BadRequestException(_))

// Get the resource property and the new value version.
updateValue <-
jsonLdDocument.body.getRequiredResourcePropertyApiV2ComplexValue.mapError(BadRequestException(_)).flatMap {
case (propertyIri: SmartIri, jsonLDObject: JsonLDObject) =>
// Get the custom value creation date, if provided.

for {
valueIri <- jsonLDObject.getRequiredIdValueAsKnoraDataIri.mapError(BadRequestException(_))
// Aside from the value's ID and type and the optional predicates above, does the value object just
otherValuePredicates: Set[IRI] = jsonLDObject.value.keySet -- Set(
JsonLDKeywords.ID,
JsonLDKeywords.TYPE,
ValueCreationDate,
NewValueVersionIri,
)
maybeValueCreationDate <- ZIO.attempt(
jsonLDObject.maybeDatatypeValueInObject(
key = ValueCreationDate,
expectedDatatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri,
validationFun = (s, errorFun) =>
ValuesValidator
.xsdDateTimeStampToInstant(s)
.getOrElse(errorFun),
),
)
// Get and validate the custom new value version IRI, if provided.

maybeNewIri <-
ZIO
.attempt(
jsonLDObject
.maybeIriInObject(NewValueVersionIri, stringFormatter.toSmartIriWithErr),
)
.flatMap(smartIriMaybe =>
ZIO.foreach(smartIriMaybe) { definedNewIri =>
if (definedNewIri == valueIri) {
ZIO.fail(
BadRequestException(
s"The IRI of a new value version cannot be the same as the IRI of the current version",
),
)
} else {
ZIO.attempt(
stringFormatter.validateCustomValueIri(
customValueIri = definedNewIri,
projectCode = valueIri.getProjectCode.get,
resourceID = valueIri.getResourceID.get,
),
)
}
},
)

value <- if (otherValuePredicates == Set(HasPermissions)) {
makeUpdateValuePermissionsV2(
resourceIri,
resourceClassIri,
propertyIri,
jsonLDObject,
valueIri,
maybeValueCreationDate,
maybeNewIri,
)
} else {
makeUpdateValueContentV2(
resourceIri,
resourceClassIri,
propertyIri,
jsonLDObject,
valueIri,
maybeValueCreationDate,
maybeNewIri,
)
}
} yield value
}
} yield updateValue
}
}

/**
* A new version of a value of a Knora property to be created.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@ final case class ValuesRouteV2()(
requestingUser <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(ctx))
apiRequestId <- Random.nextUUID
ingestState = AssetIngestState.headerAssetIngestState(ctx.request.headers)
updateValue <- UpdateValueV2.fromJsonLd(ingestState, jsonLdString, requestingUser)
updateValue <- ZIO.serviceWithZIO[ApiComplexV2JsonLdRequestParser](
_.updateValueV2fromJsonLd(jsonLdString, ingestState).mapError(BadRequestException(_)),
)
response <-
ZIO.serviceWithZIO[ValuesResponderV2](_.updateValueV2(updateValue, requestingUser, apiRequestId))
} yield response,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import org.knora.webapi.core.MessageRelay
import org.knora.webapi.messages.OntologyConstants
import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Complex.*
import org.knora.webapi.messages.OntologyConstants.Xsd
import org.knora.webapi.messages.SmartIri
import org.knora.webapi.messages.ValuesValidator
import org.knora.webapi.messages.v2.responder.valuemessages.*
import org.knora.webapi.messages.v2.responder.valuemessages.ValueContentV2.FileInfo
Expand All @@ -43,6 +44,77 @@ final case class ApiComplexV2JsonLdRequestParser(
sipiService: SipiService,
) {

def updateValueV2fromJsonLd(str: String, ingestState: AssetIngestState): IO[String, UpdateValueV2] =
ZIO.scoped {
for {
model <- ModelOps.fromJsonLd(str)
resourceAndIri <- resourceAndIri(model)
(resource, resourceIri) = resourceAndIri
resourceClassIri <- resourceClassIri(resource)
valueStatement <- valueStatement(resource)
valuePropertyIri <- valuePropertyIri(valueStatement)
valueType <- valueType(valueStatement)
valueResource = valueStatement.getObject.asResource()
valueIri <- valueIri(valueResource).someOrFail("The value IRI is required")
newValueVersionIri <- newValueVersionIri(valueResource, valueIri)
valueCreationDate <- ZIO.fromEither(valueCreationDate(valueResource))
valuePermissions <- ZIO.fromEither(valuePermissions(valueResource))
valueFileValueFilename <- ZIO.fromEither(valueFileValueFilename(valueResource))
valueContent <-
getValueContent(valueType.toString, valueResource, valueFileValueFilename, resourceIri.shortcode, ingestState)
.map(Some(_))
.orElse(ZIO.none)
updateValue <- valueContent match
case Some(valueContentV2) =>
ZIO.succeed(
UpdateValueContentV2(
resourceIri.toString,
resourceClassIri.smartIri,
valuePropertyIri.smartIri,
valueIri.toString,
valueContentV2,
valuePermissions,
valueCreationDate,
newValueVersionIri.map(_.smartIri),
ingestState,
),
)
case None =>
ZIO
.fromOption(valuePermissions)
.mapBoth(
_ => "No permissions and no value content found",
permissions =>
UpdateValuePermissionsV2(
resourceIri.toString,
resourceClassIri.smartIri,
valuePropertyIri.smartIri,
valueIri.toString,
valueType,
permissions,
valueCreationDate,
newValueVersionIri.map(_.smartIri),
),
)
} yield updateValue
}

private def newValueVersionIri(r: Resource, valueIri: ValueIri): IO[String, Option[ValueIri]] =
ZIO
.fromEither(r.objectUriOption(NewValueVersionIri))
.some
.flatMap(converter.asSmartIri(_).mapError(_.getMessage).asSomeError)
.flatMap(iri => ZIO.fromEither(ValueIri.from(iri)).asSomeError)
.filterOrFail(newV => newV != valueIri)(
Some(s"The IRI of a new value version cannot be the same as the IRI of the current version"),
)
.filterOrFail(newV => newV.sameResourceAs(valueIri))(
Some(
s"The project shortcode and resource must be equal for the new value version and the current version",
),
)
.unsome

def createValueV2FromJsonLd(str: String, ingestState: AssetIngestState): IO[String, CreateValueV2] =
ZIO.scoped {
for {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ object KnoraIris {
shortcode: Shortcode,
resourceId: ResourceId,
valueId: ValueId,
) extends KnoraIri
) extends KnoraIri {
def sameResourceAs(other: ValueIri): Boolean =
this.shortcode == other.shortcode && this.resourceId == other.resourceId
}

final case class ResourceClassIri private (smartIri: SmartIri, entityName: EntityName) extends KnoraIri

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ object ModelOpsSpec extends ZIOSpecDefault {
model2 <- ModelOps.fromJsonLd(jsonLd2)
} yield assertTrue(model1.isIsomorphicWith(model2))
},
test("should fail on invalid json ld") {
for {
exit <- ModelOps.fromJsonLd("invalid json ld").exit
} yield assertTrue(
exit == Exit.Failure(
Cause.fail("[line: 1, col: 1 ] The document could not be loaded or parsed [code=LOADING_DOCUMENT_FAILED]."),
),
)
},
),
)
}

0 comments on commit 039dcb6

Please sign in to comment.