Skip to content

Commit

Permalink
refactor: Introduce Value[A] and extract tapir and zio-json codecs (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
seakayone committed Jan 19, 2024
1 parent 78ff954 commit 9744f7b
Show file tree
Hide file tree
Showing 25 changed files with 232 additions and 222 deletions.
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
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
4 changes: 2 additions & 2 deletions webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala
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
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
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]
}
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
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
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
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
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
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
@@ -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)
}
}
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
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

0 comments on commit 9744f7b

Please sign in to comment.