Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Introduce Value[A] and extract tapir and zio-json codecs #2996

Merged
merged 26 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ object LayersTest {
with RestResourceInfoService
with SearchApiRoutes
with SearchResponderV2
with AssetPermissionResponder
with AssetPermissionsResponder
with StandoffResponderV2
with StandoffTagUtilV2
with State
Expand Down Expand Up @@ -209,7 +209,7 @@ object LayersTest {
SearchApiRoutes.layer,
SearchEndpoints.layer,
SearchResponderV2Live.layer,
AssetPermissionResponder.layer,
AssetPermissionsResponder.layer,
StandoffResponderV2Live.layer,
StandoffTagUtilV2Live.layer,
State.layer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ import org.knora.webapi.routing.UnsafeZioRun
import org.knora.webapi.sharedtestdata.SharedTestDataADM

/**
* Tests [[AssetPermissionResponder]].
* Tests [[AssetPermissionsResponder]].
*/
class AssetPermissionResponderSpec extends CoreSpec with ImplicitSender {
class AssetPermissionsResponderSpec extends CoreSpec with ImplicitSender {

override lazy val rdfDataObjects = List(
RdfDataObject(
Expand All @@ -31,7 +31,7 @@ class AssetPermissionResponderSpec extends CoreSpec with ImplicitSender {
"return details of a full quality file value" in {
// http://localhost:3333/v1/files/http%3A%2F%2Frdfh.ch%2F8a0b1e75%2Freps%2F7e4ba672
val actual = UnsafeZioRun.runOrThrow(
AssetPermissionResponder.getFileInfoForSipiADM(
AssetPermissionsResponder.getFileInfoForSipiADM(
ShortcodeIdentifier.unsafeFrom("0803"),
"incunabula_0000003328.jp2",
SharedTestDataADM.incunabulaMemberUser
Expand All @@ -44,7 +44,7 @@ class AssetPermissionResponderSpec extends CoreSpec with ImplicitSender {
"return details of a restricted view file value" in {
// http://localhost:3333/v1/files/http%3A%2F%2Frdfh.ch%2F8a0b1e75%2Freps%2F7e4ba672
val actual = UnsafeZioRun.runOrThrow(
AssetPermissionResponder.getFileInfoForSipiADM(
AssetPermissionsResponder.getFileInfoForSipiADM(
ShortcodeIdentifier.unsafeFrom("0803"),
"incunabula_0000003328.jp2",
SharedTestDataADM.anonymousUser
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ object LayersLive {
PredicateObjectMapper & ProjectADMRestService & ProjectADMService & ProjectExportService &
ProjectExportStorageService & ProjectImportService & ProjectsResponderADM & QueryTraverser & RepositoryUpdater &
ResourcesResponderV2 & ResourceUtilV2 & ResourceUtilV2 & RestCardinalityService & RestResourceInfoService &
SearchApiRoutes & SearchResponderV2 & AssetPermissionResponder & SipiService & StandoffResponderV2 & StandoffTagUtilV2 &
SearchApiRoutes & SearchResponderV2 & AssetPermissionsResponder & SipiService & StandoffResponderV2 & StandoffTagUtilV2 &
State & StoresResponderADM & StringFormatter & TriplestoreService & UsersResponderADM & ValuesResponderV2

/**
Expand Down Expand Up @@ -157,7 +157,7 @@ object LayersLive {
SearchApiRoutes.layer,
SearchEndpoints.layer,
SearchResponderV2Live.layer,
AssetPermissionResponder.layer,
AssetPermissionsResponder.layer,
SipiServiceLive.layer,
StandoffResponderV2Live.layer,
StandoffTagUtilV2Live.layer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,9 @@ object ProjectIdentifierADM {
fromString(projectIri).fold(err => throw err.head, identity)

def fromString(value: String): Validation[ValidationException, IriIdentifier] =
ProjectIri.from(value).map(IriIdentifier(_))
Validation
.fromEither(ProjectIri.from(value).map(IriIdentifier.apply))
.mapError(ValidationException.apply)

implicit val tapirCodec: Codec[String, IriIdentifier, TextPlain] =
Codec.string.mapDecode(str =>
Expand All @@ -388,22 +390,21 @@ object ProjectIdentifierADM {
def unsafeFrom(value: String): ShortcodeIdentifier = fromString(value).fold(err => throw err.head, identity)
def from(shortcode: Shortcode): ShortcodeIdentifier = ShortcodeIdentifier(shortcode)
def fromString(value: String): Validation[ValidationException, ShortcodeIdentifier] =
Shortcode.from(value).map {
ShortcodeIdentifier(_)
}
Validation.fromEither(Shortcode.from(value).map(ShortcodeIdentifier.from)).mapError(ValidationException(_))
}

/**
* Represents [[ShortnameIdentifier]] identifier.
*
* @param value that constructs the identifier in the type of [[Shortname]] value object.
*/
final case class ShortnameIdentifier(value: Shortname) extends ProjectIdentifierADM
final case class ShortnameIdentifier private (value: Shortname) extends ProjectIdentifierADM
object ShortnameIdentifier {
def from(shortname: Shortname): ShortnameIdentifier = ShortnameIdentifier(shortname)
def fromString(value: String): Validation[ValidationException, ShortnameIdentifier] =
Shortname.from(value).map {
ShortnameIdentifier(_)
}
Validation
.fromEither(Shortname.from(value).map(ShortnameIdentifier.from))
.mapError(ValidationException.apply)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Constru
* Responds to requests for information about binary representations of resources, and returns responses in Knora API
* ADM format.
*/
final case class AssetPermissionResponder(
final case class AssetPermissionsResponder(
private val projectsResponder: ProjectsResponderADM,
private val triplestoreService: TriplestoreService,
private implicit val sf: StringFormatter
Expand Down Expand Up @@ -90,11 +90,11 @@ final case class AssetPermissionResponder(
}
}

object AssetPermissionResponder {
object AssetPermissionsResponder {
def getFileInfoForSipiADM(shortcode: ShortcodeIdentifier, filename: String, user: User) =
ZIO.serviceWithZIO[AssetPermissionResponder](
ZIO.serviceWithZIO[AssetPermissionsResponder](
_.getPermissionCodeAndProjectRestrictedViewSettings(shortcode, filename, user)
)

val layer = ZLayer.derive[AssetPermissionResponder]
val layer = ZLayer.derive[AssetPermissionsResponder]
}
Original file line number Diff line number Diff line change
Expand Up @@ -1897,7 +1897,7 @@ final case class ListsResponderADMLive(
case _ =>
throw BadRequestException(s"Node $nodeIri was not found. Please verify the given IRI.")
}
projectIri <- ProjectIri.from(projectIriStr).toZIO.mapError(e => BadRequestException(e.getMessage))
projectIri <- ZIO.fromEither(ProjectIri.from(projectIriStr)).mapError(BadRequestException.apply)
} yield projectIri

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,7 @@ final case class PermissionsResponderADMLive(
private def validate(req: CreateAdministrativePermissionAPIRequestADM): Task[Unit] = ZIO.attempt {
req.id.foreach(iri => PermissionIri.from(iri).fold(msg => throw BadRequestException(msg), _ => ()))

ProjectIri.from(req.forProject).fold(msg => throw BadRequestException(msg.head.getMessage), _ => ())
ProjectIri.from(req.forProject).fold(msg => throw BadRequestException(msg), _ => ())

if (req.hasPermissions.isEmpty) throw BadRequestException("Permissions needs to be supplied.")

Expand Down Expand Up @@ -1512,8 +1512,7 @@ final case class PermissionsResponderADMLive(
for {
_ <- validate(createRequest)
projectIri <- ZIO
.fromEither(ProjectIri.from(createRequest.forProject).toEither)
.mapError(_.map(_.getMessage).mkString(","))
.fromEither(ProjectIri.from(createRequest.forProject))
.mapError(BadRequestException.apply)
project <- projectRepo
.findById(projectIri)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import org.knora.webapi.responders.IriService
import org.knora.webapi.responders.Responder
import org.knora.webapi.responders.v2.ontology.CardinalityHandler
import org.knora.webapi.responders.v2.ontology.OntologyHelpers
import org.knora.webapi.slice.admin.domain.model.KnoraProject
import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri
import org.knora.webapi.slice.admin.domain.model.User
import org.knora.webapi.slice.admin.domain.service.KnoraProjectRepo
import org.knora.webapi.slice.ontology.domain.service.CardinalityService
Expand Down Expand Up @@ -512,8 +512,10 @@ final case class OntologyResponderV2Live(
ReadOntologyV2(ontologyMetadata = unescapedNewMetadata)
)

projectIri <- KnoraProject.ProjectIri.from(createOntologyRequest.projectIri.toString).toZIO
_ <- cacheService.invalidateProjectADM(projectIri)
projectIri <- ZIO
.fromEither(ProjectIri.from(createOntologyRequest.projectIri.toString))
.mapError(BadRequestException.apply)
_ <- cacheService.invalidateProjectADM(projectIri)

} yield ReadOntologyMetadataV2(ontologies = Set(unescapedNewMetadata))

Expand Down Expand Up @@ -1867,8 +1869,10 @@ final case class OntologyResponderV2Live(
projectIri <-
ZIO
.fromOption(ontology.ontologyMetadata.projectIri)
.flatMap(iri => KnoraProject.ProjectIri.from(iri.toString).toZIO)
.orElseFail(InconsistentRepositoryDataException(s"Project IRI not found for ontology $internalOntologyIri"))
.mapBoth(
_ => InconsistentRepositoryDataException(s"Project IRI not found for ontology $internalOntologyIri"),
iri => ProjectIri.unsafeFrom(iri.toString)
)
_ <- cacheService.invalidateProjectADM(projectIri)

// Check that the ontology has been deleted.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import zio.*
import zio.prelude.Validation

import dsp.errors.BadRequestException
import dsp.errors.ValidationException
import dsp.valueobjects.Group.*
import dsp.valueobjects.Iri
import org.knora.webapi.core.MessageRelay
Expand Down Expand Up @@ -54,9 +55,11 @@ final case class GroupsRouteADM(
.getOrElse(Validation.succeed(None))
val name: Validation[Throwable, GroupName] = GroupName.make(apiRequest.name)
val descriptions: Validation[Throwable, GroupDescriptions] = GroupDescriptions.make(apiRequest.descriptions)
val project: Validation[Throwable, ProjectIri] = ProjectIri.from(apiRequest.project)
val status: Validation[Throwable, GroupStatus] = Validation.succeed(GroupStatus.make(apiRequest.status))
val selfjoin: Validation[Throwable, GroupSelfJoin] = GroupSelfJoin.make(apiRequest.selfjoin)
val project: Validation[Throwable, ProjectIri] = Validation
.fromEither(ProjectIri.from(apiRequest.project))
.mapError(ValidationException.apply)
val status: Validation[Throwable, GroupStatus] = Validation.succeed(GroupStatus.make(apiRequest.status))
val selfjoin: Validation[Throwable, GroupSelfJoin] = GroupSelfJoin.make(apiRequest.selfjoin)
val payloadValidation: Validation[Throwable, GroupCreatePayloadADM] =
Validation.validateWith(id, name, descriptions, project, status, selfjoin)(GroupCreatePayloadADM)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ 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
Expand Down Expand Up @@ -54,8 +55,9 @@ 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] = ProjectIri.from(apiRequest.projectIri)
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)
Expand Down Expand Up @@ -87,7 +89,7 @@ final case class CreateListItemsRouteADM(
.when(iri != apiRequest.parentNodeIri)
parentNodeIri = ListIri.make(apiRequest.parentNodeIri)
id = ListIri.make(apiRequest.id)
projectIri = ProjectIri.from(apiRequest.projectIri)
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,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.*
Expand Down Expand Up @@ -136,7 +137,7 @@ final case class UpdateListItemsRouteADM(
val validatedPayload = for {
_ <- ZIO.fail(BadRequestException("Route and payload listIri mismatch.")).when(iri != apiRequest.listIri)
listIri = ListIri.make(apiRequest.listIri)
projectIri = ProjectIri.from(apiRequest.projectIri)
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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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.api

import sttp.tapir.Codec
import sttp.tapir.CodecFormat
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.common.Value.BooleanValue
import org.knora.webapi.slice.common.Value.StringValue
import org.knora.webapi.slice.common.domain.SparqlEncodedString

object Codecs {
object TapirCodec {

private type StringCodec[A] = Codec[String, A, CodecFormat.TextPlain]
private def stringCodec[A <: StringValue](from: String => Either[String, A]): StringCodec[A] =
stringCodec(from, _.value)
private def stringCodec[A](from: String => Either[String, A], to: A => String): StringCodec[A] =
Codec.string.mapEither(from)(to)

private def booleanCodec[A <: BooleanValue](from: Boolean => A): StringCodec[A] =
booleanCodec(from, _.value)
private def booleanCodec[A](from: Boolean => A, to: A => Boolean): StringCodec[A] =
Codec.boolean.map(from)(to)

implicit val assetId: StringCodec[AssetId] = stringCodec(AssetId.from, _.value)
implicit val keyword: StringCodec[Keyword] = stringCodec(Keyword.from)
implicit val logo: StringCodec[Logo] = stringCodec(Logo.from)
implicit val longname: StringCodec[Longname] = stringCodec(Longname.from)
implicit val projectIri: StringCodec[ProjectIri] = stringCodec(ProjectIri.from)
implicit val selfJoin: StringCodec[SelfJoin] = booleanCodec(SelfJoin.from)
implicit val shortcode: StringCodec[Shortcode] = stringCodec(Shortcode.from)
implicit val shortname: StringCodec[Shortname] = stringCodec(Shortname.from)
implicit val sparqlEncodedString: StringCodec[SparqlEncodedString] = stringCodec(SparqlEncodedString.from)
implicit val status: StringCodec[Status] = booleanCodec(Status.from)
}

object ZioJsonCodec {

private type StringCodec[A] = JsonCodec[A]
private def stringCodec[A <: StringValue](from: String => Either[String, A]): StringCodec[A] =
stringCodec(from, _.value)
private def stringCodec[A](from: String => Either[String, A], to: A => String): StringCodec[A] =
JsonCodec[String].transformOrFail(from, to)

private def booleanCodec[A <: BooleanValue](from: Boolean => A): StringCodec[A] =
booleanCodec(from, _.value)
private def booleanCodec[A](from: Boolean => A, to: A => Boolean): StringCodec[A] =
JsonCodec[Boolean].transform(from, to)

implicit val description: StringCodec[Description] =
JsonCodec[V2.StringLiteralV2].transformOrFail(Description.from, _.value)

implicit val assetId: StringCodec[AssetId] = stringCodec(AssetId.from, _.value)
implicit val keyword: StringCodec[Keyword] = stringCodec(Keyword.from)
implicit val logo: StringCodec[Logo] = stringCodec(Logo.from)
implicit val longname: StringCodec[Longname] = stringCodec(Longname.from)
implicit val projectIri: StringCodec[ProjectIri] = stringCodec(ProjectIri.from)
implicit val selfJoin: StringCodec[SelfJoin] = booleanCodec(SelfJoin.from)
implicit val shortcode: StringCodec[Shortcode] = stringCodec(Shortcode.from)
implicit val shortname: StringCodec[Shortname] = stringCodec(Shortname.from)
implicit val sparqlEncodedString: StringCodec[SparqlEncodedString] = stringCodec(SparqlEncodedString.from)
implicit val status: StringCodec[Status] = booleanCodec(Status.from)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import zio.ZLayer
import org.knora.webapi.messages.admin.responder.sipimessages.PermissionCodeAndProjectRestrictedViewSettings
import org.knora.webapi.messages.admin.responder.sipimessages.SipiResponderResponseADMJsonProtocol.*
import org.knora.webapi.slice.admin.api.AdminPathVariables.projectShortcode
import org.knora.webapi.slice.admin.api.Codecs.TapirCodec.sparqlEncodedString
import org.knora.webapi.slice.admin.api.FilesPathVar.filename
import org.knora.webapi.slice.common.api.BaseEndpoints
import org.knora.webapi.slice.common.domain.SparqlEncodedString
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import zio.ZLayer

import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.ShortcodeIdentifier
import org.knora.webapi.messages.admin.responder.sipimessages.PermissionCodeAndProjectRestrictedViewSettings
import org.knora.webapi.responders.admin.AssetPermissionResponder
import org.knora.webapi.responders.admin.AssetPermissionsResponder
import org.knora.webapi.slice.admin.domain.model.User
import org.knora.webapi.slice.common.api.HandlerMapper
import org.knora.webapi.slice.common.api.SecuredEndpointAndZioHandler
import org.knora.webapi.slice.common.domain.SparqlEncodedString

final case class FilesEndpointsHandler(
filesEndpoints: FilesEndpoints,
sipiResponder: AssetPermissionResponder,
assetPermissionsResponder: AssetPermissionsResponder,
mapper: HandlerMapper
) {

Expand All @@ -28,7 +28,7 @@ final case class FilesEndpointsHandler(
](
filesEndpoints.getAdminFilesShortcodeFileIri,
(user: User) => { case (shortcode: ShortcodeIdentifier, filename: SparqlEncodedString) =>
sipiResponder.getPermissionCodeAndProjectRestrictedViewSettings(shortcode, filename.value, user)
assetPermissionsResponder.getPermissionCodeAndProjectRestrictedViewSettings(shortcode, filename.value, user)
}
)

Expand Down
Loading
Loading