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

feat: expose GET /admin/projects/[shortname | shortcode]/{shortname | shortcode} as ZIO HTTP routes (DEV-1587) #2365

Merged
merged 8 commits into from Jan 4, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -3,30 +3,13 @@ import zio.Task
import zio.URLayer
import zio.ZLayer

import org.knora.webapi.IRI
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetRequestADM
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetResponseADM
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM
import org.knora.webapi.responders.ActorToZioBridge

final case class RestProjectsService(bridge: ActorToZioBridge) {

/**
* Finds the project by an [[IRI]] and returns the information as a [[ProjectGetResponseADM]].
*
* @param projectIri an [[IRI]] identifying the project
* @return
* '''success''': information about the project as a [[ProjectGetResponseADM]]
*
* '''error''': [[dsp.errors.NotFoundException]] when no project for the given IRI can be found
* [[dsp.errors.ValidationException]] if the given `projectIri` is invalid
*/
def getSingleProjectADMRequest(projectIri: IRI): Task[ProjectGetResponseADM] =
ProjectIdentifierADM.IriIdentifier
.fromString(projectIri)
.toZIO
.flatMap(getSingleProjectADMRequest(_))

/**
* Finds the project by its [[ProjectIdentifierADM]] and returns the information as a [[ProjectGetResponseADM]].
*
Expand Down
Expand Up @@ -16,9 +16,9 @@ object RouteUtilZ {
*
* @param value The value in utf-8 to be url decoded
* @param errorMsg Custom error message for the error type
* @return ```success``` the decoded string
* @return '''success''' the decoded string
*
* ```failure``` A [[BadRequestException]] with the `errorMsg`
* '''failure''' A [[BadRequestException]] with the `errorMsg`
*/
def urlDecode(value: String, errorMsg: String = ""): Task[String] =
ZIO
Expand Down
@@ -1,13 +1,16 @@
package org.knora.webapi.routing.admin

import zhttp.http._
import zio.Task
import zio.URLayer
import zio.ZLayer

import dsp.errors.BadRequestException
import dsp.errors.InternalServerException
import dsp.errors.RequestRejectedException
import org.knora.webapi.config.AppConfig
import org.knora.webapi.http.handler.ExceptionHandlerZ
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM._
import org.knora.webapi.responders.admin.RestProjectsService
import org.knora.webapi.routing.RouteUtilZ

Expand All @@ -18,21 +21,34 @@ final case class ProjectsRouteZ(

val route: HttpApp[Any, Nothing] =
Http
.collectZIO[Request] { case Method.GET -> !! / "admin" / "projects" / "iri" / iriUrlEncoded =>
getProjectByIriEncoded(iriUrlEncoded)
.collectZIO[Request] {
case Method.GET -> !! / "admin" / "projects" / "iri" / iriUrlEncoded => getProjectByIriEncoded(iriUrlEncoded)
case Method.GET -> !! / "admin" / "projects" / "shortname" / shortname => getProjectByShortname(shortname)
case Method.GET -> !! / "admin" / "projects" / "shortcode" / shortcode => getProjectByShortcode(shortcode)
}
.catchAll {
case RequestRejectedException(e) =>
ExceptionHandlerZ.exceptionToJsonHttpResponseZ(e, appConfig)
case InternalServerException(e) =>
ExceptionHandlerZ.exceptionToJsonHttpResponseZ(e, appConfig)
case RequestRejectedException(e) => ExceptionHandlerZ.exceptionToJsonHttpResponseZ(e, appConfig)
case InternalServerException(e) => ExceptionHandlerZ.exceptionToJsonHttpResponseZ(e, appConfig)
}

private def getProjectByIriEncoded(iriUrlEncoded: String) =
RouteUtilZ
.urlDecode(iriUrlEncoded, "Failed to url decode IRI parameter.")
.flatMap(projectsService.getSingleProjectADMRequest(_).map(_.toJsValue.toString()))
.map(Response.json(_))
private def getProjectByIriEncoded(iriUrlEncoded: String): Task[Response] =
for {
iriDecoded <- RouteUtilZ.urlDecode(iriUrlEncoded, s"Failed to URL decode IRI parameter $iriUrlEncoded.")
iri <- IriIdentifier.fromString(iriDecoded).toZIO
projectGetResponse <- projectsService.getSingleProjectADMRequest(identifier = iri)
} yield Response.json(projectGetResponse.toJsValue.toString())

private def getProjectByShortname(shortname: String): Task[Response] =
for {
shortnameIdentifier <- ShortnameIdentifier.fromString(shortname).toZIO.mapError(e => BadRequestException(e.msg))
projectGetResponse <- projectsService.getSingleProjectADMRequest(identifier = shortnameIdentifier)
} yield Response.json(projectGetResponse.toJsValue.toString())

private def getProjectByShortcode(shortcode: String): Task[Response] =
for {
shortcodeIdentifier <- ShortcodeIdentifier.fromString(shortcode).toZIO.mapError(e => BadRequestException(e.msg))
projectGetResponse <- projectsService.getSingleProjectADMRequest(identifier = shortcodeIdentifier)
} yield Response.json(projectGetResponse.toJsValue.toString())
}

object ProjectsRouteZ {
Expand Down
Expand Up @@ -3,13 +3,11 @@ import zio.Scope
import zio.ZIO
import zio.mock._
import zio.test.Assertion
import zio.test.SmartAssertionOps
import zio.test.Spec
import zio.test.TestEnvironment
import zio.test.ZIOSpecDefault
import zio.test.assertTrue

import dsp.errors.ValidationException
import dsp.valueobjects.Project.ShortCode
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetRequestADM
Expand Down Expand Up @@ -53,12 +51,6 @@ object RestProjectsServiceSpec extends ZIOSpecDefault {
actual <- sut.getSingleProjectADMRequest(id)
} yield assertTrue(actual == expectedResponse)
}
.provide(RestProjectsService.layer, expectSuccess),
test("given an invalid iri should return with a failure") {
for {
sut <- systemUnderTest
result <- sut.getSingleProjectADMRequest("invalid").exit
} yield assertTrue(result.is(_.failure) == ValidationException("Project IRI is invalid."))
}.provide(RestProjectsService.layer, expectNoInteraction)
.provide(RestProjectsService.layer, expectSuccess)
)
}

This file was deleted.

@@ -0,0 +1,141 @@
package org.knora.webapi.routing.admin

import zhttp.http._
import zio._
import zio.mock.Expectation
import zio.test.ZIOSpecDefault
import zio.test._

import java.net.URLEncoder

import org.knora.webapi.config.AppConfig
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetRequestADM
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetResponseADM
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM
import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2
import org.knora.webapi.responders.ActorToZioBridge
import org.knora.webapi.responders.ActorToZioBridgeMock
import org.knora.webapi.responders.admin.RestProjectsService

object ProjectRouteZSpec extends ZIOSpecDefault {

private val systemUnderTest: URIO[ProjectsRouteZ, HttpApp[Any, Nothing]] = ZIO.service[ProjectsRouteZ].map(_.route)

// Test data
private val projectIri: ProjectIdentifierADM.IriIdentifier =
ProjectIdentifierADM.IriIdentifier
.fromString("http://rdfh.ch/projects/0001")
.getOrElse(throw new IllegalArgumentException())

private val projectShortname: ProjectIdentifierADM.ShortnameIdentifier =
ProjectIdentifierADM.ShortnameIdentifier
.fromString("shortname")
.getOrElse(throw new IllegalArgumentException())

private val projectShortcode: ProjectIdentifierADM.ShortcodeIdentifier =
ProjectIdentifierADM.ShortcodeIdentifier
.fromString("AB12")
.getOrElse(throw new IllegalArgumentException())

private val basePathIri: Path = !! / "admin" / "projects" / "iri"
private val basePathShortname: Path = !! / "admin" / "projects" / "shortname"
private val basePathShortcode: Path = !! / "admin" / "projects" / "shortcode"
private val validIriUrlEncoded: String = URLEncoder.encode(projectIri.value.value, "utf-8")
private val validShortname: String = "shortname"
private val validShortcode: String = "AB12"
private val expectedRequestSuccessIri: ProjectGetRequestADM = ProjectGetRequestADM(projectIri)
private val expectedRequestSuccessShortname: ProjectGetRequestADM = ProjectGetRequestADM(projectShortname)
private val expectedRequestSuccessShortcode: ProjectGetRequestADM = ProjectGetRequestADM(projectShortcode)
private val expectedResponseSuccess: ProjectGetResponseADM =
ProjectGetResponseADM(
ProjectADM(
id = "id",
shortname = "shortname",
shortcode = "AB12",
longname = None,
description = List(StringLiteralV2("description")),
keywords = List.empty,
logo = None,
ontologies = List.empty,
status = false,
selfjoin = false
)
)

// Expectations and layers for ActorToZioBridge mock
private val commonLayers: URLayer[ActorToZioBridge, ProjectsRouteZ] =
ZLayer.makeSome[ActorToZioBridge, ProjectsRouteZ](AppConfig.test, ProjectsRouteZ.layer, RestProjectsService.layer)

private val expectMessageToProjectResponderIri: ULayer[ActorToZioBridge] =
ActorToZioBridgeMock.AskAppActor
.of[ProjectGetResponseADM]
.apply(Assertion.equalTo(expectedRequestSuccessIri), Expectation.value(expectedResponseSuccess))
.toLayer

private val expectMessageToProjectResponderShortname: ULayer[ActorToZioBridge] =
ActorToZioBridgeMock.AskAppActor
.of[ProjectGetResponseADM]
.apply(Assertion.equalTo(expectedRequestSuccessShortname), Expectation.value(expectedResponseSuccess))
.toLayer

private val expectMessageToProjectResponderShortcode: ULayer[ActorToZioBridge] =
ActorToZioBridgeMock.AskAppActor
.of[ProjectGetResponseADM]
.apply(Assertion.equalTo(expectedRequestSuccessShortcode), Expectation.value(expectedResponseSuccess))
.toLayer

private val expectNoInteractionWithProjectsResponderADM = ActorToZioBridgeMock.empty

val spec: Spec[Any, Serializable] =
suite("ProjectsRouteZSpec")(
suite("get project by IRI")(
test("given valid project iri should respond with success") {
val urlWithValidIri = URL.empty.setPath(basePathIri / validIriUrlEncoded)
for {
route <- systemUnderTest
actual <- route.apply(Request(url = urlWithValidIri)).flatMap(_.body.asString)
} yield assertTrue(actual == expectedResponseSuccess.toJsValue.toString())
}.provide(commonLayers, expectMessageToProjectResponderIri),
test("given invalid project iri should respond with bad request") {
val urlWithInvalidIri = URL.empty.setPath(basePathIri / "invalid")
for {
route <- systemUnderTest
actual <- route.apply(Request(url = urlWithInvalidIri)).map(_.status)
} yield assertTrue(actual == Status.BadRequest)
}.provide(commonLayers, expectNoInteractionWithProjectsResponderADM)
),
suite("get project by shortname")(
test("given valid project shortname should respond with success") {
val urlWithValidShortname = URL.empty.setPath(basePathShortname / validShortname)
for {
route <- systemUnderTest
actual <- route.apply(Request(url = urlWithValidShortname)).flatMap(_.body.asString)
} yield assertTrue(actual == expectedResponseSuccess.toJsValue.toString())
}.provide(commonLayers, expectMessageToProjectResponderShortname),
test("given invalid project shortname should respond with bad request") {
val urlWithInvalidShortname = URL.empty.setPath(basePathShortname / "123")
for {
route <- systemUnderTest
actual <- route.apply(Request(url = urlWithInvalidShortname)).map(_.status)
} yield assertTrue(actual == Status.BadRequest)
}.provide(commonLayers, expectNoInteractionWithProjectsResponderADM)
),
suite("get project by shortcode")(
test("given valid project shortcode should respond with success") {
val urlWithValidShortcode = URL.empty.setPath(basePathShortcode / validShortcode)
for {
route <- systemUnderTest
actual <- route.apply(Request(url = urlWithValidShortcode)).flatMap(_.body.asString)
} yield assertTrue(actual == expectedResponseSuccess.toJsValue.toString())
}.provide(commonLayers, expectMessageToProjectResponderShortcode),
test("given invalid project shortcode should respond with bad request") {
val urlWithInvalidShortcode = URL.empty.setPath(basePathShortcode / "invalid")
for {
route <- systemUnderTest
actual <- route.apply(Request(url = urlWithInvalidShortcode)).map(_.status)
} yield assertTrue(actual == Status.BadRequest)
}.provide(commonLayers, expectNoInteractionWithProjectsResponderADM)
)
)
}