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(projects): cleaner value objects usage in addProject route (DEV-119) #1920

Merged
merged 7 commits into from Oct 13, 2021
9 changes: 6 additions & 3 deletions third_party/dependencies.bzl
Expand Up @@ -28,10 +28,11 @@ def dependencies():
"com.typesafe:config:1.3.3",

# ZIO
"dev.zio:zio_2.13:1.0.9",
"dev.zio:zio_2.13:2.0.0-M3",
"dev.zio:zio-json_2.13:0.1.5",
"dev.zio:zio-test_2.13:1.0.9",
"dev.zio:zio-test-junit_2.13:1.0.9",
"dev.zio:zio-test_2.13:2.0.0-M3",
"dev.zio:zio-test-junit_2.13:2.0.0-M3",
"dev.zio:zio-prelude_2.13:1.0.0-RC6",

# CORS support
"ch.megard:akka-http-cors_2.13:1.0.0",
Expand Down Expand Up @@ -186,6 +187,8 @@ BASE_TEST_DEPENDENCIES = [
"@maven//:com_typesafe_akka_akka_http_testkit_2_13",
"@maven//:com_typesafe_akka_akka_stream_2_13",
"@maven//:com_typesafe_config",
"@maven//:dev_zio_zio_2_13",
"@maven//:dev_zio_zio_prelude_2_13",
"@maven//:org_scalatest_scalatest_2_13",
"@maven//:org_scalatest_scalatest_core_2_13",
"@maven//:org_scalatest_scalatest_wordspec_2_13",
Expand Down
2 changes: 2 additions & 0 deletions webapi/src/main/scala/org/knora/webapi/messages/BUILD.bazel
Expand Up @@ -29,6 +29,8 @@ scala_library(
"@maven//:com_typesafe_scala_logging_scala_logging_2_13",
"@maven//:commons_io_commons_io",
"@maven//:commons_validator_commons_validator",
"@maven//:dev_zio_zio_2_13",
"@maven//:dev_zio_zio_prelude_2_13",
"@maven//:io_spray_spray_json_2_13",
"@maven//:javax_json_javax_json_api",
"@maven//:net_sf_saxon_Saxon_HE",
Expand Down
@@ -0,0 +1,28 @@
package org.knora.webapi.messages.admin.responder.projectsmessages

import org.knora.webapi.IRI
import org.knora.webapi.messages.admin.responder.valueObjects.{
Shortname,
Longname,
Shortcode,
Description,
Keywords,
Logo,
Status,
Selfjoin
}

/**
* Project payload
*/
final case class ProjectCreatePayloadADM(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have moved updated class to the new file specific to the create method. Does it mean we should keep things separated like that - one class per file, or actually having all payloads in ROUTEPayloadsXXX would be better idea (if there would be more)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that one RoutePayloadsADM file would be enough. There are not that many per route. There could also be a RouteValueObjects file, where the route specific value objects are defined.

Copy link
Collaborator Author

@subotic subotic Oct 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RoutePayloadsADM => ProjectPayloadsADM.scala, GroupPayloadsADM.scala, etc.
RouteValueObjects => ProjectValueObjectsADM.scala, GroupValueObjectsADM.scala, etc.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, anything that is shared, should be inside a SharedValueObjectsADM.scala to make it obvious.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was exactly my plan, but after our latest conversation about the modularisation I'm not sure if we should have shared value objects anymore.

id: Option[IRI] = None,
shortname: Shortname,
shortcode: Shortcode,
longname: Option[Longname],
description: Description,
keywords: Keywords,
logo: Option[Logo],
status: Status,
selfjoin: Selfjoin
)

This file was deleted.

Expand Up @@ -4,6 +4,7 @@ import org.knora.webapi.LanguageCodes
import org.knora.webapi.exceptions.{AssertionException, BadRequestException}
import org.knora.webapi.messages.StringFormatter
import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2
import zio.prelude.Validation

import scala.util.matching.Regex

Expand Down Expand Up @@ -136,12 +137,14 @@ sealed abstract case class Shortcode private (value: String)
object Shortcode {
val stringFormatter = StringFormatter.getGeneralInstance

def create(value: String): Either[Throwable, Shortcode] =
def make(value: String): Validation[Throwable, Shortcode] =
if (value.isEmpty) {
Left(BadRequestException("Missing shortcode"))
Validation.fail(BadRequestException("Missing shortcode"))
} else {
val shortcode: String = stringFormatter.validateProjectShortcode(value, throw AssertionException("not valid"))
Right(new Shortcode(shortcode) {})
val validatedValue: Validation[Throwable, String] = Validation(
stringFormatter.validateProjectShortcode(value, throw AssertionException("not valid"))
)
validatedValue.map(new Shortcode(_) {})
}
}

Expand All @@ -152,25 +155,32 @@ sealed abstract case class Shortname private (value: String)
object Shortname {
val stringFormatter = StringFormatter.getGeneralInstance

def create(value: String): Either[Throwable, Shortname] =
def make(value: String): Validation[Throwable, Shortname] =
if (value.isEmpty) {
Left(BadRequestException("Missing shortname"))
Validation.fail(BadRequestException("Missing shortname"))
} else {
val shortname = stringFormatter.validateAndEscapeProjectShortname(value, throw AssertionException("not valid"))
Right(new Shortname(shortname) {})
val validatedValue = Validation(
stringFormatter.validateAndEscapeProjectShortname(value, throw AssertionException("not valid"))
)
validatedValue.map(new Shortname(_) {})
}
}

/**
* Project Longname value object.
*/
sealed abstract case class Longname private (value: String)
object Longname {
def create(value: String): Either[Throwable, Longname] =
object Longname { self =>
def make(value: String): Validation[Throwable, Longname] =
if (value.isEmpty) {
Left(BadRequestException("Missing long name"))
Validation.fail(BadRequestException("Missing long name"))
} else {
Right(new Longname(value) {})
Validation.succeed(new Longname(value) {})
}
def make(value: Option[String]): Validation[Throwable, Option[Longname]] =
value match {
case None => Validation.succeed(None)
case Some(v) => self.make(v).map(Some(_))
}
}

Expand All @@ -179,24 +189,29 @@ object Longname {
*/
sealed abstract case class Keywords private (value: Seq[String])
object Keywords {
def create(value: Seq[String]): Either[Throwable, Keywords] =
def make(value: Seq[String]): Validation[Throwable, Keywords] =
if (value.isEmpty) {
Left(BadRequestException("Missing keywords"))
Validation.fail(BadRequestException("Missing keywords"))
} else {
Right(new Keywords(value) {})
Validation.succeed(new Keywords(value) {})
}
}

/**
* Project Logo value object.
*/
sealed abstract case class Logo private (value: String)
object Logo {
def create(value: String): Either[Throwable, Logo] =
object Logo { self =>
def make(value: String): Validation[Throwable, Logo] =
if (value.isEmpty) {
Left(BadRequestException("Missing logo"))
Validation.fail(BadRequestException("Missing logo"))
} else {
Right(new Logo(value) {})
Validation.succeed(new Logo(value) {})
}
def make(value: Option[String]): Validation[Throwable, Option[Logo]] =
value match {
case None => Validation.succeed(None)
case Some(v) => self.make(v).map(Some(_))
}
}

Expand All @@ -222,28 +237,28 @@ object Name {
*/
sealed abstract case class Selfjoin private (value: Boolean)
object Selfjoin {
def create(value: Boolean): Either[Throwable, Selfjoin] =
Right(new Selfjoin(value) {})
def make(value: Boolean): Validation[Throwable, Selfjoin] =
Validation.succeed(new Selfjoin(value) {})
}

/**
* Status value object.
*/
sealed abstract case class Status private (value: Boolean)
object Status {
def create(value: Boolean): Either[Throwable, Status] =
Right(new Status(value) {})
def make(value: Boolean): Validation[Throwable, Status] =
Validation.succeed(new Status(value) {})
}

/**
* Description value object.
*/
sealed abstract case class Description private (value: Seq[StringLiteralV2])
object Description {
def create(value: Seq[StringLiteralV2]): Either[Throwable, Description] =
def make(value: Seq[StringLiteralV2]): Validation[Throwable, Description] =
if (value.isEmpty) {
Left(BadRequestException("Missing description"))
Validation.fail(BadRequestException("Missing description"))
} else {
Right(new Description(value) {})
Validation.succeed(new Description(value) {})
}
}
2 changes: 2 additions & 0 deletions webapi/src/main/scala/org/knora/webapi/routing/BUILD.bazel
Expand Up @@ -32,6 +32,8 @@ scala_library(
"@maven//:com_typesafe_play_twirl_api_2_13",
"@maven//:com_typesafe_scala_logging_scala_logging_2_13",
"@maven//:commons_validator_commons_validator",
"@maven//:dev_zio_zio_2_13",
"@maven//:dev_zio_zio_prelude_2_13",
"@maven//:io_spray_spray_json_2_13",
"@maven//:io_swagger_swagger_annotations",
"@maven//:io_swagger_swagger_jaxrs",
Expand Down
13 changes: 13 additions & 0 deletions webapi/src/main/scala/org/knora/webapi/routing/KnoraRoute.scala
Expand Up @@ -41,6 +41,7 @@ import org.knora.webapi.messages.admin.responder.projectsmessages.{
}
import org.knora.webapi.messages.admin.responder.usersmessages.UserADM
import org.knora.webapi.settings.{KnoraDispatchers, KnoraSettings, KnoraSettingsImpl}
import zio.prelude.Validation

import scala.concurrent.{ExecutionContext, Future}

Expand Down Expand Up @@ -159,4 +160,16 @@ abstract class KnoraRoute(routeData: KnoraRouteData) extends KnoraRouteFactory(r
)).mapTo[ProjectGetResponseADM]
} yield projectInfoResponse.project
}

/**
* Helper method converting an [[Either]] to a [[Future]].
*/
def toFuture[A](either: Either[Throwable, A]): Future[A] = either.fold(Future.failed, Future.successful)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this (i.e. do we still want to use Either in the future and not Validation everywhere)?


/**
* Helper method converting an [[zio.prelude.Validation]] to a [[Future]].
* FIXME: only the first error is returned, which defeats the purpose, but don't know better at the moment.
*/
def toFuture[A](validation: Validation[Throwable, A]): Future[A] =
validation.fold(errors => Future.failed(errors.head), Future.successful)
}
Expand Up @@ -96,11 +96,11 @@ class GroupsRouteADM(routeData: KnoraRouteData)
id = stringFormatter
.validateAndEscapeOptionalIri(apiRequest.id, throw BadRequestException(s"Invalid group IRI")),
name = Name.create(apiRequest.name).fold(e => throw e, v => v),
descriptions = Description.create(apiRequest.descriptions).fold(e => throw e, v => v),
descriptions = Description.make(apiRequest.descriptions).fold(e => throw e.head, v => v),
project = stringFormatter
.validateAndEscapeProjectIri(apiRequest.project, throw BadRequestException(s"Invalid project IRI")),
status = Status.create(apiRequest.status).fold(e => throw e, v => v),
selfjoin = Selfjoin.create(apiRequest.selfjoin).fold(e => throw e, v => v)
status = Status.make(apiRequest.status).fold(e => throw e.head, v => v),
selfjoin = Selfjoin.make(apiRequest.selfjoin).fold(e => throw e.head, v => v)
)

val requestMessage = for {
Expand Down
Expand Up @@ -19,8 +19,6 @@

package org.knora.webapi.routing.admin

import java.nio.file.Files
import java.util.UUID
import akka.Done
import akka.http.scaladsl.model.headers.{ContentDispositionTypes, `Content-Disposition`}
import akka.http.scaladsl.model.{ContentTypes, HttpEntity}
Expand All @@ -31,23 +29,18 @@ import akka.stream.IOResult
import akka.stream.scaladsl.{FileIO, Source}
import akka.util.ByteString
import io.swagger.annotations._
import javax.ws.rs.Path
import org.knora.webapi.IRI
import org.knora.webapi.annotation.ApiMayChange
import org.knora.webapi.exceptions.BadRequestException
import org.knora.webapi.feature.FeatureFactoryConfig
import org.knora.webapi.messages.admin.responder.projectsmessages._
import org.knora.webapi.messages.admin.responder.valueObjects.{
Description,
Keywords,
Logo,
Longname,
Selfjoin,
Shortcode,
Shortname,
Status
}
import org.knora.webapi.messages.admin.responder.valueObjects._
subotic marked this conversation as resolved.
Show resolved Hide resolved
import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData, RouteUtilADM}
import zio.prelude.Validation

import java.nio.file.Files
import java.util.UUID
import javax.ws.rs.Path
import scala.concurrent.Future
import scala.util.Try

Expand Down Expand Up @@ -149,30 +142,27 @@ class ProjectsRouteADM(routeData: KnoraRouteData)
private def addProject(featureFactoryConfig: FeatureFactoryConfig): Route = path(ProjectsBasePath) {
post {
entity(as[CreateProjectApiRequestADM]) { apiRequest => requestContext =>
val maybeLongname: Option[Longname] = apiRequest.longname match {
case Some(value) => Some(Longname.create(value).fold(error => throw error, value => value))
case None => None
}

val maybeLogo: Option[Logo] = apiRequest.logo match {
case Some(value) => Some(Logo.create(value).fold(error => throw error, value => value))
case None => None
}

val projectCreatePayload: ProjectCreatePayloadADM =
ProjectCreatePayloadADM.create(
id = stringFormatter
.validateAndEscapeOptionalProjectIri(apiRequest.id, throw BadRequestException(s"Invalid project IRI")),
shortname = Shortname.create(apiRequest.shortname).fold(error => throw error, value => value),
shortcode = Shortcode.create(apiRequest.shortcode).fold(error => throw error, value => value),
longname = maybeLongname,
description = Description.create(apiRequest.description).fold(error => throw error, value => value),
keywords = Keywords.create(apiRequest.keywords).fold(error => throw error, value => value),
logo = maybeLogo,
status = Status.create(apiRequest.status).fold(error => throw error, value => value),
selfjoin = Selfjoin.create(apiRequest.selfjoin).fold(error => throw error, value => value)
// zio prelude: validation
val id = Validation(
stringFormatter
.validateAndEscapeOptionalProjectIri(apiRequest.id, throw BadRequestException(s"Invalid project IRI"))
)
val shortname = Shortname.make(apiRequest.shortname)
val shortcode = Shortcode.make(apiRequest.shortcode)
val longname = Longname.make(apiRequest.longname)
val description = Description.make(apiRequest.description)
val keywords = Keywords.make(apiRequest.keywords)
val logo = Logo.make(apiRequest.logo)
val status = Status.make(apiRequest.status)
val selfjoin = Selfjoin.make(apiRequest.selfjoin)

val projectCreatePayload: Validation[Throwable, ProjectCreatePayloadADM] =
Validation.validateWith(id, shortname, shortcode, longname, description, keywords, logo, status, selfjoin)(
ProjectCreatePayloadADM
)

val requestMessage: Future[ProjectCreateRequestADM] = for {
projectCreatePayload <- toFuture(projectCreatePayload)
requestingUser <- getUserADM(
requestContext = requestContext,
featureFactoryConfig = featureFactoryConfig
subotic marked this conversation as resolved.
Show resolved Hide resolved
Expand Down