Skip to content

Commit

Permalink
fix: Fix project name, description and keywords value objects (2892) (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
mpro7 authored Nov 1, 2023
1 parent 8b08c74 commit d1388bc
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 128 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ object ITTestDataFactory {
.make(name)
.getOrElse(throw new IllegalArgumentException(s"Invalid Name $name."))

def projectDescription(description: Seq[V2.StringLiteralV2]): ProjectDescription =
ProjectDescription
def projectDescription(description: Seq[V2.StringLiteralV2]): Description =
Description
.make(description)
.getOrElse(throw new IllegalArgumentException(s"Invalid ProjectDescription $description."))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender {
shortname = Shortname.make("newproject").fold(error => throw error.head, value => value),
shortcode = Shortcode.make(shortcode).fold(error => throw error.head, value => value), // lower case
longname = Name.make(Some("project longname")).fold(error => throw error.head, value => value),
description = ProjectDescription
description = Description
.make(Seq(V2.StringLiteralV2(value = "project description", language = Some("en"))))
.fold(error => throw error.head, value => value),
keywords = Keywords.make(Seq("keywords")).fold(error => throw error.head, value => value),
Expand Down Expand Up @@ -265,7 +265,7 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender {
shortname = Shortname.make("newproject2").fold(error => throw error.head, value => value),
shortcode = Shortcode.make("1112").fold(error => throw error.head, value => value), // lower case
longname = Some(Name.make("project longname").fold(error => throw error.head, value => value)),
description = ProjectDescription
description = Description
.make(Seq(V2.StringLiteralV2(value = "project description", language = Some("en"))))
.fold(error => throw error.head, value => value),
keywords = Keywords.make(Seq("keywords")).fold(error => throw error.head, value => value),
Expand Down Expand Up @@ -297,7 +297,7 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender {
shortname = Shortname.make("project_with_char").fold(error => throw error.head, value => value),
shortcode = Shortcode.make("1312").fold(error => throw error.head, value => value), // lower case
longname = Name.make(Some(longnameWithSpecialCharacter)).fold(error => throw error.head, value => value),
description = ProjectDescription
description = Description
.make(Seq(V2.StringLiteralV2(value = descriptionWithSpecialCharacter, language = Some("en"))))
.fold(error => throw error.head, value => value),
keywords = Keywords.make(Seq(keywordWithSpecialCharacter)).fold(error => throw error.head, value => value),
Expand Down Expand Up @@ -329,7 +329,7 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender {
shortname = Shortname.make("newproject").fold(error => throw error.head, value => value),
shortcode = Shortcode.make("111C").fold(error => throw error.head, value => value), // lower case
longname = Name.make(Some("project longname")).fold(error => throw error.head, value => value),
description = ProjectDescription
description = Description
.make(Seq(V2.StringLiteralV2(value = "project description", language = Some("en"))))
.fold(error => throw error.head, value => value),
keywords = Keywords.make(Seq("keywords")).fold(error => throw error.head, value => value),
Expand All @@ -349,7 +349,7 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender {
shortname = Shortname.make("newproject3").fold(error => throw error.head, value => value),
shortcode = Shortcode.make("111C").fold(error => throw error.head, value => value), // lower case
longname = Name.make(Some("project longname")).fold(error => throw error.head, value => value),
description = ProjectDescription
description = Description
.make(Seq(V2.StringLiteralV2(value = "project description", language = Some("en"))))
.fold(error => throw error.head, value => value),
keywords = Keywords.make(Seq("keywords")).fold(error => throw error.head, value => value),
Expand Down
135 changes: 66 additions & 69 deletions webapi/src/main/scala/dsp/valueobjects/Project.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,36 +34,46 @@ object Project {
* @param shortname string to be checked.
* @return the same string.
*/
def validateAndEscapeProjectShortname(shortname: String): Option[String] =
private def validateAndEscapeProjectShortname(shortname: String): Option[String] =
shortnameRegex
.findFirstIn(shortname)
.flatMap(Iri.toSparqlEncodedString)

// TODO-mpro: longname, description, keywords, logo are missing enhanced validation
object ErrorMessages {
val ShortcodeMissing = "Shortcode cannot be empty."
val ShortcodeInvalid = (v: String) => s"Invalid project shortcode: $v"
val ShortnameMissing = "Shortname cannot be empty."
val ShortnameInvalid = (v: String) => s"Shortname is invalid: $v"
val NameMissing = "Name cannot be empty."
val NameInvalid = "Name must be 3 to 256 characters long."
val ProjectDescriptionMissing = "Description cannot be empty."
val ProjectDescriptionInvalid = "Description must be 3 to 40960 characters long."
val KeywordsMissing = "Keywords cannot be empty."
val KeywordsInvalid = "Keywords must be 3 to 64 characters long."
val LogoMissing = "Logo cannot be empty."
}

/**
* Project Shortcode value object.
*/
sealed abstract case class Shortcode private (value: String)
object Shortcode { self =>
implicit val decoder: JsonDecoder[Shortcode] = JsonDecoder[String].mapOrFail { case value =>
Shortcode.make(value).toEitherWith(e => e.head.getMessage())
implicit val decoder: JsonDecoder[Shortcode] = JsonDecoder[String].mapOrFail { value =>
Shortcode.make(value).toEitherWith(e => e.head.getMessage)
}
implicit val encoder: JsonEncoder[Shortcode] =
JsonEncoder[String].contramap((shortcode: Shortcode) => shortcode.value)

def unsafeFrom(str: String) = make(str)
.getOrElse(throw new IllegalArgumentException(s"Invalid project shortcode: $str"))
.getOrElse(throw new IllegalArgumentException(ErrorMessages.ShortcodeInvalid(str)))

def make(value: String): Validation[ValidationException, Shortcode] =
if (value.isEmpty) {
Validation.fail(ValidationException(ProjectErrorMessages.ShortcodeMissing))
} else {
if (value.isEmpty) Validation.fail(ValidationException(ErrorMessages.ShortcodeMissing))
else
ProjectIDRegex.matches(value.toUpperCase) match {
case false => Validation.fail(ValidationException(ProjectErrorMessages.ShortcodeInvalid(value)))
case false => Validation.fail(ValidationException(ErrorMessages.ShortcodeInvalid(value)))
case true => Validation.succeed(new Shortcode(value.toUpperCase) {})
}
}
}

/**
Expand All @@ -78,11 +88,11 @@ object Project {
JsonEncoder[String].contramap((shortname: Shortname) => shortname.value)

def make(value: String): Validation[ValidationException, Shortname] =
if (value.isEmpty) Validation.fail(ValidationException(ProjectErrorMessages.ShortnameMissing))
if (value.isEmpty) Validation.fail(ValidationException(ErrorMessages.ShortnameMissing))
else
Validation
.fromOption(validateAndEscapeProjectShortname(value))
.mapError(_ => ValidationException(ProjectErrorMessages.ShortnameInvalid(value)))
.mapError(_ => ValidationException(ErrorMessages.ShortnameInvalid(value)))
.map(new Shortname(_) {})

def make(value: Option[String]): Validation[ValidationException, Option[Shortname]] =
Expand All @@ -96,21 +106,20 @@ object Project {
* Project Name value object.
* (Formerly `Longname`)
*/
// TODO-BL: [domain-model] this should be multi-lang-string, I suppose; needs real validation once value constraints are defined
sealed abstract case class Name private (value: String)
object Name { self =>
implicit val decoder: JsonDecoder[Name] = JsonDecoder[String].mapOrFail { case value =>
Name.make(value).toEitherWith(e => e.head.getMessage())
implicit val decoder: JsonDecoder[Name] = JsonDecoder[String].mapOrFail { value =>
Name.make(value).toEitherWith(e => e.head.getMessage)
}
implicit val encoder: JsonEncoder[Name] =
JsonEncoder[String].contramap((name: Name) => name.value)

private def isLengthCorrect(name: String): Boolean = name.length > 2 && name.length < 257

def make(value: String): Validation[ValidationException, Name] =
if (value.isEmpty) {
Validation.fail(ValidationException(ProjectErrorMessages.NameMissing))
} else {
Validation.succeed(new Name(value) {})
}
if (value.isEmpty) Validation.fail(ValidationException(ErrorMessages.NameMissing))
else if (!isLengthCorrect(value)) Validation.fail(ValidationException(ErrorMessages.NameInvalid))
else Validation.succeed(new Name(value) {})

def make(value: Option[String]): Validation[ValidationException, Option[Name]] =
value match {
Expand All @@ -120,27 +129,27 @@ object Project {
}

/**
* ProjectDescription value object.
* Description value object.
*/
// TODO-BL: [domain-model] should probably be MultiLangString; should probably be called `Description` as it's clear that it's part of Project
// ATM it can't be changed to MultiLangString, because that has the language tag required, whereas in V2, it's currently optional, so this would be a breaking change.
sealed abstract case class ProjectDescription private (value: Seq[V2.StringLiteralV2]) // make it plural
object ProjectDescription { self =>
implicit val decoder: JsonDecoder[ProjectDescription] = JsonDecoder[Seq[V2.StringLiteralV2]].mapOrFail {
case value =>
ProjectDescription.make(value).toEitherWith(e => e.head.getMessage())
sealed abstract case class Description private (value: Seq[V2.StringLiteralV2])
object Description { self =>
implicit val decoder: JsonDecoder[Description] = JsonDecoder[Seq[V2.StringLiteralV2]].mapOrFail { value =>
Description.make(value).toEitherWith(e => e.head.getMessage)
}
implicit val encoder: JsonEncoder[ProjectDescription] =
JsonEncoder[Seq[V2.StringLiteralV2]].contramap((description: ProjectDescription) => description.value)

def make(value: Seq[V2.StringLiteralV2]): Validation[ValidationException, ProjectDescription] =
if (value.isEmpty) {
Validation.fail(ValidationException(ProjectErrorMessages.ProjectDescriptionsMissing))
} else {
Validation.succeed(new ProjectDescription(value) {})
}
implicit val encoder: JsonEncoder[Description] =
JsonEncoder[Seq[V2.StringLiteralV2]].contramap((description: Description) => description.value)

private def isLengthCorrect(descriptionToCheck: Seq[V2.StringLiteralV2]): Boolean = {
val checked = descriptionToCheck.filter(d => d.value.length > 2 && d.value.length < 40961)
descriptionToCheck == checked
}

def make(value: Seq[V2.StringLiteralV2]): Validation[ValidationException, Description] =
if (value.isEmpty) Validation.fail(ValidationException(ErrorMessages.ProjectDescriptionMissing))
else if (!isLengthCorrect(value)) Validation.fail(ValidationException(ErrorMessages.ProjectDescriptionInvalid))
else Validation.succeed(new Description(value) {})

def make(value: Option[Seq[V2.StringLiteralV2]]): Validation[ValidationException, Option[ProjectDescription]] =
def make(value: Option[Seq[V2.StringLiteralV2]]): Validation[ValidationException, Option[Description]] =
value match {
case Some(v) => self.make(v).map(Some(_))
case None => Validation.succeed(None)
Expand All @@ -152,18 +161,21 @@ object Project {
*/
sealed abstract case class Keywords private (value: Seq[String])
object Keywords { self =>
implicit val decoder: JsonDecoder[Keywords] = JsonDecoder[Seq[String]].mapOrFail { case value =>
Keywords.make(value).toEitherWith(e => e.head.getMessage())
implicit val decoder: JsonDecoder[Keywords] = JsonDecoder[Seq[String]].mapOrFail { value =>
Keywords.make(value).toEitherWith(e => e.head.getMessage)
}
implicit val encoder: JsonEncoder[Keywords] =
JsonEncoder[Seq[String]].contramap((keywords: Keywords) => keywords.value)

private def isLengthCorrect(keywordsToCheck: Seq[String]): Boolean = {
val checked = keywordsToCheck.filter(k => k.length > 2 && k.length < 65)
keywordsToCheck == checked
}

def make(value: Seq[String]): Validation[ValidationException, Keywords] =
if (value.isEmpty) {
Validation.fail(ValidationException(ProjectErrorMessages.KeywordsMissing))
} else {
Validation.succeed(new Keywords(value) {})
}
if (value.isEmpty) Validation.fail(ValidationException(ErrorMessages.KeywordsMissing))
else if (!isLengthCorrect(value)) Validation.fail(ValidationException(ErrorMessages.KeywordsInvalid))
else Validation.succeed(new Keywords(value) {})

def make(value: Option[Seq[String]]): Validation[ValidationException, Option[Keywords]] =
value match {
Expand All @@ -177,16 +189,16 @@ object Project {
*/
sealed abstract case class Logo private (value: String)
object Logo { self =>
implicit val decoder: JsonDecoder[Logo] = JsonDecoder[String].mapOrFail { case value =>
Logo.make(value).toEitherWith(e => e.head.getMessage())
implicit val decoder: JsonDecoder[Logo] = JsonDecoder[String].mapOrFail { value =>
Logo.make(value).toEitherWith(e => e.head.getMessage)
}
implicit val encoder: JsonEncoder[Logo] =
JsonEncoder[String].contramap((logo: Logo) => logo.value)

def make(value: String): Validation[ValidationException, Logo] =
if (value.isEmpty) {
Validation.fail(ValidationException(ProjectErrorMessages.LogoMissing))
} else {
if (value.isEmpty)
Validation.fail(ValidationException(ErrorMessages.LogoMissing))
else {
Validation.succeed(new Logo(value) {})
}
def make(value: Option[String]): Validation[ValidationException, Option[Logo]] =
Expand All @@ -201,8 +213,8 @@ object Project {
*/
sealed abstract case class ProjectSelfJoin private (value: Boolean)
object ProjectSelfJoin { self =>
implicit val decoder: JsonDecoder[ProjectSelfJoin] = JsonDecoder[Boolean].mapOrFail { case value =>
ProjectSelfJoin.make(value).toEitherWith(e => e.head.getMessage())
implicit val decoder: JsonDecoder[ProjectSelfJoin] = JsonDecoder[Boolean].mapOrFail { value =>
ProjectSelfJoin.make(value).toEitherWith(e => e.head.getMessage)
}
implicit val encoder: JsonEncoder[ProjectSelfJoin] =
JsonEncoder[Boolean].contramap((selfJoin: ProjectSelfJoin) => selfJoin.value)
Expand All @@ -226,8 +238,8 @@ object Project {
val deleted = new ProjectStatus(false) {}
val active = new ProjectStatus(true) {}

implicit val decoder: JsonDecoder[ProjectStatus] = JsonDecoder[Boolean].mapOrFail { case value =>
ProjectStatus.make(value).toEitherWith(e => e.head.getMessage())
implicit val decoder: JsonDecoder[ProjectStatus] = JsonDecoder[Boolean].mapOrFail { value =>
ProjectStatus.make(value).toEitherWith(e => e.head.getMessage)
}
implicit val encoder: JsonEncoder[ProjectStatus] =
JsonEncoder[Boolean].contramap((status: ProjectStatus) => status.value)
Expand All @@ -242,18 +254,3 @@ object Project {
}
}
}

object ProjectErrorMessages {
val ShortcodeMissing = "Shortcode cannot be empty."
val ShortcodeInvalid = (v: String) => s"Shortcode is invalid: $v"
val ShortnameMissing = "Shortname cannot be empty."
val ShortnameInvalid = (v: String) => s"Shortname is invalid: $v"
val NameMissing = "Name cannot be empty."
val NameInvalid = (v: String) => s"Name is invalid: $v"
val ProjectDescriptionsMissing = "Description cannot be empty."
val ProjectDescriptionsInvalid = (v: String) => s"Description is invalid: $v"
val KeywordsMissing = "Keywords cannot be empty."
val KeywordsInvalid = (v: String) => s"Keywords are invalid: $v"
val LogoMissing = "Logo cannot be empty."
val LogoInvalid = (v: String) => s"Logo is invalid: $v"
}
Original file line number Diff line number Diff line change
Expand Up @@ -384,8 +384,6 @@ case class ListNodeGetResponseADM(node: NodeADM) extends ListItemGetResponseADM(

/**
* Provides basic information about any node (root or child) without it's children.
*
* @param nodeinfo the basic information about a node.
*/
abstract class NodeInfoGetResponseADM() extends KnoraResponseADM with ListADMJsonProtocol

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ object ProjectsEndpointsRequests {
shortname: Shortname,
shortcode: Shortcode,
longname: Option[Name] = None,
description: ProjectDescription,
description: Description,
keywords: Keywords,
logo: Option[Logo] = None,
status: ProjectStatus,
Expand All @@ -31,7 +31,7 @@ object ProjectsEndpointsRequests {
final case class ProjectUpdateRequest(
shortname: Option[Shortname] = None,
longname: Option[Name] = None,
description: Option[ProjectDescription] = None,
description: Option[Description] = None,
keywords: Option[Keywords] = None,
logo: Option[Logo] = None,
status: Option[ProjectStatus] = None,
Expand Down
Loading

0 comments on commit d1388bc

Please sign in to comment.