diff --git a/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala b/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala index 57f05c80b1..3408ec2064 100644 --- a/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala +++ b/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala @@ -177,7 +177,7 @@ object LayersTest { ListsEndpoints.layer, ListsEndpointsHandlers.layer, ListsResponder.layer, - ListsResponderV2Live.layer, + ListsResponderV2.layer, MaintenanceEndpoints.layer, MaintenanceEndpointsHandlers.layer, MaintenanceRestService.layer, diff --git a/integration/src/test/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADMSpec.scala b/integration/src/test/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADMSpec.scala index 0bfb27de00..4a2c033bd1 100644 --- a/integration/src/test/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADMSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADMSpec.scala @@ -7,20 +7,13 @@ package org.knora.webapi.messages.admin.responder.listsmessages import spray.json.* -import java.util.UUID - import dsp.errors.BadRequestException import dsp.valueobjects.Iri.* -import dsp.valueobjects.List.* -import dsp.valueobjects.ListErrorMessages -import dsp.valueobjects.V2 import org.knora.webapi.CoreSpec -import org.knora.webapi.messages.admin.responder.listsmessages.ListNodeCreatePayloadADM.ListChildNodeCreatePayloadADM import org.knora.webapi.messages.store.triplestoremessages.StringLiteralSequenceV2 import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 import org.knora.webapi.sharedtestdata.SharedListsTestDataADM -import org.knora.webapi.sharedtestdata.SharedTestDataADM -import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri +import org.knora.webapi.slice.admin.domain.model.ListProperties.* /** * This spec is used to test 'ListAdminMessages'. @@ -122,29 +115,6 @@ class ListsMessagesADMSpec extends CoreSpec with ListADMJsonProtocol { converted.children should be(children) } - "throw 'BadRequestException' if invalid position given in payload of `createChildNodeRequest`" in { - val caught = intercept[BadRequestException]( - ListChildNodeCreateRequestADM( - createChildNodeRequest = ListChildNodeCreatePayloadADM( - parentNodeIri = ListIri.make(exampleListIri).fold(e => throw e.head, v => v), - projectIri = ProjectIri.unsafeFrom(SharedTestDataADM.imagesProjectIri), - position = Some(Position.make(-3).fold(e => throw e.head, v => v)), - labels = Labels - .make(Seq(V2.StringLiteralV2(value = "New child node", language = Some("en")))) - .fold(e => throw e.head, v => v), - comments = Some( - Comments - .make(Seq(V2.StringLiteralV2(value = "New child comment", language = Some("en")))) - .fold(e => throw e.head, v => v) - ) - ), - requestingUser = SharedTestDataADM.imagesUser01, - apiRequestID = UUID.randomUUID() - ) - ) - assert(caught.getMessage === ListErrorMessages.InvalidPosition) - } - "throw 'BadRequestException' for `ChangeNodePositionApiRequestADM` when no parent node iri is given" in { val payload = s""" @@ -185,7 +155,9 @@ class ListsMessagesADMSpec extends CoreSpec with ListADMJsonProtocol { val thrown = the[BadRequestException] thrownBy payload.parseJson.convertTo[ChangeNodePositionApiRequestADM] - thrown.getMessage should equal(ListErrorMessages.InvalidPosition) + thrown.getMessage should equal( + "Invalid position value is given. Position should be either a positive value, 0 or -1." + ) } } } diff --git a/integration/src/test/scala/org/knora/webapi/responders/admin/ListsResponderSpec.scala b/integration/src/test/scala/org/knora/webapi/responders/admin/ListsResponderSpec.scala index 6573428fa4..536002dde1 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/admin/ListsResponderSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/admin/ListsResponderSpec.scala @@ -14,8 +14,6 @@ import dsp.errors.BadRequestException import dsp.errors.DuplicateValueException import dsp.errors.UpdateNotPerformedException import dsp.valueobjects.Iri -import dsp.valueobjects.Iri.* -import dsp.valueobjects.List.* import dsp.valueobjects.V2 import org.knora.webapi.* import org.knora.webapi.messages.admin.responder.listsmessages.ListNodeCreatePayloadADM.ListChildNodeCreatePayloadADM @@ -28,10 +26,11 @@ import org.knora.webapi.sharedtestdata.SharedListsTestDataADM import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.sharedtestdata.SharedTestDataADM2.* import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri +import org.knora.webapi.slice.admin.domain.model.ListProperties.* import org.knora.webapi.util.MutableTestIri /** - * Tests [[ListsResponderADM]]. + * Tests [[ListsResponder]]. */ class ListsResponderSpec extends CoreSpec with ImplicitSender { @@ -72,59 +71,47 @@ class ListsResponderSpec extends CoreSpec with ImplicitSender { } "return basic list information (anything list)" in { - appActor ! ListNodeInfoGetRequestADM( - iri = "http://rdfh.ch/lists/0001/treeList", - requestingUser = SharedTestDataADM.anythingUser1 - ) - - val received: RootNodeInfoGetResponseADM = expectMsgType[RootNodeInfoGetResponseADM](timeout) - - // log.debug("returned basic keyword list information: {}", MessageUtil.toSource(received.items.head)) - - received.listinfo.sorted should be(treeListInfo.sorted) + val actual = + UnsafeZioRun.runOrThrow(ListsResponder.listNodeInfoGetRequestADM("http://rdfh.ch/lists/0001/treeList")) + actual match { + case RootNodeInfoGetResponseADM(listInfo) => listInfo.sorted should be(treeListInfo.sorted) + case _ => fail(s"Expecting RootNodeInfoGetResponseADM.") + } } "return basic list information (anything other list)" in { - appActor ! ListNodeInfoGetRequestADM( - iri = "http://rdfh.ch/lists/0001/otherTreeList", - requestingUser = SharedTestDataADM.anythingUser1 + val actual = UnsafeZioRun.runOrThrow( + ListsResponder.listNodeInfoGetRequestADM("http://rdfh.ch/lists/0001/otherTreeList") ) - - val received: RootNodeInfoGetResponseADM = expectMsgType[RootNodeInfoGetResponseADM](timeout) - - // log.debug("returned basic keyword list information: {}", MessageUtil.toSource(received.items.head)) - - received.listinfo.sorted should be(otherTreeListInfo.sorted) + actual match { + case RootNodeInfoGetResponseADM(listInfo) => listInfo.sorted should be(otherTreeListInfo.sorted) + case _ => fail(s"Expecting RootNodeInfoGetResponseADM.") + } } "return basic node information (images list - sommer)" in { - appActor ! ListNodeInfoGetRequestADM( - iri = "http://rdfh.ch/lists/00FF/526f26ed04", - requestingUser = SharedTestDataADM.imagesUser01 + val actual = UnsafeZioRun.runOrThrow( + ListsResponder.listNodeInfoGetRequestADM("http://rdfh.ch/lists/00FF/526f26ed04") ) - - val received: ChildNodeInfoGetResponseADM = expectMsgType[ChildNodeInfoGetResponseADM](timeout) - - // log.debug("returned basic keyword list information: {}", MessageUtil.toSource(received.items.head)) - - received.nodeinfo.sorted should be(summerNodeInfo.sorted) + actual match { + case ChildNodeInfoGetResponseADM(childInfo) => childInfo.sorted should be(summerNodeInfo.sorted) + case _ => fail(s"Expecting ChildNodeInfoGetResponseADM.") + } } "return a full list response" in { - appActor ! ListGetRequestADM( - iri = "http://rdfh.ch/lists/0001/treeList", - requestingUser = SharedTestDataADM.anythingUser1 - ) - - val received: ListGetResponseADM = expectMsgType[ListGetResponseADM](timeout) - - // log.debug("returned whole keyword list: {}", MessageUtil.toSource(received.items.head)) - - received.list.listinfo.sorted should be(treeListInfo.sorted) - - received.list.children.map(_.sorted) should be(treeListChildNodes.map(_.sorted)) + val actual = UnsafeZioRun.runOrThrow( + ListsResponder.listGetRequestADM("http://rdfh.ch/lists/0001/treeList") + ) + actual match { + case ListGetResponseADM(list) => + list.listinfo.sorted should be(treeListInfo.sorted) + list.children.map(_.sorted) should be(treeListChildNodes.map(_.sorted)) + case _ => fail(s"Expecting ListGetResponseADM.") + } } } + val newListIri = new MutableTestIri val firstChildIri = new MutableTestIri val secondChildIri = new MutableTestIri @@ -135,13 +122,9 @@ class ListsResponderSpec extends CoreSpec with ImplicitSender { appActor ! ListRootNodeCreateRequestADM( createRootNode = ListRootNodeCreatePayloadADM( projectIri = ProjectIri.unsafeFrom(imagesProjectIri), - name = Some(ListName.make("neuelistename").fold(e => throw e.head, v => v)), - labels = Labels - .make(Seq(V2.StringLiteralV2(value = "Neue Liste", language = Some("de")))) - .fold(e => throw e.head, v => v), - comments = Comments - .make(Seq(V2.StringLiteralV2(value = "Neuer Kommentar", language = Some("de")))) - .fold(e => throw e.head, v => v) + name = Some(ListName.unsafeFrom("neuelistename")), + labels = Labels.unsafeFrom(Seq(V2.StringLiteralV2(value = "Neue Liste", language = Some("de")))), + comments = Comments.unsafeFrom(Seq(V2.StringLiteralV2(value = "Neuer Kommentar", language = Some("de")))) ), requestingUser = SharedTestDataADM.imagesUser01, apiRequestID = UUID.randomUUID @@ -175,13 +158,11 @@ class ListsResponderSpec extends CoreSpec with ImplicitSender { appActor ! ListRootNodeCreateRequestADM( createRootNode = ListRootNodeCreatePayloadADM( projectIri = ProjectIri.unsafeFrom(imagesProjectIri), - name = Some(ListName.make(nameWithSpecialCharacter).fold(e => throw e.head, v => v)), - labels = Labels - .make(Seq(V2.StringLiteralV2(value = labelWithSpecialCharacter, language = Some("de")))) - .fold(e => throw e.head, v => v), + name = Some(ListName.unsafeFrom(nameWithSpecialCharacter)), + labels = + Labels.unsafeFrom(Seq(V2.StringLiteralV2(value = labelWithSpecialCharacter, language = Some("de")))), comments = Comments - .make(Seq(V2.StringLiteralV2(value = commentWithSpecialCharacter, language = Some("de")))) - .fold(e => throw e.head, v => v) + .unsafeFrom(Seq(V2.StringLiteralV2(value = commentWithSpecialCharacter, language = Some("de")))) ), requestingUser = SharedTestDataADM.imagesUser01, apiRequestID = UUID.randomUUID @@ -213,28 +194,25 @@ class ListsResponderSpec extends CoreSpec with ImplicitSender { val changeNodeInfoRequest = NodeInfoChangeRequestADM( listIri = newListIri.get, changeNodeRequest = ListNodeChangePayloadADM( - listIri = ListIri.make(newListIri.get).fold(e => throw e.head, v => v), + listIri = ListIri.unsafeFrom(newListIri.get), projectIri = ProjectIri.unsafeFrom(imagesProjectIri), - name = Some(ListName.make("updated name").fold(e => throw e.head, v => v)), + name = Some(ListName.unsafeFrom("updated name")), labels = Some( - Labels - .make( - Seq( - V2.StringLiteralV2(value = "Neue geänderte Liste", language = Some("de")), - V2.StringLiteralV2(value = "Changed List", language = Some("en")) - ) + Labels.unsafeFrom( + Seq( + V2.StringLiteralV2(value = "Neue geänderte Liste", language = Some("de")), + V2.StringLiteralV2(value = "Changed List", language = Some("en")) ) - .fold(e => throw e.head, v => v) + ) ), comments = Some( Comments - .make( + .unsafeFrom( Seq( V2.StringLiteralV2(value = "Neuer Kommentar", language = Some("de")), V2.StringLiteralV2(value = "New Comment", language = Some("en")) ) ) - .fold(e => throw e.head, v => v) ) ), requestingUser = SharedTestDataADM.imagesUser01, @@ -267,12 +245,12 @@ class ListsResponderSpec extends CoreSpec with ImplicitSender { } "not update basic list information if name is duplicate" in { - val name = Some(ListName.make("sommer").fold(e => throw e.head, v => v)) + val name = Some(ListName.unsafeFrom("sommer")) val projectIRI = ProjectIri.unsafeFrom(imagesProjectIri) appActor ! NodeInfoChangeRequestADM( listIri = newListIri.get, changeNodeRequest = ListNodeChangePayloadADM( - listIri = ListIri.make(newListIri.get).fold(e => throw e.head, v => v), + listIri = ListIri.unsafeFrom(newListIri.get), projectIri = projectIRI, name = name ), @@ -291,16 +269,16 @@ class ListsResponderSpec extends CoreSpec with ImplicitSender { "add child to list - to the root node" in { appActor ! ListChildNodeCreateRequestADM( createChildNodeRequest = ListChildNodeCreatePayloadADM( - parentNodeIri = ListIri.make(newListIri.get).fold(e => throw e.head, v => v), + parentNodeIri = ListIri.unsafeFrom(newListIri.get), projectIri = ProjectIri.unsafeFrom(imagesProjectIri), - name = Some(ListName.make("first").fold(e => throw e.head, v => v)), - labels = Labels - .make(Seq(V2.StringLiteralV2(value = "New First Child List Node Value", language = Some("en")))) - .fold(e => throw e.head, v => v), + name = Some(ListName.unsafeFrom("first")), + labels = Labels.unsafeFrom( + Seq(V2.StringLiteralV2(value = "New First Child List Node Value", language = Some("en"))) + ), comments = Some( - Comments - .make(Seq(V2.StringLiteralV2(value = "New First Child List Node Comment", language = Some("en")))) - .fold(e => throw e.head, v => v) + Comments.unsafeFrom( + Seq(V2.StringLiteralV2(value = "New First Child List Node Comment", language = Some("en"))) + ) ) ), requestingUser = SharedTestDataADM.imagesUser01, @@ -344,17 +322,17 @@ class ListsResponderSpec extends CoreSpec with ImplicitSender { "add second child to list in first position - to the root node" in { appActor ! ListChildNodeCreateRequestADM( createChildNodeRequest = ListChildNodeCreatePayloadADM( - parentNodeIri = ListIri.make(newListIri.get).fold(e => throw e.head, v => v), + parentNodeIri = ListIri.unsafeFrom(newListIri.get), projectIri = ProjectIri.unsafeFrom(imagesProjectIri), - name = Some(ListName.make("second").fold(e => throw e.head, v => v)), - position = Some(Position.make(0).fold(e => throw e.head, v => v)), - labels = Labels - .make(Seq(V2.StringLiteralV2(value = "New Second Child List Node Value", language = Some("en")))) - .fold(e => throw e.head, v => v), + name = Some(ListName.unsafeFrom("second")), + position = Some(Position.unsafeFrom(0)), + labels = Labels.unsafeFrom( + Seq(V2.StringLiteralV2(value = "New Second Child List Node Value", language = Some("en"))) + ), comments = Some( - Comments - .make(Seq(V2.StringLiteralV2(value = "New Second Child List Node Comment", language = Some("en")))) - .fold(e => throw e.head, v => v) + Comments.unsafeFrom( + Seq(V2.StringLiteralV2(value = "New Second Child List Node Comment", language = Some("en"))) + ) ) ), requestingUser = SharedTestDataADM.imagesUser01, @@ -398,16 +376,15 @@ class ListsResponderSpec extends CoreSpec with ImplicitSender { "add child to second child node" in { appActor ! ListChildNodeCreateRequestADM( createChildNodeRequest = ListChildNodeCreatePayloadADM( - parentNodeIri = ListIri.make(secondChildIri.get).fold(e => throw e.head, v => v), + parentNodeIri = ListIri.unsafeFrom(secondChildIri.get), projectIri = ProjectIri.unsafeFrom(imagesProjectIri), - name = Some(ListName.make("third").fold(e => throw e.head, v => v)), + name = Some(ListName.unsafeFrom("third")), labels = Labels - .make(Seq(V2.StringLiteralV2(value = "New Third Child List Node Value", language = Some("en")))) - .fold(e => throw e.head, v => v), + .unsafeFrom(Seq(V2.StringLiteralV2(value = "New Third Child List Node Value", language = Some("en")))), comments = Some( - Comments - .make(Seq(V2.StringLiteralV2(value = "New Third Child List Node Comment", language = Some("en")))) - .fold(e => throw e.head, v => v) + Comments.unsafeFrom( + Seq(V2.StringLiteralV2(value = "New Third Child List Node Comment", language = Some("en"))) + ) ) ), requestingUser = SharedTestDataADM.imagesUser01, @@ -449,20 +426,20 @@ class ListsResponderSpec extends CoreSpec with ImplicitSender { } "not create a node if given new position is out of range" in { - val givenPosition = Some(Position.make(20).fold(e => throw e.head, v => v)) + val givenPosition = Some(Position.unsafeFrom(20)) appActor ! ListChildNodeCreateRequestADM( createChildNodeRequest = ListChildNodeCreatePayloadADM( - parentNodeIri = ListIri.make(newListIri.get).fold(e => throw e.head, v => v), + parentNodeIri = ListIri.unsafeFrom(newListIri.get), projectIri = ProjectIri.unsafeFrom(imagesProjectIri), - name = Some(ListName.make("fourth").fold(e => throw e.head, v => v)), + name = Some(ListName.unsafeFrom("fourth")), position = givenPosition, - labels = Labels - .make(Seq(V2.StringLiteralV2(value = "New Fourth Child List Node Value", language = Some("en")))) - .fold(e => throw e.head, v => v), + labels = Labels.unsafeFrom( + Seq(V2.StringLiteralV2(value = "New Fourth Child List Node Value", language = Some("en"))) + ), comments = Some( - Comments - .make(Seq(V2.StringLiteralV2(value = "New Fourth Child List Node Comment", language = Some("en")))) - .fold(e => throw e.head, v => v) + Comments.unsafeFrom( + Seq(V2.StringLiteralV2(value = "New Fourth Child List Node Comment", language = Some("en"))) + ) ) ), requestingUser = SharedTestDataADM.imagesUser01, @@ -617,11 +594,12 @@ class ListsResponderSpec extends CoreSpec with ImplicitSender { isShifted should be(true) /* check old parent node */ - appActor ! ListGetRequestADM( - iri = oldParentIri, - requestingUser = SharedTestDataADM.anythingAdminUser - ) - val receivedNode: ListNodeGetResponseADM = expectMsgType[ListNodeGetResponseADM](timeout) + val actual = UnsafeZioRun.runOrThrow(ListsResponder.listGetRequestADM(oldParentIri)) + val receivedNode: ListNodeGetResponseADM = actual match { + case it: ListNodeGetResponseADM => it + case _ => fail(s"Expecting ListNodeGetResponseADM.") + } + // node must not be in children of old parent val oldParentChildren = receivedNode.node.children oldParentChildren.size should be(4) @@ -666,11 +644,12 @@ class ListsResponderSpec extends CoreSpec with ImplicitSender { isShifted should be(true) /* check old parent node */ - appActor ! ListGetRequestADM( - iri = oldParentIri, - requestingUser = SharedTestDataADM.anythingAdminUser - ) - val receivedNode: ListNodeGetResponseADM = expectMsgType[ListNodeGetResponseADM](timeout) + val actual = UnsafeZioRun.runOrThrow(ListsResponder.listGetRequestADM(oldParentIri)) + val receivedNode: ListNodeGetResponseADM = actual match { + case node: ListNodeGetResponseADM => node + case _ => fail(s"Expecting ListNodeGetResponseADM.") + } + // node must not be in children of old parent val oldParentChildren = receivedNode.node.children oldParentChildren.size should be(3) diff --git a/webapi/src/main/scala/dsp/valueobjects/Iri.scala b/webapi/src/main/scala/dsp/valueobjects/Iri.scala index 8f43986de4..c770a21062 100644 --- a/webapi/src/main/scala/dsp/valueobjects/Iri.scala +++ b/webapi/src/main/scala/dsp/valueobjects/Iri.scala @@ -166,37 +166,6 @@ object Iri { else Left(s"Invalid IRI: $value") } - /** - * GroupIri value object. - */ - - /** - * ListIri value object. - */ - sealed abstract case class ListIri private (value: String) extends Iri - object ListIri { self => - def make(value: String): Validation[Throwable, ListIri] = - if (value.isEmpty) Validation.fail(BadRequestException(IriErrorMessages.ListIriMissing)) - else { - val isUuid: Boolean = UuidUtil.hasValidLength(value.split("/").last) - - if (!isListIri(value)) - Validation.fail(BadRequestException(IriErrorMessages.ListIriInvalid)) - else if (isUuid && !UuidUtil.hasSupportedVersion(value)) - Validation.fail(BadRequestException(IriErrorMessages.UuidVersionInvalid)) - else - validateAndEscapeIri(value) - .mapError(_ => BadRequestException(IriErrorMessages.ListIriInvalid)) - .map(new ListIri(_) {}) - } - - def make(value: Option[String]): Validation[Throwable, Option[ListIri]] = - value match { - case Some(v) => self.make(v).map(Some(_)) - case None => Validation.succeed(None) - } - } - /** * Base64Uuid value object. * This is base64 encoded UUID version without paddings. @@ -259,8 +228,6 @@ object Iri { } object IriErrorMessages { - val ListIriMissing = "List IRI cannot be empty." - val ListIriInvalid = "List IRI is invalid" val ProjectIriMissing = "Project IRI cannot be empty." val ProjectIriInvalid = "Project IRI is invalid." val RoleIriMissing = "Role IRI cannot be empty." diff --git a/webapi/src/main/scala/dsp/valueobjects/List.scala b/webapi/src/main/scala/dsp/valueobjects/List.scala deleted file mode 100644 index 59094df49c..0000000000 --- a/webapi/src/main/scala/dsp/valueobjects/List.scala +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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 dsp.valueobjects - -import zio.prelude.Validation - -import dsp.errors.BadRequestException - -object List { - - /** - * List ListName value object. - */ - sealed abstract case class ListName private (value: String) - object ListName { self => - def make(value: String): Validation[BadRequestException, ListName] = - if (value.isEmpty) Validation.fail(BadRequestException(ListErrorMessages.ListNameMissing)) - else - Validation - .fromOption(Iri.toSparqlEncodedString(value)) - .mapError(_ => BadRequestException(ListErrorMessages.ListNameInvalid)) - .map(new ListName(_) {}) - - def make(value: Option[String]): Validation[BadRequestException, Option[ListName]] = - value match { - case Some(v) => self.make(v).map(Some(_)) - case None => Validation.succeed(None) - } - } - - /** - * List Position value object. - */ - sealed abstract case class Position private (value: Int) - object Position { self => - def make(value: Int): Validation[BadRequestException, Position] = - if (value < -1) Validation.fail(BadRequestException(ListErrorMessages.InvalidPosition)) - else Validation.succeed(new Position(value) {}) - - def make(value: Option[Int]): Validation[BadRequestException, Option[Position]] = - value match { - case Some(v) => self.make(v).map(Some(_)) - case None => Validation.succeed(None) - } - } - - /** - * List Labels value object. - */ - sealed abstract case class Labels private (value: Seq[V2.StringLiteralV2]) - object Labels { self => - def make(value: Seq[V2.StringLiteralV2]): Validation[BadRequestException, Labels] = - if (value.isEmpty) Validation.fail(BadRequestException(ListErrorMessages.LabelsMissing)) - else { - val validatedLabels = value.map(l => - Validation - .fromOption(Iri.toSparqlEncodedString(l.value)) - .mapError(_ => BadRequestException(ListErrorMessages.LabelsInvalid)) - .map(s => V2.StringLiteralV2(s, l.language)) - ) - Validation.validateAll(validatedLabels).map(new Labels(_) {}) - } - - def make(value: Option[Seq[V2.StringLiteralV2]]): Validation[BadRequestException, Option[Labels]] = - value match { - case Some(v) => self.make(v).map(Some(_)) - case None => Validation.succeed(None) - } - } - - /** - * List Comments value object. - */ - sealed abstract case class Comments private (value: Seq[V2.StringLiteralV2]) - object Comments { self => - def make(value: Seq[V2.StringLiteralV2]): Validation[BadRequestException, Comments] = - if (value.isEmpty) Validation.fail(BadRequestException(ListErrorMessages.CommentsMissing)) - else { - val validatedComments = value.map(c => - Validation - .fromOption(Iri.toSparqlEncodedString(c.value)) - .mapError(_ => BadRequestException(ListErrorMessages.CommentsInvalid)) - .map(s => V2.StringLiteralV2(s, c.language)) - ) - Validation.validateAll(validatedComments).map(new Comments(_) {}) - } - - def make(value: Option[Seq[V2.StringLiteralV2]]): Validation[BadRequestException, Option[Comments]] = - value match { - case Some(v) => self.make(v).map(Some(_)) - case None => Validation.succeed(None) - } - } -} - -object ListErrorMessages { - val ListNameMissing = "List name cannot be empty." - val ListNameInvalid = "List name is invalid." - val LabelsMissing = "At least one label needs to be supplied." - val LabelsInvalid = "Invalid label." - val CommentsMissing = "At least one comment needs to be supplied." - val CommentsInvalid = "Invalid comment." - val ListCreatePermission = "A list can only be created by the project or system administrator." - val ListNodeCreatePermission = "A list node can only be created by the project or system administrator." - val ListChangePermission = "A list can only be changed by the project or system administrator." - val UpdateRequestEmptyLabel = "List labels cannot be empty." - val InvalidPosition = "Invalid position value is given. Position should be either a positive value, 0 or -1." -} diff --git a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala index ba3cd70ca7..0c2e233c11 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -125,7 +125,7 @@ object LayersLive { ListsEndpoints.layer, ListsEndpointsHandlers.layer, ListsResponder.layer, - ListsResponderV2Live.layer, + ListsResponderV2.layer, MaintenanceEndpoints.layer, MaintenanceEndpointsHandlers.layer, MaintenanceRestService.layer, diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADM.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADM.scala index 547c0cb771..9240ed6dd5 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADM.scala @@ -9,21 +9,23 @@ import org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import spray.json.* import java.util.UUID +import scala.util.Try import dsp.errors.BadRequestException import dsp.valueobjects.Iri -import dsp.valueobjects.ListErrorMessages import dsp.valueobjects.V2 import org.knora.webapi.* import org.knora.webapi.core.RelayedMessage import org.knora.webapi.messages.ResponderRequest.KnoraRequestADM import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.messages.admin.responder.AdminKnoraResponseADM import org.knora.webapi.messages.admin.responder.KnoraResponseADM import org.knora.webapi.messages.admin.responder.listsmessages.ListNodeCreatePayloadADM.ListChildNodeCreatePayloadADM import org.knora.webapi.messages.admin.responder.listsmessages.ListNodeCreatePayloadADM.ListRootNodeCreatePayloadADM import org.knora.webapi.messages.store.triplestoremessages.StringLiteralSequenceV2 import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 import org.knora.webapi.messages.store.triplestoremessages.TriplestoreJsonProtocol +import org.knora.webapi.slice.admin.domain.model.ListProperties import org.knora.webapi.slice.admin.domain.model.User /////////////// API requests @@ -140,9 +142,8 @@ case class ChangeNodePositionApiRequestADM(position: Int, parentIri: IRI) extend } if (!Iri.isListIri(parentIri)) throw BadRequestException(s"Invalid IRI is given: $parentIri.") - if (position < -1) { - throw BadRequestException(ListErrorMessages.InvalidPosition) - } + ListProperties.Position.from(position).fold(error => throw BadRequestException(error), _ => ()) + def toJsValue: JsValue = changeNodePositionApiRequestADMFormat.write(this) } @@ -154,22 +155,6 @@ case class ChangeNodePositionApiRequestADM(position: Int, parentIri: IRI) extend */ sealed trait ListsResponderRequestADM extends KnoraRequestADM with RelayedMessage -/** - * Requests a node (root or child). A successful response will be a [[ListItemGetResponseADM]] - * - * @param iri the IRI of the node (root or child). - * @param requestingUser the user making the request. - */ -case class ListGetRequestADM(iri: IRI, requestingUser: User) extends ListsResponderRequestADM - -/** - * Request basic information about a node (root or child). A successful response will be a [[NodeInfoGetResponseADM]] - * - * @param iri the IRI of the list node. - * @param requestingUser the user making the request. - */ -case class ListNodeInfoGetRequestADM(iri: IRI, requestingUser: User) extends ListsResponderRequestADM - /** * Requests the path from the root node of a list to a particular node. A successful response will be * a [[NodePathGetResponseADM]]. @@ -347,14 +332,14 @@ case class ListsGetResponseADM(lists: Seq[ListNodeInfoADM]) extends KnoraRespons def toJsValue: JsValue = listsGetResponseADMFormat.write(this) } -abstract class ListItemGetResponseADM() extends KnoraResponseADM with ListADMJsonProtocol +sealed trait ListItemGetResponseADM extends AdminKnoraResponseADM with ListADMJsonProtocol /** - * Provides completes information about the list. The basic information (rood node) and all the child nodes. + * Provides completes information about the list. The basic information (root node) and all the child nodes. * * @param list the complete list. */ -case class ListGetResponseADM(list: ListADM) extends ListItemGetResponseADM() { +case class ListGetResponseADM(list: ListADM) extends ListItemGetResponseADM { def toJsValue: JsValue = listGetResponseADMFormat.write(this) } @@ -364,7 +349,7 @@ case class ListGetResponseADM(list: ListADM) extends ListItemGetResponseADM() { * * @param node the node. */ -case class ListNodeGetResponseADM(node: NodeADM) extends ListItemGetResponseADM() { +case class ListNodeGetResponseADM(node: NodeADM) extends ListItemGetResponseADM { def toJsValue: JsValue = listNodeGetResponseADMFormat.write(this) } @@ -372,14 +357,14 @@ case class ListNodeGetResponseADM(node: NodeADM) extends ListItemGetResponseADM( /** * Provides basic information about any node (root or child) without it's children. */ -abstract class NodeInfoGetResponseADM() extends KnoraResponseADM with ListADMJsonProtocol +sealed trait NodeInfoGetResponseADM extends AdminKnoraResponseADM with ListADMJsonProtocol /** * Provides basic information about a root node without it's children. * * @param listinfo the basic information about a list. */ -case class RootNodeInfoGetResponseADM(listinfo: ListRootNodeInfoADM) extends NodeInfoGetResponseADM() { +case class RootNodeInfoGetResponseADM(listinfo: ListRootNodeInfoADM) extends NodeInfoGetResponseADM { def toJsValue: JsValue = listInfoGetResponseADMFormat.write(this) } @@ -389,7 +374,7 @@ case class RootNodeInfoGetResponseADM(listinfo: ListRootNodeInfoADM) extends Nod * * @param nodeinfo the basic information about a list node. */ -case class ChildNodeInfoGetResponseADM(nodeinfo: ListChildNodeInfoADM) extends NodeInfoGetResponseADM() { +case class ChildNodeInfoGetResponseADM(nodeinfo: ListChildNodeInfoADM) extends NodeInfoGetResponseADM { def toJsValue: JsValue = listNodeInfoGetResponseADMFormat.write(this) } @@ -1294,13 +1279,36 @@ trait ListADMJsonProtocol extends SprayJsonSupport with DefaultJsonProtocol with implicit val nodePathGetResponseADMFormat: RootJsonFormat[NodePathGetResponseADM] = jsonFormat(NodePathGetResponseADM, "elements") implicit val listsGetResponseADMFormat: RootJsonFormat[ListsGetResponseADM] = jsonFormat(ListsGetResponseADM, "lists") - implicit val listGetResponseADMFormat: RootJsonFormat[ListGetResponseADM] = jsonFormat(ListGetResponseADM, "list") + + implicit val listItemGetResponseADMJsonFormat: RootJsonFormat[ListItemGetResponseADM] = + new RootJsonFormat[ListItemGetResponseADM] { + override def write(obj: ListItemGetResponseADM): JsValue = obj match { + case list: ListGetResponseADM => list.toJsValue + case node: ListNodeGetResponseADM => node.toJsValue + } + override def read(json: JsValue): ListItemGetResponseADM = + Try(listGetResponseADMFormat.read(json)) + .getOrElse(listNodeGetResponseADMFormat.read(json)) + } + implicit val listGetResponseADMFormat: RootJsonFormat[ListGetResponseADM] = jsonFormat(ListGetResponseADM, "list") implicit val listNodeGetResponseADMFormat: RootJsonFormat[ListNodeGetResponseADM] = jsonFormat(ListNodeGetResponseADM, "node") + + implicit val nodeInfoGetResponseADMJsonFormat: RootJsonFormat[NodeInfoGetResponseADM] = + new RootJsonFormat[NodeInfoGetResponseADM] { + override def write(obj: NodeInfoGetResponseADM): JsValue = obj match { + case root: RootNodeInfoGetResponseADM => root.toJsValue + case node: ChildNodeInfoGetResponseADM => node.toJsValue + } + override def read(json: JsValue): NodeInfoGetResponseADM = + Try(listInfoGetResponseADMFormat.read(json)) + .getOrElse(listNodeInfoGetResponseADMFormat.read(json)) + } implicit val listInfoGetResponseADMFormat: RootJsonFormat[RootNodeInfoGetResponseADM] = jsonFormat(RootNodeInfoGetResponseADM, "listinfo") implicit val listNodeInfoGetResponseADMFormat: RootJsonFormat[ChildNodeInfoGetResponseADM] = jsonFormat(ChildNodeInfoGetResponseADM, "nodeinfo") + implicit val changeNodeNameApiRequestADMFormat: RootJsonFormat[ChangeNodeNameApiRequestADM] = jsonFormat(ChangeNodeNameApiRequestADM, "name") implicit val changeNodeLabelsApiRequestADMFormat: RootJsonFormat[ChangeNodeLabelsApiRequestADM] = diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsPayloadsADM.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsPayloadsADM.scala index 04c3b03d68..3ab847f695 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsPayloadsADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsPayloadsADM.scala @@ -5,17 +5,13 @@ package org.knora.webapi.messages.admin.responder.listsmessages -import dsp.valueobjects.Iri.* -import dsp.valueobjects.List.* import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri +import org.knora.webapi.slice.admin.domain.model.ListProperties.* /** * List root node and child node creation payloads */ sealed trait ListNodeCreatePayloadADM -// TODO-mpro: -// 1. lack of consistency between parentNodeIri and hasRootNode in change payload - should be renamed to parentNodeIri -// 2. Rethink other field names if they are descriptive enough, e.g. id should be renamed to customIri or something similar object ListNodeCreatePayloadADM { final case class ListRootNodeCreatePayloadADM( id: Option[ListIri] = None, @@ -39,7 +35,6 @@ object ListNodeCreatePayloadADM { * List node update payload */ final case class ListNodeChangePayloadADM( -// TODO-mpro: listIri can be probably removed here or maybe from the route?? listIri: ListIri, projectIri: ProjectIri, hasRootNode: Option[ListIri] = None, @@ -52,20 +47,14 @@ final case class ListNodeChangePayloadADM( /** * Node Name update payload */ -final case class NodeNameChangePayloadADM( - name: ListName -) +final case class NodeNameChangePayloadADM(name: ListName) /** * Node Labels update payload */ -final case class NodeLabelsChangePayloadADM( - labels: Labels -) +final case class NodeLabelsChangePayloadADM(labels: Labels) /** * Node Comments update payload */ -final case class NodeCommentsChangePayloadADM( - comments: Comments -) +final case class NodeCommentsChangePayloadADM(comments: Comments) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponder.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponder.scala index 9c8f399b57..bda6f9290f 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponder.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponder.scala @@ -17,8 +17,6 @@ import scala.annotation.tailrec import dsp.errors.* import dsp.valueobjects.Iri import dsp.valueobjects.Iri.* -import dsp.valueobjects.List.ListName -import dsp.valueobjects.ListErrorMessages import org.knora.webapi.config.AppConfig import org.knora.webapi.core.MessageHandler import org.knora.webapi.core.MessageRelay @@ -42,6 +40,9 @@ import org.knora.webapi.responders.IriService import org.knora.webapi.responders.Responder import org.knora.webapi.responders.admin.ListsResponder.Queries import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri +import org.knora.webapi.slice.admin.domain.model.ListErrorMessages +import org.knora.webapi.slice.admin.domain.model.ListProperties.ListIri +import org.knora.webapi.slice.admin.domain.model.ListProperties.ListName import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.admin.domain.service.ProjectADMService import org.knora.webapi.slice.common.repo.service.PredicateObjectMapper @@ -72,8 +73,6 @@ final case class ListsResponder( * Receives a message of type [[ListsResponderRequestADM]], and returns an appropriate response message. */ override def handle(msg: ResponderRequest): Task[Any] = msg match { - case ListGetRequestADM(listIri, _) => listGetRequestADM(listIri) - case ListNodeInfoGetRequestADM(listIri, _) => listNodeInfoGetRequestADM(listIri) case NodePathGetRequestADM(iri, requestingUser) => nodePathGetAdminRequest(iri, requestingUser) case ListRootNodeCreateRequestADM(createRootNode, _, apiRequestID) => listCreateRequestADM(createRootNode, apiRequestID) @@ -178,63 +177,35 @@ final case class ListsResponder( /** * Retrieves a complete node (root or child) with all children from the triplestore and returns it as a [[ListItemGetResponseADM]]. - * If an IRI of a root node is given, the response is a list with root node info and all chilren of the list. + * If an IRI of a root node is given, the response is a list with root node info and all children of the list. * If an IRI of a child node is given, the response is a node with its information and all children of the sublist. * * @param nodeIri the Iri if the required node. * @return a [[ListItemGetResponseADM]]. */ - private def listGetRequestADM(nodeIri: IRI) = { + def listGetRequestADM(nodeIri: IRI): Task[ListItemGetResponseADM] = { def getNodeADM(childNode: ListChildNodeADM): Task[ListNodeGetResponseADM] = for { maybeNodeInfo <- listNodeInfoGetADM(nodeIri = nodeIri) + nodeInfo <- maybeNodeInfo match { + case Some(childNodeInfo: ListChildNodeInfoADM) => ZIO.succeed(childNodeInfo) + case _ => ZIO.fail(NotFoundException(s"Information not found for node '$nodeIri'")) + } + } yield ListNodeGetResponseADM(NodeADM(nodeInfo, childNode.children)) - nodeinfo = maybeNodeInfo match { - case Some(childNodeInfo: ListChildNodeInfoADM) => childNodeInfo - case _ => throw NotFoundException(s"Information not found for node '$nodeIri'") - } - - // make a NodeADM instance - entirenode = ListNodeGetResponseADM( - node = NodeADM( - nodeinfo = nodeinfo, - children = childNode.children - ) - ) - } yield entirenode + ZIO.ifZIO(rootNodeByIriExists(nodeIri))( + listGetADM(nodeIri).someOrFail(NotFoundException(s"List '$nodeIri' not found")).map(ListGetResponseADM.apply), + for { + maybeNode <- listNodeGetADM(nodeIri, shallow = true) - for { - exists <- rootNodeByIriExists(nodeIri) - // Is root node IRI given? - result <- - if (exists) { - for { - // Yes. Get the entire list - maybeList <- listGetADM(rootNodeIri = nodeIri) - - entireList = maybeList match { - case Some(list) => ListGetResponseADM(list = list) - case None => throw NotFoundException(s"List '$nodeIri' not found") - } - } yield entireList - } else { - for { - // No. Get the node and all its sublist children. - // First, get node itself and all children. - maybeNode <- listNodeGetADM(nodeIri = nodeIri, shallow = true) - - entireNode <- maybeNode match { - // make sure that it is a child node - case Some(childNode: ListChildNodeADM) => - // get the info of the child node - getNodeADM(childNode) - - case _ => throw NotFoundException(s"Node '$nodeIri' not found") - } - } yield entireNode - } - } yield result + entireNode <- maybeNode match { + // make sure that it is a child node + case Some(childNode: ListChildNodeADM) => getNodeADM(childNode) + case _ => ZIO.fail(NotFoundException(s"Node '$nodeIri' not found")) + } + } yield entireNode + ) } /** @@ -360,16 +331,12 @@ final case class ListsResponder( * @param nodeIri the IRI of the list node to be queried. * @return a [[ChildNodeInfoGetResponseADM]]. */ - private def listNodeInfoGetRequestADM(nodeIri: IRI) = - for { - maybeListNodeInfoADM <- listNodeInfoGetADM(nodeIri = nodeIri) - - result = maybeListNodeInfoADM match { - case Some(childInfo: ListChildNodeInfoADM) => ChildNodeInfoGetResponseADM(childInfo) - case Some(rootInfo: ListRootNodeInfoADM) => RootNodeInfoGetResponseADM(rootInfo) - case _ => throw NotFoundException(s"List node '$nodeIri' not found") - } - } yield result + def listNodeInfoGetRequestADM(nodeIri: IRI): Task[NodeInfoGetResponseADM] = + listNodeInfoGetADM(nodeIri = nodeIri).flatMap { + case Some(childInfo: ListChildNodeInfoADM) => ZIO.succeed(ChildNodeInfoGetResponseADM(childInfo)) + case Some(rootInfo: ListRootNodeInfoADM) => ZIO.succeed(RootNodeInfoGetResponseADM(rootInfo)) + case _ => ZIO.fail(NotFoundException(s"List node '$nodeIri' not found")) + } /** * Retrieves a complete node including children. The node can be the lists root node or child node. @@ -1035,7 +1002,7 @@ final case class ListsResponder( changeNodeNameSparql <- getUpdateNodeInfoSparqlStatement( changeNodeInfoRequest = ListNodeChangePayloadADM( - listIri = ListIri.make(nodeIri).fold(e => throw e.head, v => v), + listIri = ListIri.unsafeFrom(nodeIri), projectIri = projectIri, name = Some(changeNodeNameRequest.name) ) @@ -1101,7 +1068,7 @@ final case class ListsResponder( } changeNodeLabelsSparql <- getUpdateNodeInfoSparqlStatement( changeNodeInfoRequest = ListNodeChangePayloadADM( - listIri = ListIri.make(nodeIri).fold(e => throw e.head, v => v), + listIri = ListIri.unsafeFrom(nodeIri), projectIri = projectIri, labels = Some(changeNodeLabelsRequest.labels) ) @@ -1167,7 +1134,7 @@ final case class ListsResponder( changeNodeCommentsSparql <- getUpdateNodeInfoSparqlStatement( ListNodeChangePayloadADM( - listIri = ListIri.make(nodeIri).fold(e => throw e.head, v => v), + listIri = ListIri.unsafeFrom(nodeIri), projectIri = projectIri, comments = Some(changeNodeCommentsRequest.comments) ) @@ -2067,6 +2034,12 @@ object ListsResponder { def getLists(projectIri: Option[ProjectIri]): ZIO[ListsResponder, Throwable, ListsGetResponseADM] = ZIO.serviceWithZIO[ListsResponder](_.getLists(projectIri)) + def listGetRequestADM(nodeIri: IRI): ZIO[ListsResponder, Throwable, ListItemGetResponseADM] = + ZIO.serviceWithZIO[ListsResponder](_.listGetRequestADM(nodeIri)) + + def listNodeInfoGetRequestADM(nodeIri: String): ZIO[ListsResponder, Throwable, NodeInfoGetResponseADM] = + ZIO.serviceWithZIO[ListsResponder](_.listNodeInfoGetRequestADM(nodeIri)) + val layer: URLayer[ AppConfig & IriService & MessageRelay & PredicateObjectMapper & StringFormatter & TriplestoreService, ListsResponder diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ListsResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ListsResponderV2.scala index 3e3cc90a63..b6bb91bcaa 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ListsResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ListsResponderV2.scala @@ -5,7 +5,6 @@ package org.knora.webapi.responders.v2 -import zio.Task import zio.* import org.knora.webapi.IRI @@ -14,22 +13,13 @@ import org.knora.webapi.core.MessageHandler import org.knora.webapi.core.MessageRelay import org.knora.webapi.messages.ResponderRequest import org.knora.webapi.messages.admin.responder.listsmessages.ChildNodeInfoGetResponseADM -import org.knora.webapi.messages.admin.responder.listsmessages.ListGetRequestADM import org.knora.webapi.messages.admin.responder.listsmessages.ListGetResponseADM -import org.knora.webapi.messages.admin.responder.listsmessages.ListNodeInfoGetRequestADM import org.knora.webapi.messages.v2.responder.listsmessages.* import org.knora.webapi.responders.Responder +import org.knora.webapi.responders.admin.ListsResponder import org.knora.webapi.slice.admin.domain.model.User -/** - * Responds to requests relating to lists and nodes. - */ -trait ListsResponderV2 -final case class ListsResponderV2Live( - appConfig: AppConfig, - messageRelay: MessageRelay -) extends ListsResponderV2 - with MessageHandler { +final case class ListsResponderV2(appConfig: AppConfig, listsResponder: ListsResponder) extends MessageHandler { def isResponsibleFor(message: ResponderRequest): Boolean = message.isInstanceOf[ListsResponderRequestV2] @@ -51,25 +41,11 @@ final case class ListsResponderV2Live( * @param requestingUser the user making the request. * @return a [[ListGetResponseV2]]. */ - private def getList( - listIri: IRI, - requestingUser: User - ): Task[ListGetResponseV2] = - for { - listResponseADM <- - messageRelay - .ask[ListGetResponseADM]( - ListGetRequestADM( - iri = listIri, - requestingUser = requestingUser - ) - ) - - } yield ListGetResponseV2( - list = listResponseADM.list, - requestingUser.lang, - appConfig.fallbackLanguage - ) + private def getList(listIri: IRI, requestingUser: User): Task[ListGetResponseV2] = + listsResponder + .listGetRequestADM(listIri) + .mapAttempt(_.asInstanceOf[ListGetResponseADM]) + .map(resp => ListGetResponseV2(resp.list, requestingUser.lang, appConfig.fallbackLanguage)) /** * Gets a single list node from the triplestore. @@ -79,36 +55,26 @@ final case class ListsResponderV2Live( * @param requestingUser the user making the request. * @return a [[NodeGetResponseV2]]. */ - private def getNode( - nodeIri: IRI, - requestingUser: User - ): Task[NodeGetResponseV2] = - for { - nodeResponse <- - messageRelay - .ask[ChildNodeInfoGetResponseADM]( - ListNodeInfoGetRequestADM( - iri = nodeIri, - requestingUser = requestingUser - ) - ) - - } yield NodeGetResponseV2( - node = nodeResponse.nodeinfo, - requestingUser.lang, - appConfig.fallbackLanguage - ) + private def getNode(nodeIri: IRI, requestingUser: User): Task[NodeGetResponseV2] = + listsResponder + .listNodeInfoGetRequestADM(nodeIri) + .flatMap { + case ChildNodeInfoGetResponseADM(node) => ZIO.succeed(node) + case _ => ZIO.die(new IllegalStateException(s"No child node found $nodeIri")) + } + .map(NodeGetResponseV2(_, requestingUser.lang, appConfig.fallbackLanguage)) } -object ListsResponderV2Live { +object ListsResponderV2 { val layer: URLayer[ - AppConfig & MessageRelay, + AppConfig & ListsResponder & MessageRelay, ListsResponderV2 ] = ZLayer.fromZIO { for { ac <- ZIO.service[AppConfig] + lr <- ZIO.service[ListsResponder] mr <- ZIO.service[MessageRelay] - handler <- mr.subscribe(ListsResponderV2Live(ac, mr)) + handler <- mr.subscribe(ListsResponderV2(ac, lr)) } yield handler } } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/ListsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/ListsRouteADM.scala index b384463f4a..abe0cb985b 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/ListsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/ListsRouteADM.scala @@ -25,14 +25,13 @@ final case class ListsRouteADM( private val routeData: KnoraRouteData, override protected implicit val runtime: Runtime[routing.Authenticator & StringFormatter & MessageRelay] ) extends KnoraRoute(routeData, runtime) { - private val getNodeRoute: GetListItemsRouteADM = GetListItemsRouteADM(routeData, runtime) + private val createNodeRoute: CreateListItemsRouteADM = CreateListItemsRouteADM(routeData, runtime) private val deleteNodeRoute: DeleteListItemsRouteADM = DeleteListItemsRouteADM(routeData, runtime) private val updateNodeRoute: UpdateListItemsRouteADM = UpdateListItemsRouteADM(routeData, runtime) override def makeRoute: Route = - getNodeRoute.makeRoute ~ - createNodeRoute.makeRoute ~ + createNodeRoute.makeRoute ~ deleteNodeRoute.makeRoute ~ updateNodeRoute.makeRoute } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/CreateListItemsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/CreateListItemsRouteADM.scala index 03b74ded24..5ad7e4b383 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/CreateListItemsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/CreateListItemsRouteADM.scala @@ -5,7 +5,9 @@ package org.knora.webapi.routing.admin.lists -import org.apache.pekko +import org.apache.pekko.http.scaladsl.server.Directives.* +import org.apache.pekko.http.scaladsl.server.PathMatcher +import org.apache.pekko.http.scaladsl.server.Route import zio.* import zio.prelude.Validation @@ -13,10 +15,6 @@ import java.util.UUID import dsp.errors.BadRequestException import dsp.errors.ForbiddenException -import dsp.errors.ValidationException -import dsp.valueobjects.Iri.* -import dsp.valueobjects.List.* -import dsp.valueobjects.ListErrorMessages import org.knora.webapi.core.MessageRelay import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.admin.responder.listsmessages.ListNodeCreatePayloadADM.ListChildNodeCreatePayloadADM @@ -27,10 +25,10 @@ import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM.* import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri - -import pekko.http.scaladsl.server.Directives.* -import pekko.http.scaladsl.server.PathMatcher -import pekko.http.scaladsl.server.Route +import org.knora.webapi.slice.admin.domain.model.ListErrorMessages +import org.knora.webapi.slice.admin.domain.model.ListProperties.* +import org.knora.webapi.slice.common.ToValidation.validateOneWithFrom +import org.knora.webapi.slice.common.ToValidation.validateOptionWithFrom /** * Provides routes to create list items. @@ -55,18 +53,18 @@ final case class CreateListItemsRouteADM( private def createListRootNode(): Route = path(listsBasePath) { post { entity(as[ListRootNodeCreateApiRequestADM]) { apiRequest => requestContext => - val maybeId: Validation[Throwable, Option[ListIri]] = ListIri.make(apiRequest.id) - val projectIri: Validation[Throwable, ProjectIri] = - Validation.fromEither(ProjectIri.from(apiRequest.projectIri)).mapError(ValidationException.apply) - val maybeName: Validation[Throwable, Option[ListName]] = ListName.make(apiRequest.name) - val labels: Validation[Throwable, Labels] = Labels.make(apiRequest.labels) - val comments: Validation[Throwable, Comments] = Comments.make(apiRequest.comments) - val validatedPayload: Validation[Throwable, ListRootNodeCreatePayloadADM] = - Validation.validateWith(maybeId, projectIri, maybeName, labels, comments)(ListRootNodeCreatePayloadADM) + val maybeId = validateOptionWithFrom(apiRequest.id, ListIri.from, BadRequestException.apply) + val projectIri = validateOneWithFrom(apiRequest.projectIri, ProjectIri.from, BadRequestException.apply) + val nameValidation = validateOptionWithFrom(apiRequest.name, ListName.from, BadRequestException.apply) + val labels = validateOneWithFrom(apiRequest.labels, Labels.from, BadRequestException.apply) + val comments = validateOneWithFrom(apiRequest.comments, Comments.from, BadRequestException.apply) val requestMessage = for { - payload <- validatedPayload.toZIO - user <- Authenticator.getUserADM(requestContext) + payload <- + Validation + .validateWith(maybeId, projectIri, nameValidation, labels, comments)(ListRootNodeCreatePayloadADM) + .toZIO + user <- Authenticator.getUserADM(requestContext) _ <- ZIO .fail(ForbiddenException(ListErrorMessages.ListCreatePermission)) @@ -87,13 +85,13 @@ final case class CreateListItemsRouteADM( _ <- ZIO .fail(BadRequestException("Route and payload parentNodeIri mismatch.")) .when(iri != apiRequest.parentNodeIri) - parentNodeIri = ListIri.make(apiRequest.parentNodeIri) - id = ListIri.make(apiRequest.id) - projectIri = Validation.fromEither(ProjectIri.from(apiRequest.projectIri)).mapError(ValidationException.apply) - name = ListName.make(apiRequest.name) - position = Position.make(apiRequest.position) - labels = Labels.make(apiRequest.labels) - comments = Comments.make(apiRequest.comments) + parentNodeIri = validateOneWithFrom(apiRequest.parentNodeIri, ListIri.from, BadRequestException.apply) + id = validateOptionWithFrom(apiRequest.id, ListIri.from, BadRequestException.apply) + projectIri = validateOneWithFrom(apiRequest.projectIri, ProjectIri.from, BadRequestException.apply) + name = validateOptionWithFrom(apiRequest.name, ListName.from, BadRequestException.apply) + position = validateOptionWithFrom(apiRequest.position, Position.from, BadRequestException.apply) + labels = validateOneWithFrom(apiRequest.labels, Labels.from, BadRequestException.apply) + comments = validateOptionWithFrom(apiRequest.comments, Comments.from, BadRequestException.apply) } yield Validation.validateWith(id, parentNodeIri, projectIri, name, position, labels, comments)( ListChildNodeCreatePayloadADM ) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/GetListItemsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/GetListItemsRouteADM.scala deleted file mode 100644 index 21b16c006e..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/GetListItemsRouteADM.scala +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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.routing.admin.lists - -import org.apache.pekko -import zio.* - -import org.knora.webapi.core.MessageRelay -import org.knora.webapi.messages.StringFormatter -import org.knora.webapi.messages.admin.responder.listsmessages.* -import org.knora.webapi.routing.Authenticator -import org.knora.webapi.routing.KnoraRoute -import org.knora.webapi.routing.KnoraRouteData -import org.knora.webapi.routing.RouteUtilADM.* - -import pekko.http.scaladsl.server.Directives.* -import pekko.http.scaladsl.server.PathMatcher -import pekko.http.scaladsl.server.Route - -/** - * Provides routes to get list items. - * - * @param routeData the [[KnoraRouteData]] to be used in constructing the route. - */ -final case class GetListItemsRouteADM( - private val routeData: KnoraRouteData, - override protected implicit val runtime: Runtime[Authenticator & MessageRelay & StringFormatter] -) extends KnoraRoute(routeData, runtime) - with ListADMJsonProtocol { - - val listsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "lists") - - def makeRoute: Route = - getListNode ~ - getListOrNodeInfo("infos") ~ - getListOrNodeInfo("nodes") ~ - getListInfo - - /** - * Returns a list node, root or child, with children (if exist). - */ - private def getListNode: Route = path(listsBasePath / Segment) { iri => - get { ctx => - val task = getIriUser(iri, ctx).map(r => ListGetRequestADM(r.iri, r.user)) - runJsonRouteZ(task, ctx) - } - } - - /** - * Returns basic information about list node, root or child, w/o children (if exist). - */ - private def getListOrNodeInfo(routeSwitch: String): Route = - path(listsBasePath / routeSwitch / Segment) { iri => - get { ctx => - val task = getIriUser(iri, ctx).map(r => ListNodeInfoGetRequestADM(r.iri, r.user)) - runJsonRouteZ(task, ctx) - } - } - - /** - * Returns basic information about a node, root or child, w/o children. - */ - private def getListInfo: Route = - // Brought from new lists route implementation, has the e functionality as getListOrNodeInfo - path(listsBasePath / Segment / "info") { iri => - get { ctx => - val task = getIriUser(iri, ctx).map(r => ListNodeInfoGetRequestADM(r.iri, r.user)) - runJsonRouteZ(task, ctx) - } - } -} diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/UpdateListItemsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/UpdateListItemsRouteADM.scala index cd2f9a4dbc..dfaeda10b8 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/UpdateListItemsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/UpdateListItemsRouteADM.scala @@ -5,7 +5,9 @@ package org.knora.webapi.routing.admin.lists -import org.apache.pekko +import org.apache.pekko.http.scaladsl.server.Directives.* +import org.apache.pekko.http.scaladsl.server.PathMatcher +import org.apache.pekko.http.scaladsl.server.Route import zio.* import zio.prelude.Validation @@ -13,11 +15,7 @@ import java.util.UUID import dsp.errors.BadRequestException import dsp.errors.ForbiddenException -import dsp.errors.ValidationException import dsp.valueobjects.Iri -import dsp.valueobjects.Iri.* -import dsp.valueobjects.List.* -import dsp.valueobjects.ListErrorMessages import org.knora.webapi.core.MessageRelay import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.admin.responder.listsmessages.* @@ -26,10 +24,14 @@ import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM.* import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri - -import pekko.http.scaladsl.server.Directives.* -import pekko.http.scaladsl.server.PathMatcher -import pekko.http.scaladsl.server.Route +import org.knora.webapi.slice.admin.domain.model.ListErrorMessages +import org.knora.webapi.slice.admin.domain.model.ListProperties.Comments +import org.knora.webapi.slice.admin.domain.model.ListProperties.Labels +import org.knora.webapi.slice.admin.domain.model.ListProperties.ListIri +import org.knora.webapi.slice.admin.domain.model.ListProperties.ListName +import org.knora.webapi.slice.admin.domain.model.ListProperties.Position +import org.knora.webapi.slice.common.ToValidation.validateOneWithFrom +import org.knora.webapi.slice.common.ToValidation.validateOptionWithFrom /** * Provides routes to update list items. @@ -63,7 +65,7 @@ final case class UpdateListItemsRouteADM( .validateAndEscapeIri(iri) .toZIO .orElseFail(BadRequestException(s"Invalid param node IRI: $iri")) - listName <- ListName.make(apiRequest.name).toZIO.mapError(e => BadRequestException(e.getMessage)) + listName <- ZIO.fromEither(ListName.from(apiRequest.name)).mapError(BadRequestException.apply) payload = NodeNameChangePayloadADM(listName) uuid <- getUserUuid(requestContext) } yield NodeNameChangeRequestADM(nodeIri, payload, uuid.user, uuid.uuid) @@ -84,7 +86,7 @@ final case class UpdateListItemsRouteADM( .validateAndEscapeIri(iri) .toZIO .orElseFail(BadRequestException(s"Invalid param node IRI: $iri")) - labels <- Labels.make(apiRequest.labels).toZIO.mapError(e => BadRequestException(e.getMessage)) + labels <- validateOneWithFrom(apiRequest.labels, Labels.from, BadRequestException.apply).toZIO payload = NodeLabelsChangePayloadADM(labels) uuid <- getUserUuid(requestContext) } yield NodeLabelsChangeRequestADM(nodeIri, payload, uuid.user, uuid.uuid) @@ -105,7 +107,7 @@ final case class UpdateListItemsRouteADM( .validateAndEscapeIri(iri) .toZIO .orElseFail(BadRequestException(s"Invalid param node IRI: $iri")) - comments <- Comments.make(apiRequest.comments).toZIO.mapError(e => BadRequestException(e.getMessage)) + comments <- ZIO.fromEither(Comments.from(apiRequest.comments)).mapError(BadRequestException.apply) payload = NodeCommentsChangePayloadADM(comments) uuid <- getUserUuid(requestContext) } yield NodeCommentsChangeRequestADM(nodeIri, payload, uuid.user, uuid.uuid) @@ -136,13 +138,13 @@ final case class UpdateListItemsRouteADM( entity(as[ListNodeChangeApiRequestADM]) { apiRequest => requestContext => val validatedPayload = for { _ <- ZIO.fail(BadRequestException("Route and payload listIri mismatch.")).when(iri != apiRequest.listIri) - listIri = ListIri.make(apiRequest.listIri) - projectIri = Validation.fromEither(ProjectIri.from(apiRequest.projectIri)).mapError(ValidationException.apply) - hasRootNode = ListIri.make(apiRequest.hasRootNode) - position = Position.make(apiRequest.position) - name = ListName.make(apiRequest.name) - labels = Labels.make(apiRequest.labels) - comments = Comments.make(apiRequest.comments) + listIri = validateOneWithFrom(apiRequest.listIri, ListIri.from, BadRequestException.apply) + projectIri = validateOneWithFrom(apiRequest.projectIri, ProjectIri.from, BadRequestException.apply) + hasRootNode = validateOptionWithFrom(apiRequest.hasRootNode, ListIri.from, BadRequestException.apply) + position = validateOptionWithFrom(apiRequest.position, Position.from, BadRequestException.apply) + name = validateOptionWithFrom(apiRequest.name, ListName.from, BadRequestException.apply) + labels = validateOptionWithFrom(apiRequest.labels, Labels.from, BadRequestException.apply) + comments = validateOptionWithFrom(apiRequest.comments, Comments.from, BadRequestException.apply) } yield Validation.validateWith(listIri, projectIri, hasRootNode, position, name, labels, comments)( ListNodeChangePayloadADM ) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Codecs.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Codecs.scala index 952cd76ff0..660dd0b28c 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Codecs.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Codecs.scala @@ -12,9 +12,15 @@ import zio.json.JsonCodec import dsp.valueobjects.V2 import org.knora.webapi.slice.admin.api.model.MaintenanceRequests.AssetId import org.knora.webapi.slice.admin.domain.model.KnoraProject.* +import org.knora.webapi.slice.admin.domain.model.ListProperties.Comments +import org.knora.webapi.slice.admin.domain.model.ListProperties.Labels +import org.knora.webapi.slice.admin.domain.model.ListProperties.ListIri +import org.knora.webapi.slice.admin.domain.model.ListProperties.ListName +import org.knora.webapi.slice.admin.domain.model.ListProperties.Position import org.knora.webapi.slice.admin.domain.model.RestrictedViewSize import org.knora.webapi.slice.admin.domain.model.UserIri import org.knora.webapi.slice.common.Value.BooleanValue +import org.knora.webapi.slice.common.Value.IntValue import org.knora.webapi.slice.common.Value.StringValue import org.knora.webapi.slice.common.domain.SparqlEncodedString @@ -59,10 +65,24 @@ object Codecs { private def booleanCodec[A](from: Boolean => A, to: A => Boolean): StringCodec[A] = JsonCodec[Boolean].transform(from, to) + private def intCodec[A <: IntValue](from: Int => Either[String, A]): StringCodec[A] = + JsonCodec.int.transformOrFail(from, _.value) + + // list properties + implicit val comments: StringCodec[Comments] = + JsonCodec[Seq[V2.StringLiteralV2]].transformOrFail(Comments.from, _.value) implicit val description: StringCodec[Description] = JsonCodec[V2.StringLiteralV2].transformOrFail(Description.from, _.value) + implicit val labels: StringCodec[Labels] = + JsonCodec[Seq[V2.StringLiteralV2]].transformOrFail(Labels.from, _.value) + implicit val listIri: StringCodec[ListIri] = stringCodec(ListIri.from) + implicit val listName: StringCodec[ListName] = stringCodec(ListName.from) + implicit val position: StringCodec[Position] = intCodec(Position.from) - implicit val assetId: StringCodec[AssetId] = stringCodec(AssetId.from, _.value) + // maintenance + implicit val assetId: StringCodec[AssetId] = stringCodec(AssetId.from, _.value) + + // project implicit val keyword: StringCodec[Keyword] = stringCodec(Keyword.from) implicit val logo: StringCodec[Logo] = stringCodec(Logo.from) implicit val longname: StringCodec[Longname] = stringCodec(Longname.from) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/FilesEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/FilesEndpoints.scala index 395ad28293..78911da57f 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/FilesEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/FilesEndpoints.scala @@ -31,7 +31,9 @@ final case class FilesEndpoints(base: BaseEndpoints) { "Returns the permission code and the project's restricted view settings for a given shortcode and filename." ) - val endpoints: Seq[AnyEndpoint] = Seq(getAdminFilesShortcodeFileIri.endpoint) + val endpoints: Seq[AnyEndpoint] = Seq( + getAdminFilesShortcodeFileIri + ).map(_.endpoint.tag("Admin Files")) } object FilesEndpoints { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpoints.scala index 9f0217054c..fde7bb1347 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpoints.scala @@ -17,30 +17,28 @@ import org.knora.webapi.slice.admin.api.AdminPathVariables.groupIri import org.knora.webapi.slice.common.api.BaseEndpoints final case class GroupsEndpoints(baseEndpoints: BaseEndpoints) { + private val base = "admin" / "groups" - private val tags = List("Groups", "Admin API") val getGroups = baseEndpoints.publicEndpoint.get .in(base) .out(sprayJsonBody[GroupsGetResponseADM]) .description("Returns all groups.") - .tags(tags) val getGroup = baseEndpoints.publicEndpoint.get .in(base / groupIri) .out(sprayJsonBody[GroupGetResponseADM]) .description("Returns a single group identified by IRI.") - .tags(tags) val getGroupMembers = baseEndpoints.securedEndpoint.get .in(base / groupIri / "members") .out(sprayJsonBody[GroupMembersGetResponseADM]) .description("Returns all members of a single group.") - .tags(tags) private val securedEndpoins = Seq(getGroupMembers).map(_.endpoint) - val endpoints: Seq[AnyEndpoint] = Seq(getGroups, getGroup) ++ securedEndpoins + val endpoints: Seq[AnyEndpoint] = (Seq(getGroups, getGroup) ++ securedEndpoins) + .map(_.tag("Admin Groups")) } object GroupsEndpoints { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ListsEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ListsEndpoints.scala index fad2280e27..b8ea13cf43 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ListsEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ListsEndpoints.scala @@ -11,11 +11,15 @@ import sttp.tapir.json.spray.jsonBody as sprayJsonBody import zio.ZLayer import org.knora.webapi.messages.admin.responder.listsmessages.ListADMJsonProtocol +import org.knora.webapi.messages.admin.responder.listsmessages.ListItemGetResponseADM import org.knora.webapi.messages.admin.responder.listsmessages.ListsGetResponseADM +import org.knora.webapi.messages.admin.responder.listsmessages.NodeInfoGetResponseADM import org.knora.webapi.slice.admin.api.model.AdminQueryVariables import org.knora.webapi.slice.common.api.BaseEndpoints +import org.knora.webapi.slice.search.api.SearchEndpointsInputs.InputIri case class ListsEndpoints(baseEndpoints: BaseEndpoints) extends ListADMJsonProtocol { + private val base = "admin" / "lists" val getListsQueryByProjectIriOption = baseEndpoints.publicEndpoint.get @@ -24,7 +28,34 @@ case class ListsEndpoints(baseEndpoints: BaseEndpoints) extends ListADMJsonProto .out(sprayJsonBody[ListsGetResponseADM].description("Contains the list of all root nodes of each found list.")) .description("Get all lists or all lists belonging to a project.") - val endpoints = List(getListsQueryByProjectIriOption) + private val listIriPathVar: EndpointInput.PathCapture[InputIri] = path[InputIri].description("The IRI of the list.") + val getListsByIri = baseEndpoints.publicEndpoint.get + .in(base / listIriPathVar) + .out(sprayJsonBody[ListItemGetResponseADM]) + .description("Returns a list node, root or child, with children (if exist).") + + private val getListInfoDesc = "Returns basic information about a list node, root or child, w/o children (if exist)." + val getListsByIriInfo = baseEndpoints.publicEndpoint.get + .in(base / listIriPathVar / "info") + .out(sprayJsonBody[NodeInfoGetResponseADM]) + .description(getListInfoDesc) + + private val getListInfoDeprecation = "*Deprecated*. Use GET admin/lists//info instead. " + val getListsInfosByIri = baseEndpoints.publicEndpoint.get + .in(base / "infos" / listIriPathVar) + .out(sprayJsonBody[NodeInfoGetResponseADM]) + .description(getListInfoDeprecation + getListInfoDesc) + .deprecated() + + val getListsNodesByIri = baseEndpoints.publicEndpoint.get + .in(base / "nodes" / listIriPathVar) + .out(sprayJsonBody[NodeInfoGetResponseADM]) + .description(getListInfoDeprecation + getListInfoDesc) + .deprecated() + + val endpoints = + List(getListsQueryByProjectIriOption, getListsByIri, getListsByIriInfo) + .map(_.tag("Admin Lists")) } object ListsEndpoints { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ListsEndpointsHandlers.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ListsEndpointsHandlers.scala index 4a6e5657fa..437c98b4da 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ListsEndpointsHandlers.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ListsEndpointsHandlers.scala @@ -11,6 +11,7 @@ import org.knora.webapi.responders.admin.ListsResponder import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.common.api.HandlerMapper import org.knora.webapi.slice.common.api.PublicEndpointHandler +import org.knora.webapi.slice.search.api.SearchEndpointsInputs.InputIri final case class ListsEndpointsHandlers( listsEndpoints: ListsEndpoints, @@ -23,8 +24,32 @@ final case class ListsEndpointsHandlers( (iri: Option[ProjectIri]) => listsResponder.getLists(iri) ) + private val getListsByIriHandler = PublicEndpointHandler( + listsEndpoints.getListsByIri, + (iri: InputIri) => listsResponder.listGetRequestADM(iri.value) + ) + + private val getListsByIriInfoHandler = PublicEndpointHandler( + listsEndpoints.getListsByIriInfo, + (iri: InputIri) => listsResponder.listNodeInfoGetRequestADM(iri.value) + ) + + private val getListsInfosByIriHandler = PublicEndpointHandler( + listsEndpoints.getListsInfosByIri, + (iri: InputIri) => listsResponder.listNodeInfoGetRequestADM(iri.value) + ) + + private val getListsNodesByIriHandler = PublicEndpointHandler( + listsEndpoints.getListsNodesByIri, + (iri: InputIri) => listsResponder.listNodeInfoGetRequestADM(iri.value) + ) + val allHandlers = List( - getListsQueryByProjectIriHandler + getListsByIriHandler, + getListsQueryByProjectIriHandler, + getListsByIriInfoHandler, + getListsInfosByIriHandler, + getListsNodesByIriHandler ).map(mapper.mapPublicEndpointHandler(_)) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceEndpoints.scala index c53b15e402..68e20fcc9c 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceEndpoints.scala @@ -35,7 +35,7 @@ final case class MaintenanceEndpoints(baseEndpoints: BaseEndpoints) { ) .out(statusCode(StatusCode.Accepted)) - val endpoints: Seq[AnyEndpoint] = Seq(postMaintenance).map(_.endpoint) + val endpoints: Seq[AnyEndpoint] = Seq(postMaintenance).map(_.endpoint.tag("Admin Maintenance")) } object MaintenanceEndpoints { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsEndpoints.scala index 236183dbb9..e12c127740 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsEndpoints.scala @@ -10,21 +10,7 @@ import sttp.tapir.generic.auto.* import sttp.tapir.json.spray.jsonBody as sprayJsonBody import zio.ZLayer -import org.knora.webapi.messages.admin.responder.permissionsmessages.AdministrativePermissionCreateResponseADM -import org.knora.webapi.messages.admin.responder.permissionsmessages.AdministrativePermissionGetResponseADM -import org.knora.webapi.messages.admin.responder.permissionsmessages.AdministrativePermissionsForProjectGetResponseADM -import org.knora.webapi.messages.admin.responder.permissionsmessages.ChangePermissionGroupApiRequestADM -import org.knora.webapi.messages.admin.responder.permissionsmessages.ChangePermissionHasPermissionsApiRequestADM -import org.knora.webapi.messages.admin.responder.permissionsmessages.ChangePermissionPropertyApiRequestADM -import org.knora.webapi.messages.admin.responder.permissionsmessages.ChangePermissionResourceClassApiRequestADM -import org.knora.webapi.messages.admin.responder.permissionsmessages.CreateAdministrativePermissionAPIRequestADM -import org.knora.webapi.messages.admin.responder.permissionsmessages.CreateDefaultObjectAccessPermissionAPIRequestADM -import org.knora.webapi.messages.admin.responder.permissionsmessages.DefaultObjectAccessPermissionCreateResponseADM -import org.knora.webapi.messages.admin.responder.permissionsmessages.DefaultObjectAccessPermissionsForProjectGetResponseADM -import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionDeleteResponseADM -import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionGetResponseADM -import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionsADMJsonProtocol -import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionsForProjectGetResponseADM +import org.knora.webapi.messages.admin.responder.permissionsmessages.* import org.knora.webapi.slice.admin.api.AdminPathVariables.groupIri import org.knora.webapi.slice.admin.api.AdminPathVariables.permissionIri import org.knora.webapi.slice.admin.api.AdminPathVariables.projectIri @@ -107,7 +93,7 @@ final case class PermissionsEndpoints(base: BaseEndpoints) extends PermissionsAD putPerrmissionsHasPermissions, putPermisssionsResourceClass, putPermissionsProperty - ).map(_.endpoint) + ).map(_.endpoint.tag("Admin Permissions")) } object PermissionsEndpoints { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpoints.scala index 2c59d2ebac..d3aa662f63 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpoints.scala @@ -43,63 +43,52 @@ final case class ProjectsEndpoints( private val adminMembers = "admin-members" private val restrictedViewSettings = "RestrictedViewSettings" - private val tags = List("Projects", "Admin API") - object Public { val getAdminProjects = baseEndpoints.publicEndpoint.get .in(projectsBase) .out(sprayJsonBody[ProjectsGetResponseADM]) .description("Returns all projects.") - .tags(tags) val getAdminProjectsKeywords = baseEndpoints.publicEndpoint.get .in(projectsBase / keywords) .out(sprayJsonBody[ProjectsKeywordsGetResponseADM]) .description("Returns all unique keywords for all projects as a list.") - .tags(tags) val getAdminProjectsByProjectIri = baseEndpoints.publicEndpoint.get .in(projectsByIri) .out(sprayJsonBody[ProjectGetResponseADM]) .description("Returns a single project identified by the IRI.") - .tags(tags) val getAdminProjectsByProjectShortcode = baseEndpoints.publicEndpoint.get .in(projectsByShortcode) .out(sprayJsonBody[ProjectGetResponseADM]) .description("Returns a single project identified by the shortcode.") - .tags(tags) val getAdminProjectsByProjectShortname = baseEndpoints.publicEndpoint.get .in(projectsByShortname) .out(sprayJsonBody[ProjectGetResponseADM]) .description("Returns a single project identified by the shortname.") - .tags(tags) val getAdminProjectsKeywordsByProjectIri = baseEndpoints.publicEndpoint.get .in(projectsByIri / keywords) .out(sprayJsonBody[ProjectKeywordsGetResponseADM]) .description("Returns all keywords for a single project.") - .tags(tags) val getAdminProjectsByProjectIriRestrictedViewSettings = baseEndpoints.publicEndpoint.get .in(projectsByIri / restrictedViewSettings) .out(sprayJsonBody[ProjectRestrictedViewSettingsGetResponseADM]) .description("Returns the project's restricted view settings identified by the IRI.") - .tags(tags) val getAdminProjectsByProjectShortcodeRestrictedViewSettings = baseEndpoints.publicEndpoint.get .in(projectsByShortcode / restrictedViewSettings) .out(sprayJsonBody[ProjectRestrictedViewSettingsGetResponseADM]) .description("Returns the project's restricted view settings identified by the shortcode.") - .tags(tags) val getAdminProjectsByProjectShortnameRestrictedViewSettings = baseEndpoints.publicEndpoint.get .in(projectsByShortname / restrictedViewSettings) .out(sprayJsonBody[ProjectRestrictedViewSettingsGetResponseADM]) .description("Returns the project's restricted view settings identified by the shortname.") - .tags(tags) } object Secured { @@ -122,32 +111,27 @@ final case class ProjectsEndpoints( .in(bodyProjectSetRestrictedViewSizeRequest) .out(zioJsonBody[RestrictedViewResponse]) .description("Sets the project's restricted view settings identified by the IRI.") - .tags(tags) val postAdminProjectsByProjectShortcodeRestrictedViewSettings = baseEndpoints.securedEndpoint.post .in(projectsByShortcode / restrictedViewSettings) .in(bodyProjectSetRestrictedViewSizeRequest) .out(zioJsonBody[RestrictedViewResponse]) .description("Sets the project's restricted view settings identified by the shortcode.") - .tags(tags) val getAdminProjectsByProjectIriMembers = baseEndpoints.securedEndpoint.get .in(projectsByIri / members) .out(sprayJsonBody[ProjectMembersGetResponseADM]) .description("Returns all project members of a project identified by the IRI.") - .tags(tags) val getAdminProjectsByProjectShortcodeMembers = baseEndpoints.securedEndpoint.get .in(projectsByShortcode / members) .out(sprayJsonBody[ProjectMembersGetResponseADM]) .description("Returns all project members of a project identified by the shortcode.") - .tags(tags) val getAdminProjectsByProjectShortnameMembers = baseEndpoints.securedEndpoint.get .in(projectsByShortname / members) .out(sprayJsonBody[ProjectMembersGetResponseADM]) .description("Returns all project members of a project identified by the shortname.") - .tags(tags) val getAdminProjectsByProjectIriAdminMembers = baseEndpoints.securedEndpoint.get .in(projectsByIri / adminMembers) @@ -157,51 +141,43 @@ final case class ProjectsEndpoints( .in(projectsByShortcode / adminMembers) .out(sprayJsonBody[ProjectAdminMembersGetResponseADM]) .description("Returns all admin members of a project identified by the shortcode.") - .tags(tags) val getAdminProjectsByProjectShortnameAdminMembers = baseEndpoints.securedEndpoint.get .in(projectsByShortname / adminMembers) .out(sprayJsonBody[ProjectAdminMembersGetResponseADM]) .description("Returns all admin members of a project identified by the shortname.") - .tags(tags) val deleteAdminProjectsByIri = baseEndpoints.securedEndpoint.delete .in(projectsByIri) .out(sprayJsonBody[ProjectOperationResponseADM]) .description("Deletes a project identified by the IRI.") - .tags(tags) val getAdminProjectsExports = baseEndpoints.securedEndpoint.get .in(projectsBase / `export`) .out(zioJsonBody[Chunk[ProjectExportInfoResponse]]) .description("Lists existing exports of all projects.") - .tags(tags) val postAdminProjectsByShortcodeExport = baseEndpoints.securedEndpoint.post .in(projectsByShortcode / `export`) .out(statusCode(StatusCode.Accepted)) .description("Trigger an export of a project identified by the shortcode.") - .tags(tags) val postAdminProjectsByShortcodeImport = baseEndpoints.securedEndpoint.post .in(projectsByShortcode / "import") .out(zioJsonBody[ProjectImportResponse]) .description("Trigger an import of a project identified by the shortcode.") - .tags(tags) val postAdminProjects = baseEndpoints.securedEndpoint.post .in(projectsBase) .in(zioJsonBody[ProjectCreateRequest]) .out(sprayJsonBody[ProjectOperationResponseADM]) .description("Creates a new project.") - .tags(tags) val putAdminProjectsByIri = baseEndpoints.securedEndpoint.put .in(projectsByIri) .in(zioJsonBody[ProjectUpdateRequest]) .out(sprayJsonBody[ProjectOperationResponseADM]) .description("Updates a project identified by the IRI.") - .tags(tags) val getAdminProjectsByIriAllData = baseEndpoints.securedEndpoint.get .in(projectsByIri / "AllData") @@ -209,11 +185,10 @@ final case class ProjectsEndpoints( .out(header[String]("Content-Type")) .out(streamBinaryBody(PekkoStreams)(CodecFormat.OctetStream())) .description("Returns all ontologies, data, and configuration belonging to a project identified by the IRI.") - .tags(tags) } val endpoints: Seq[AnyEndpoint] = - Seq( + (Seq( Public.getAdminProjects, Public.getAdminProjectsByProjectIri, Public.getAdminProjectsByProjectIriRestrictedViewSettings, @@ -239,7 +214,7 @@ final case class ProjectsEndpoints( Secured.putAdminProjectsByIri, Secured.postAdminProjectsByProjectIriRestrictedViewSettings, Secured.postAdminProjectsByProjectShortcodeRestrictedViewSettings - ).map(_.endpoint) + ).map(_.endpoint)).map(_.tag("Admin Projects")) } object ProjectsEndpoints { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/StoreEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/StoreEndpoints.scala index d99deb91a1..ae6d1e7625 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/StoreEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/StoreEndpoints.scala @@ -8,7 +8,6 @@ package org.knora.webapi.slice.admin.api import sttp.tapir.* import sttp.tapir.generic.auto.schemaForCaseClass import sttp.tapir.json.zio.jsonBody -import sttp.tapir.query import zio.ZLayer import zio.json.DeriveJsonCodec import zio.json.JsonCodec @@ -36,9 +35,8 @@ final case class StoreEndpoints(baseEndpoints: BaseEndpoints) { .description( "Resets the content of the triplestore, only available if configuration `allowReloadOverHttp` is set to `true`." ) - .tags(List("admin")) - val endpoints = Seq(postStoreResetTriplestoreContent) + val endpoints = Seq(postStoreResetTriplestoreContent).map(_.tag("Admin Store")) } object StoreEndpoints { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersEndpoints.scala index 4b0daaae11..d994812680 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersEndpoints.scala @@ -23,28 +23,25 @@ object PathVars { path[UserIri].description("The user IRI. Must be URL-encoded.") } final case class UsersEndpoints(baseEndpoints: BaseEndpoints) { + private val base = "admin" / "users" - private val tags = List("Users", "Admin API") val getUsers = baseEndpoints.securedEndpoint.get .in(base) .out(sprayJsonBody[UsersGetResponseADM]) .description("Returns all users.") - .tags(tags) val getUserByIri = baseEndpoints.withUserEndpoint.get .in(base / "iri" / PathVars.userIriPathVar) .out(sprayJsonBody[UserResponseADM]) .description("Returns a user identified by IRI.") - .tags(tags) val deleteUser = baseEndpoints.securedEndpoint.delete .in(base / "iri" / PathVars.userIriPathVar) .out(sprayJsonBody[UserOperationResponseADM]) .description("Delete a user identified by IRI (change status to false).") - .tags(tags) - val endpoints: Seq[AnyEndpoint] = Seq(getUsers, getUserByIri, deleteUser).map(_.endpoint) + val endpoints: Seq[AnyEndpoint] = Seq(getUsers, getUserByIri, deleteUser).map(_.endpoint.tag("Admin Users")) } object UsersEndpoints { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/ListProperties.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/ListProperties.scala new file mode 100644 index 0000000000..506159fc72 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/ListProperties.scala @@ -0,0 +1,97 @@ +/* + * 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.admin.domain.model + +import zio.prelude.Validation + +import dsp.valueobjects.Iri +import dsp.valueobjects.Iri.isListIri +import dsp.valueobjects.Iri.validateAndEscapeIri +import dsp.valueobjects.IriErrorMessages +import dsp.valueobjects.UuidUtil +import dsp.valueobjects.V2 +import org.knora.webapi.slice.common.IntValueCompanion +import org.knora.webapi.slice.common.StringValueCompanion +import org.knora.webapi.slice.common.Value +import org.knora.webapi.slice.common.Value.IntValue +import org.knora.webapi.slice.common.Value.StringValue +import org.knora.webapi.slice.common.WithFrom + +object ListProperties { + + final case class ListIri private (value: String) extends AnyVal with StringValue + + object ListIri extends StringValueCompanion[ListIri] { + def from(value: String): Either[String, ListIri] = + if (value.isEmpty) Left("List IRI cannot be empty.") + else { + val isUuid: Boolean = UuidUtil.hasValidLength(value.split("/").last) + + if (!isListIri(value)) + Left("List IRI is invalid") + else if (isUuid && !UuidUtil.hasSupportedVersion(value)) + Left(IriErrorMessages.UuidVersionInvalid) + else + validateAndEscapeIri(value) + .mapError(_ => "List IRI is invalid") + .map(ListIri.apply) + .toEitherWith(_.head) + } + } + + final case class ListName private (value: String) extends AnyVal with StringValue + object ListName extends StringValueCompanion[ListName] { + def from(value: String): Either[String, ListName] = + if (value.isEmpty) Left("List name cannot be empty.") + else Iri.toSparqlEncodedString(value).toRight("List name is invalid.").map(ListName.apply) + } + + final case class Position private (value: Int) extends AnyVal with IntValue + + object Position extends IntValueCompanion[Position] { + def from(value: Int): Either[String, Position] = + if (value >= -1) { Right(Position(value)) } + else { Left("Invalid position value is given. Position should be either a positive value, 0 or -1.") } + } + + final case class Labels private (value: Seq[V2.StringLiteralV2]) extends Value[Seq[V2.StringLiteralV2]] + + object Labels extends WithFrom[Seq[V2.StringLiteralV2], Labels] { + def from(value: Seq[V2.StringLiteralV2]): Either[String, Labels] = + if (value.isEmpty) Left("At least one label needs to be supplied.") + else { + val validatedLabels = value.map(l => + Validation + .fromOption(Iri.toSparqlEncodedString(l.value)) + .mapError(_ => "Invalid label.") + .map(V2.StringLiteralV2(_, l.language)) + ) + Validation.validateAll(validatedLabels).map(Labels.apply).toEitherWith(_.head) + } + } + + final case class Comments private (value: Seq[V2.StringLiteralV2]) extends Value[Seq[V2.StringLiteralV2]] + + object Comments extends WithFrom[Seq[V2.StringLiteralV2], Comments] { + def from(value: Seq[V2.StringLiteralV2]): Either[String, Comments] = + if (value.isEmpty) Left("At least one comment needs to be supplied.") + else { + val validatedComments = value.map(c => + Validation + .fromOption(Iri.toSparqlEncodedString(c.value)) + .mapError(_ => "Invalid comment.") + .map(s => V2.StringLiteralV2(s, c.language)) + ) + Validation.validateAll(validatedComments).map(Comments.apply).toEitherWith(_.head) + } + } +} + +object ListErrorMessages { + val ListCreatePermission = "A list can only be created by the project or system administrator." + val ListNodeCreatePermission = "A list node can only be created by the project or system administrator." + val ListChangePermission = "A list can only be changed by the project or system administrator." +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/ValueTypes.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/ValueTypes.scala index be4a38f74e..1da286b112 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/ValueTypes.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/ValueTypes.scala @@ -5,6 +5,9 @@ package org.knora.webapi.slice.common +import zio.prelude.Validation + +import org.knora.webapi.slice.common.Value.IntValue import org.knora.webapi.slice.common.Value.StringValue trait Value[A] extends Any { @@ -12,8 +15,9 @@ trait Value[A] extends Any { } object Value { - type StringValue = Value[String] type BooleanValue = Value[Boolean] + type IntValue = Value[Int] + type StringValue = Value[String] } trait WithFrom[-I, +A] { @@ -25,3 +29,37 @@ trait WithFrom[-I, +A] { } trait StringValueCompanion[A <: StringValue] extends WithFrom[String, A] +trait IntValueCompanion[A <: IntValue] extends WithFrom[Int, A] + +object ToValidation { + def validateOneWithFrom[A, B <: Value[?], E <: Throwable]( + a: A, + validator: A => Either[String, B], + err: String => E + ): Validation[E, B] = + Validation.fromEither(validator(a)).mapError(err.apply) + + /** + * Helper function to validate an optional value using a validator function. + * + * If the value is None, the validation will succeed with None. + * + * If the value is Some, the validation will use the validator function to validate the value. + * If the validation succeeds, the validated value will be wrapped in Some. + * If the validation fails, the error message will be mapped using the err function and the validation will fail. + * + * @param maybeA the optional value to validate + * @param validator the validator function + * @param err the error function to map the error message + * + * @return a Validation containing the validated value or a failed Validation containing the error constructed by err + */ + def validateOptionWithFrom[A, B <: Value[?], E]( + maybeA: Option[A], + validator: A => Either[String, B], + err: String => E + ): Validation[E, Option[B]] = maybeA match { + case Some(a) => Validation.fromEither(validator(a)).map(Some(_)).mapError(err.apply) + case None => Validation.succeed(None) + } +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/MetricsServer.scala b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/MetricsServer.scala index c911fbf732..8d3c1c7c8d 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/MetricsServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/MetricsServer.scala @@ -6,6 +6,8 @@ package org.knora.webapi.slice.infrastructure import sttp.apispec.openapi +import sttp.apispec.openapi.Contact +import sttp.apispec.openapi.Info import sttp.apispec.openapi.OpenAPI import sttp.tapir.server.ziohttp.ZioHttpInterpreter import sttp.tapir.swagger.bundle.SwaggerInterpreter @@ -69,8 +71,16 @@ object DocsServer { apiV2 <- ZIO.serviceWith[ApiV2Endpoints](_.endpoints) admin <- ZIO.serviceWith[AdminApiEndpoints](_.endpoints) allEndpoints = (apiV2 ++ admin).toList + info = Info( + title = "DSP-API", + version = BuildInfo.version, + summary = Some( + "DSP-API is part of the the DaSCH Service Platform, a repository for the long-term preservation and reuse of data in the humanities." + ), + contact = Some(Contact(name = Some("DaSCH"), url = Some("https://www.dasch.swiss/"))) + ) } yield SwaggerInterpreter(customiseDocsModel = addServer(config)) - .fromEndpoints[Task](allEndpoints, BuildInfo.name, BuildInfo.version) + .fromEndpoints[Task](allEndpoints, info) private def addServer(config: KnoraApi) = (openApi: OpenAPI) => { openApi.copy(servers = diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/api/ResourceInfoEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/api/ResourceInfoEndpoints.scala index 81e7bdb13e..454626b967 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/api/ResourceInfoEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/api/ResourceInfoEndpoints.scala @@ -27,7 +27,7 @@ final case class ResourceInfoEndpoints(baseEndpoints: BaseEndpoints) { .out(jsonBody[ListResponseDto]) val endpoints: Seq[AnyEndpoint] = - Seq(getResourcesInfo) + Seq(getResourcesInfo).map(_.tag("V2 Resources")) } object ResourceInfoEndpoints { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala index 56683fcbd3..e75582a3fc 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala @@ -69,7 +69,6 @@ object SearchEndpointsInputs { final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { - private val tags = List("v2", "search") private val gravsearchDescription = "The Gravsearch query. See https://docs.dasch.swiss/latest/DSP-API/03-endpoints/api-v2/query-language/" @@ -79,7 +78,6 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { .in(ApiV2.Inputs.formatOptions) .out(stringBody) .out(header[MediaType](HeaderNames.ContentType)) - .tags(tags) .description("Search for resources using a Gravsearch query.") val getGravsearch = baseEndpoints.withUserEndpoint.get @@ -87,7 +85,6 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { .in(ApiV2.Inputs.formatOptions) .out(stringBody) .out(header[MediaType](HeaderNames.ContentType)) - .tags(tags) .description("Search for resources using a Gravsearch query.") val postGravsearchCount = baseEndpoints.withUserEndpoint.post @@ -96,7 +93,6 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { .in(ApiV2.Inputs.formatOptions) .out(stringBody) .out(header[MediaType](HeaderNames.ContentType)) - .tags(tags) .description("Count resources using a Gravsearch query.") val getGravsearchCount = baseEndpoints.withUserEndpoint.get @@ -104,7 +100,6 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { .in(ApiV2.Inputs.formatOptions) .out(stringBody) .out(header[MediaType](HeaderNames.ContentType)) - .tags(tags) .description("Count resources using a Gravsearch query.") val getSearchByLabel = baseEndpoints.withUserEndpoint.get @@ -115,7 +110,6 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { .in(SearchEndpointsInputs.limitToResourceClass) .out(stringBody) .out(header[MediaType](HeaderNames.ContentType)) - .tags(tags) .description("Search for resources by label.") val getSearchByLabelCount = baseEndpoints.withUserEndpoint.get @@ -125,7 +119,6 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { .in(SearchEndpointsInputs.limitToResourceClass) .out(stringBody) .out(header[MediaType](HeaderNames.ContentType)) - .tags(tags) .description("Search for resources by label.") val getFullTextSearch = baseEndpoints.withUserEndpoint.get @@ -138,7 +131,6 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { .in(SearchEndpointsInputs.returnFiles) .out(stringBody) .out(header[MediaType](HeaderNames.ContentType)) - .tags(tags) .description("Search for resources by label.") val getFullTextSearchCount = baseEndpoints.withUserEndpoint.get @@ -149,7 +141,6 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { .in(SearchEndpointsInputs.limitToStandoffClass) .out(stringBody) .out(header[MediaType](HeaderNames.ContentType)) - .tags(tags) .description("Search for resources by label.") val endpoints: Seq[AnyEndpoint] = @@ -162,7 +153,7 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { getSearchByLabelCount, getFullTextSearch, getFullTextSearchCount - ).map(_.endpoint) + ).map(_.endpoint.tag("V2 Search")) } object SearchEndpoints { diff --git a/webapi/src/test/scala/dsp/valueobjects/IriSpec.scala b/webapi/src/test/scala/dsp/valueobjects/IriSpec.scala index 883c30023c..d0730897be 100644 --- a/webapi/src/test/scala/dsp/valueobjects/IriSpec.scala +++ b/webapi/src/test/scala/dsp/valueobjects/IriSpec.scala @@ -6,7 +6,6 @@ package dsp.valueobjects import zio.prelude.Validation -import zio.test.Assertion.* import zio.test.* import dsp.errors.BadRequestException @@ -22,61 +21,10 @@ object IriSpec extends ZIOSpecDefault { private val invalidIri = "Invalid IRI" - private val validListIri = "http://rdfh.ch/lists/0803/qBCJAdzZSCqC_2snW5Q7Nw" - private val listIriWithUUIDVersion3 = "http://rdfh.ch/lists/0803/6_xROK_UN1S2ZVNSzLlSXQ" - private val validRoleIri = "http://rdfh.ch/roles/ZPKPVh8yQs6F7Oyukb8WIQ" private val roleIriWithUUIDVersion3 = "http://rdfh.ch/roles/Ul3IYhDMOQ2fyoVY0ePz0w" - def spec: Spec[Any, Throwable] = listIriTest + uuidTest + roleIriTest - - private val listIriTest = suite("IriSpec - ListIri")( - test("pass an empty value and return an error") { - assertTrue(ListIri.make("") == Validation.fail(BadRequestException(IriErrorMessages.ListIriMissing))) && - assertTrue( - ListIri.make(Some("")) == Validation.fail(BadRequestException(IriErrorMessages.ListIriMissing)) - ) - }, - test("pass an invalid value and return an error") { - assertTrue( - ListIri.make(invalidIri) == Validation.fail( - BadRequestException(IriErrorMessages.ListIriInvalid) - ) - ) && - assertTrue( - ListIri.make(Some(invalidIri)) == Validation.fail( - BadRequestException(IriErrorMessages.ListIriInvalid) - ) - ) - }, - test("pass an invalid IRI containing unsupported UUID version and return an error") { - assertTrue( - ListIri.make(listIriWithUUIDVersion3) == Validation.fail( - BadRequestException(IriErrorMessages.UuidVersionInvalid) - ) - ) && - assertTrue( - ListIri.make(Some(listIriWithUUIDVersion3)) == Validation.fail( - BadRequestException(IriErrorMessages.UuidVersionInvalid) - ) - ) - }, - test("pass a valid value and successfully create value object") { - val listIri = ListIri.make(validListIri) - val maybeListIri = ListIri.make(Some(validListIri)) - - (for { - iri <- listIri - maybeIri <- maybeListIri - } yield assertTrue(iri.value == validListIri) && - assert(maybeIri)(isSome(equalTo(iri)))).toZIO - }, - test("successfully validate passing None") { - assertTrue( - ListIri.make(None) == Validation.succeed(None) - ) - } - ) + def spec: Spec[Any, Throwable] = uuidTest + roleIriTest private val uuidTest = suite("IriSpec - Base64Uuid")( test("pass an empty value and return an error") { diff --git a/webapi/src/test/scala/dsp/valueobjects/ListSpec.scala b/webapi/src/test/scala/dsp/valueobjects/ListSpec.scala deleted file mode 100644 index a73a7f473e..0000000000 --- a/webapi/src/test/scala/dsp/valueobjects/ListSpec.scala +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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 dsp.valueobjects - -import zio.prelude.Validation -import zio.test.Spec -import zio.test.ZIOSpecDefault -import zio.test.assertTrue - -import dsp.errors.BadRequestException -import dsp.valueobjects.List.Comments -import dsp.valueobjects.List.Labels -import dsp.valueobjects.List.ListName -import dsp.valueobjects.List.Position - -/** - * This spec is used to test the [[List]] value objects creation. - */ -object ListSpec extends ZIOSpecDefault { - private val validName = "Valid list name" - private val invalidName = "Invalid list name\r" - private val validPosition = 0 - private val invalidPosition = -2 - private val validLabel = Seq(V2.StringLiteralV2(value = "Valid list label", language = Some("en"))) - private val invalidLabel = Seq(V2.StringLiteralV2(value = "Invalid list label \r", language = Some("en"))) - private val validComment = Seq(V2.StringLiteralV2(value = "Valid list comment", language = Some("en"))) - private val invalidComment = Seq(V2.StringLiteralV2(value = "Invalid list comment \r", language = Some("en"))) - - def spec: Spec[Any, Any] = listNameTest + positionTest + labelsTest + commentsTest - - private val listNameTest = suite("ListSpec - ListName")( - test("pass an empty value and return an error") { - assertTrue( - ListName.make("") == Validation.fail(BadRequestException(ListErrorMessages.ListNameMissing)), - ListName.make(Some("")) == Validation.fail(BadRequestException(ListErrorMessages.ListNameMissing)) - ) - }, - test("pass an invalid value and return an error") { - assertTrue( - ListName.make(invalidName) == Validation.fail(BadRequestException(ListErrorMessages.ListNameInvalid)), - ListName.make(Some(invalidName)) == Validation.fail(BadRequestException(ListErrorMessages.ListNameInvalid)) - ) - }, - test("pass a valid value and successfully create value object") { - assertTrue( - ListName.make(validName).toOption.get.value == validName, - ListName.make(Option(validName)).getOrElse(null).get.value == validName - ) - }, - test("successfully validate passing None") { - assertTrue( - ListName.make(None) == Validation.succeed(None) - ) - } - ) - - private val positionTest = suite("ListSpec - Position")( - test("pass an invalid value and return an error") { - assertTrue( - Position.make(invalidPosition) == Validation.fail(BadRequestException(ListErrorMessages.InvalidPosition)), - Position.make(Some(invalidPosition)) == Validation.fail(BadRequestException(ListErrorMessages.InvalidPosition)) - ) - }, - test("pass a valid value and successfully create value object") { - assertTrue( - Position.make(validPosition).toOption.get.value == validPosition, - Position.make(Option(validPosition)).getOrElse(null).get.value == validPosition - ) - }, - test("successfully validate passing None") { - assertTrue( - Position.make(None) == Validation.succeed(None) - ) - } - ) - - private val labelsTest = suite("ListSpec - Labels")( - test("pass an empty object and return an error") { - assertTrue( - Labels.make(Seq.empty) == Validation.fail(BadRequestException(ListErrorMessages.LabelsMissing)), - Labels.make(Some(Seq.empty)) == Validation.fail(BadRequestException(ListErrorMessages.LabelsMissing)) - ) - }, - test("pass an invalid object and return an error") { - assertTrue( - Labels.make(invalidLabel) == Validation.fail(BadRequestException(ListErrorMessages.LabelsInvalid)), - Labels.make(Some(invalidLabel)) == Validation.fail(BadRequestException(ListErrorMessages.LabelsInvalid)) - ) - }, - test("pass a valid object and successfully create value object") { - assertTrue( - Labels.make(validLabel).toOption.get.value == validLabel, - Labels.make(Option(validLabel)).getOrElse(null).get.value == validLabel - ) - }, - test("successfully validate passing None") { - assertTrue( - Labels.make(None) == Validation.succeed(None) - ) - } - ) - - private val commentsTest = suite("ListSpec - Comments")( - test("pass an empty object and return an error") { - assertTrue( - Comments.make(Seq.empty) == Validation.fail(BadRequestException(ListErrorMessages.CommentsMissing)), - Comments.make(Some(Seq.empty)) == Validation.fail(BadRequestException(ListErrorMessages.CommentsMissing)) - ) - }, - test("pass an invalid object and return an error") { - assertTrue( - Comments.make(invalidComment) == Validation.fail(BadRequestException(ListErrorMessages.CommentsInvalid)), - Comments.make(Some(invalidComment)) == Validation.fail(BadRequestException(ListErrorMessages.CommentsInvalid)) - ) - }, - test("pass a valid object and successfully create value object") { - assertTrue( - Comments.make(validComment).toOption.get.value == validComment, - Comments.make(Option(validComment)).getOrElse(null).get.value == validComment - ) - }, - test("successfully validate passing None") { - assertTrue( - Comments.make(None) == Validation.succeed(None) - ) - } - ) -} diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/model/ListPropertiesSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/model/ListPropertiesSpec.scala new file mode 100644 index 0000000000..f62d3318f2 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/model/ListPropertiesSpec.scala @@ -0,0 +1,97 @@ +/* + * 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.admin.domain.model + +import zio.test.Gen +import zio.test.Spec +import zio.test.ZIOSpecDefault +import zio.test.assertTrue +import zio.test.check + +import dsp.valueobjects.IriErrorMessages +import dsp.valueobjects.V2 +import org.knora.webapi.slice.admin.domain.model.ListProperties.* + +/** + * This spec is used to test the [[List]] value objects creation. + */ +object ListPropertiesSpec extends ZIOSpecDefault { + def spec: Spec[Any, Any] = + suite("ListProperties")(listIriSuite, listNameSuite, positionSuite, labelsTest, commentsTest) + + private val listIriSuite = suite("ListIri")( + test("pass an empty value and return an error") { + assertTrue(ListIri.from("") == Left("List IRI cannot be empty.")) + }, + test("pass an invalid value and return an error") { + val invalid = "ftp://rdfh.ch/lists/0803/qBCJAdzZSCqC_2snW5Q7Nw" + assertTrue(ListIri.from(invalid) == Left("List IRI is invalid")) + }, + test("pass an invalid IRI containing unsupported UUID version and return an error") { + val invalid = "http://rdfh.ch/lists/0803/6_xROK_UN1S2ZVNSzLlSXQ" + assertTrue(ListIri.from(invalid) == Left(IriErrorMessages.UuidVersionInvalid)) + }, + test("pass a valid value and successfully create value object") { + val valid = "http://rdfh.ch/lists/0803/qBCJAdzZSCqC_2snW5Q7Nw" + assertTrue(ListIri.from(valid).map(_.value) == Right(valid)) + } + ) + + private val listNameSuite = suite("ListName")( + test("pass an empty value and return an error") { + assertTrue(ListName.from("") == Left("List name cannot be empty.")) + }, + test("pass an invalid value and return an error") { + assertTrue(ListName.from("Invalid list name\r") == Left("List name is invalid.")) + }, + test("pass a valid value and successfully create value object") { + assertTrue(ListName.from("Valid list name").map(_.value) == Right("Valid list name")) + } + ) + + private val positionSuite = suite("Position")( + test("should be greater than or equal -1") { + check(Gen.int(-10, 10)) { i => + val actual = Position.from(i) + i match { + case i if i >= -1 => assertTrue(actual.map(_.value) == Right(i)) + case _ => + assertTrue( + actual == Left("Invalid position value is given. Position should be either a positive value, 0 or -1.") + ) + } + } + } + ) + + private val labelsTest = suite("Labels")( + test("pass an empty object and return an error") { + assertTrue(Labels.from(Seq.empty) == Left("At least one label needs to be supplied.")) + }, + test("pass an invalid object and return an error") { + val invalid = Seq(V2.StringLiteralV2(value = "Invalid list label \r", language = Some("en"))) + assertTrue(Labels.from(invalid) == Left("Invalid label.")) + }, + test("pass a valid object and successfully create value object") { + val valid = Seq(V2.StringLiteralV2(value = "Valid list label", language = Some("en"))) + assertTrue(Labels.from(valid).map(_.value) == Right(valid)) + } + ) + + private val commentsTest = suite("Comments")( + test("pass an empty object and return an error") { + assertTrue(Comments.from(Seq.empty) == Left("At least one comment needs to be supplied.")) + }, + test("pass an invalid object and return an error") { + val invalid = Seq(V2.StringLiteralV2(value = "Invalid list comment \r", language = Some("en"))) + assertTrue(Comments.from(invalid) == Left("Invalid comment.")) + }, + test("pass a valid object and successfully create value object") { + val valid = Seq(V2.StringLiteralV2(value = "Valid list comment", language = Some("en"))) + assertTrue(Comments.from(valid).map(_.value) == Right(valid)) + } + ) +}