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: move value objects to separate project (DEV-615) #2069

Merged
merged 16 commits into from Jun 2, 2022
9 changes: 9 additions & 0 deletions build.sbt
Expand Up @@ -201,6 +201,7 @@ lazy val webapi: Project = Project(id = "webapi", base = file("webapi"))
),
buildInfoPackage := "org.knora.webapi.http.version"
)
.dependsOn(valueObjects)

lazy val webapiJavaRunOptions = Seq(
// "-showversion",
Expand Down Expand Up @@ -282,3 +283,11 @@ lazy val schemaRepoSearchService = project
testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework"))
)
.dependsOn(schemaRepo)

lazy val valueObjects = project
.in(file("dsp-value-objects"))
.settings(
name := "valueObjects",
libraryDependencies ++= Dependencies.valueObjectsLibraryDependencies,
testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework"))
)
100 changes: 100 additions & 0 deletions dsp-value-objects/src/main/scala/dsp/valueobjects/Group.scala
@@ -0,0 +1,100 @@
/*
* Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors.
* SPDX-License-Identifier: Apache-2.0
*/

package dsp.valueobjects

import zio.prelude.Validation

sealed trait Group
Copy link
Collaborator

Choose a reason for hiding this comment

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

we don't need to define a trait for Group. We only discussed it in the context of IRI.

object Group {

/**
* GroupName value object.
*/
sealed abstract case class GroupName private (value: String)
object GroupName { self =>
def make(value: String): Validation[Throwable, GroupName] =
if (value.isEmpty) {
Validation.fail(V2.BadRequestException(GroupErrorMessages.GroupNameMissing))
} else {
val validatedValue = Validation(
V2IriValidation.toSparqlEncodedString(
value,
throw V2.BadRequestException(GroupErrorMessages.GroupNameInvalid)
)
)

validatedValue.map(new GroupName(_) {})
}

def make(value: Option[String]): Validation[Throwable, Option[GroupName]] =
value match {
case Some(v) => self.make(v).map(Some(_))
case None => Validation.succeed(None)
}
}

/**
* GroupDescriptions value object.

Choose a reason for hiding this comment

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

I'm not sure anymore if we discussed the naming of this already. But I think that "description" should be singular. There is only one description per group (and all other entities), just possibly in multiple languages. What do you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is indeed a little bit misleading. Since value objects are going to be refactored in next step, this can be a subject of discussion.

Copy link
Collaborator

Choose a reason for hiding this comment

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

agree, only one description.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fine, but technically it is still the Seq of values, which means that the way how the translations/internationalisation is implemented is wrong. Nothing stops user from adding more than one description in the same language.

*/
sealed abstract case class GroupDescriptions private (value: Seq[V2.StringLiteralV2])
object GroupDescriptions { self =>
def make(value: Seq[V2.StringLiteralV2]): Validation[Throwable, GroupDescriptions] =
if (value.isEmpty) {
Validation.fail(V2.BadRequestException(GroupErrorMessages.GroupDescriptionsMissing))
} else {
val validatedDescriptions = Validation(value.map { description =>
val validatedDescription =
V2IriValidation.toSparqlEncodedString(
description.value,
throw V2.BadRequestException(GroupErrorMessages.GroupDescriptionsInvalid)
)
V2.StringLiteralV2(value = validatedDescription, language = description.language)
})
validatedDescriptions.map(new GroupDescriptions(_) {})
}

def make(value: Option[Seq[V2.StringLiteralV2]]): Validation[Throwable, Option[GroupDescriptions]] =
value match {
case Some(v) => self.make(v).map(Some(_))
case None => Validation.succeed(None)
}
}

/**
* GroupStatus value object.
*/
sealed abstract case class GroupStatus private (value: Boolean)
object GroupStatus { self =>
def make(value: Boolean): Validation[Throwable, GroupStatus] =
Validation.succeed(new GroupStatus(value) {})
def make(value: Option[Boolean]): Validation[Throwable, Option[GroupStatus]] =
value match {
case Some(v) => self.make(v).map(Some(_))
case None => Validation.succeed(None)
}
}

/**
* GroupSelfJoin value object.
*/
sealed abstract case class GroupSelfJoin private (value: Boolean)
object GroupSelfJoin { self =>
def make(value: Boolean): Validation[Throwable, GroupSelfJoin] =
Validation.succeed(new GroupSelfJoin(value) {})
def make(value: Option[Boolean]): Validation[Throwable, Option[GroupSelfJoin]] =
value match {
case Some(v) => self.make(v).map(Some(_))
case None => Validation.succeed(None)
}
}
}

object GroupErrorMessages {
val GroupNameMissing = "Group name cannot be empty."
val GroupNameInvalid = "Group name is invalid."
val GroupDescriptionsMissing = "Group description cannot be empty."
val GroupDescriptionsInvalid = "Group description is invalid."
}
157 changes: 157 additions & 0 deletions dsp-value-objects/src/main/scala/dsp/valueobjects/Iri.scala
@@ -0,0 +1,157 @@
/*
* Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors.
* SPDX-License-Identifier: Apache-2.0
*/

package dsp.valueobjects

import zio.prelude.Validation

sealed trait Iri
object Iri {

/**
* GroupIri value object.
*/
sealed abstract case class GroupIri private (value: String)
Copy link
Collaborator

Choose a reason for hiding this comment

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

all IRI case classes need to extend the IRI sealed trait, or it will not work as intended.

object GroupIri { self =>
def make(value: String): Validation[Throwable, GroupIri] =
if (value.isEmpty) {
Validation.fail(V2.BadRequestException(IriErrorMessages.GroupIriMissing))
} else {
val isUuid: Boolean = V2UuidValidation.hasUuidLength(value.split("/").last)

if (!V2IriValidation.isKnoraGroupIriStr(value)) {
Validation.fail(V2.BadRequestException(IriErrorMessages.GroupIriInvalid))
} else if (isUuid && !V2UuidValidation.isUuidVersion4Or5(value)) {
Validation.fail(V2.BadRequestException(IriErrorMessages.UuidVersionInvalid))
} else {
val validatedValue = Validation(
V2IriValidation.validateAndEscapeIri(value, throw V2.BadRequestException(IriErrorMessages.GroupIriInvalid))
)

validatedValue.map(new GroupIri(_) {})
}
}

def make(value: Option[String]): Validation[Throwable, Option[GroupIri]] =
value match {
case Some(v) => self.make(v).map(Some(_))
case None => Validation.succeed(None)
}
}

/**
* ListIri value object.
*/
sealed abstract case class ListIri private (value: String)
object ListIri { self =>
def make(value: String): Validation[Throwable, ListIri] =
if (value.isEmpty) {
Validation.fail(V2.BadRequestException(IriErrorMessages.ListIriMissing))
} else {
val isUuid: Boolean = V2UuidValidation.hasUuidLength(value.split("/").last)

if (!V2IriValidation.isKnoraListIriStr(value)) {
Validation.fail(V2.BadRequestException(IriErrorMessages.ListIriInvalid))
} else if (isUuid && !V2UuidValidation.isUuidVersion4Or5(value)) {
Validation.fail(V2.BadRequestException(IriErrorMessages.UuidVersionInvalid))
} else {
val validatedValue = Validation(
V2IriValidation.validateAndEscapeIri(
value,
throw V2.BadRequestException(IriErrorMessages.ListIriInvalid)
)
)

validatedValue.map(new ListIri(_) {})
}
}

def make(value: Option[String]): Validation[Throwable, Option[ListIri]] =
value match {
case Some(v) => self.make(v).map(Some(_))
case None => Validation.succeed(None)
}
}

/**
* ProjectIri value object.
*/
sealed abstract case class ProjectIri private (value: String)
object ProjectIri { self =>
def make(value: String): Validation[Throwable, ProjectIri] =
if (value.isEmpty) {
Validation.fail(V2.BadRequestException(IriErrorMessages.ProjectIriMissing))
} else {
val isUuid: Boolean = V2UuidValidation.hasUuidLength(value.split("/").last)

if (!V2IriValidation.isKnoraProjectIriStr(value)) {
Validation.fail(V2.BadRequestException(IriErrorMessages.ProjectIriInvalid))
} else if (isUuid && !V2UuidValidation.isUuidVersion4Or5(value)) {
Validation.fail(V2.BadRequestException(IriErrorMessages.UuidVersionInvalid))
} else {
val validatedValue = Validation(
V2IriValidation.validateAndEscapeProjectIri(
value,
throw V2.BadRequestException(IriErrorMessages.ProjectIriInvalid)
)
)

validatedValue.map(new ProjectIri(_) {})
}
}

def make(value: Option[String]): Validation[Throwable, Option[ProjectIri]] =
value match {
case Some(v) => self.make(v).map(Some(_))
case None => Validation.succeed(None)
}
}

/**
* UserIri value object.
*/
sealed abstract case class UserIri private (value: String) extends Iri
object UserIri { self =>
def make(value: String): Validation[Throwable, UserIri] =
if (value.isEmpty) {
Validation.fail(V2.BadRequestException(IriErrorMessages.UserIriMissing))
} else {
val isUuid: Boolean = V2UuidValidation.hasUuidLength(value.split("/").last)

if (!V2IriValidation.isKnoraUserIriStr(value)) {
Validation.fail(V2.BadRequestException(IriErrorMessages.UserIriInvalid))
} else if (isUuid && !V2UuidValidation.isUuidVersion4Or5(value)) {
Validation.fail(V2.BadRequestException(IriErrorMessages.UuidVersionInvalid))
} else {
val validatedValue = Validation(
V2IriValidation.validateAndEscapeUserIri(
value,
throw V2.BadRequestException(IriErrorMessages.UserIriInvalid)
)
)

validatedValue.map(new UserIri(_) {})
}
}

def make(value: Option[String]): Validation[Throwable, Option[UserIri]] =
value match {
case Some(v) => self.make(v).map(Some(_))
case None => Validation.succeed(None)
}
}
}

object IriErrorMessages {
val GroupIriMissing = "Group IRI cannot be empty."
val GroupIriInvalid = "Group IRI is invalid."
val ListIriMissing = "List IRI cannot be empty."
val ListIriInvalid = "List IRI is invalid"
val ProjectIriMissing = "Project IRI cannot be empty."
val ProjectIriInvalid = "Project IRI is invalid."
val UserIriMissing = "User IRI cannot be empty."
val UserIriInvalid = "User IRI is invalid."
val UuidVersionInvalid = "Invalid UUID used to create IRI. Only versions 4 and 5 are supported."
}