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.GroupDescriptionMissing))
} else {
val validatedDescriptions = Validation(value.map { description =>
val validatedDescription =
V2IriValidation.toSparqlEncodedString(
description.value,
throw V2.BadRequestException(GroupErrorMessages.GroupDescriptionInvalid)
)
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 GroupDescriptionMissing = "Group description cannot be empty." // make it plural
val GroupDescriptionInvalid = "Group description is invalid." // make it plural
mpro7 marked this conversation as resolved.
Show resolved Hide resolved
}
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)
mpro7 marked this conversation as resolved.
Show resolved Hide resolved

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

validatedValue.map(new GroupIri(_) {})
Copy link
Collaborator

Choose a reason for hiding this comment

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

eventually it would be nice to be able to simplify these validations. It has nested conditions to the point where it gets hard to read the code. But that's for the validation epic, not for this task.

Also, I general question I have: these validation functions (like uuidHasLength() etc.), shouldn't they ideally live in the value object?
And things like UUIDs, shouldn't they themselves be value objects with their own validation?
(Again, not scope of this PR, just in terms of the design)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the comment. Value objects refactor it's planned in next steps.

}
}

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.UuidInvalid))
} else {
val validatedValue = Validation(
V2IriValidation.validateAndEscapeIri(
value,
throw V2.BadRequestException(IriErrorMessages.ListIriInvalid)
)
)

validatedValue.map(new ListIri(_) {})
Copy link
Collaborator

Choose a reason for hiding this comment

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

And again, these different IRI types share a lot of the same logic, so there probably should be a shared method for checking these things. (in a later PR :) )

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the comment. Value objects refactor it's planned in next steps.

}
}

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.UuidInvalid))
} 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.UuidInvalid))
} 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 cannot be empty."
mpro7 marked this conversation as resolved.
Show resolved Hide resolved
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 UuidInvalid = "Invalid UUID used to create IRI. Only versions 4 and 5 are supported."
mpro7 marked this conversation as resolved.
Show resolved Hide resolved
}