Skip to content

Commit

Permalink
feat: expose GET /admin/projects/[shortname | shortcode]/{shortname |…
Browse files Browse the repository at this point in the history
… shortcode} as ZIO HTTP routes (#2365)
  • Loading branch information
irinaschubert committed Jan 4, 2023
1 parent 1b8e74b commit 9907cdf
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 116 deletions.
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)
)
)
}

0 comments on commit 9907cdf

Please sign in to comment.