Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
refactor(projects): cleaner value objects usage in addProject route (D…
  • Loading branch information
subotic committed Oct 13, 2021
1 parent c79c194 commit 32b9e49
Show file tree
Hide file tree
Showing 15 changed files with 224 additions and 231 deletions.
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(
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)

/**
* 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,7 +29,6 @@ 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
Expand All @@ -48,6 +45,11 @@ import org.knora.webapi.messages.admin.responder.valueObjects.{
Status
}
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,34 +151,28 @@ 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 {
requestingUser <- getUserADM(
requestContext = requestContext,
featureFactoryConfig = featureFactoryConfig
)
projectCreatePayload <- toFuture(projectCreatePayload)
requestingUser <- getUserADM(requestContext, featureFactoryConfig)
} yield ProjectCreateRequestADM(
createRequest = projectCreatePayload,
featureFactoryConfig = featureFactoryConfig,
Expand Down

0 comments on commit 32b9e49

Please sign in to comment.