From fa6f7075768b57b22008b0dbd6b9a390327cf055 Mon Sep 17 00:00:00 2001 From: Pat Garrity Date: Sun, 23 Mar 2025 22:16:53 -0500 Subject: [PATCH] WIP: Model Implementation --- adr/2025-03-23-database-design.md.md | 2 +- .../gs/maintainer/model/ArtifactGroup.scala | 40 +++++++++++++ .../gs/maintainer/model/ArtifactName.scala | 41 ++++++++++++++ .../gs/maintainer/model/ModelError.scala | 37 ++++++++++++ .../scala/gs/maintainer/model/Project.scala | 9 +++ .../gs/maintainer/model/RegisteredAt.scala | 56 +++++++++++++++++++ 6 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 modules/model/src/main/scala/gs/maintainer/model/ArtifactGroup.scala create mode 100644 modules/model/src/main/scala/gs/maintainer/model/ArtifactName.scala create mode 100644 modules/model/src/main/scala/gs/maintainer/model/ModelError.scala create mode 100644 modules/model/src/main/scala/gs/maintainer/model/Project.scala create mode 100644 modules/model/src/main/scala/gs/maintainer/model/RegisteredAt.scala diff --git a/adr/2025-03-23-database-design.md.md b/adr/2025-03-23-database-design.md.md index 89bfe14..bf9ef80 100644 --- a/adr/2025-03-23-database-design.md.md +++ b/adr/2025-03-23-database-design.md.md @@ -47,7 +47,7 @@ CREATE TABLE registered_project( group TEXT NOT NULL, name TEXT NOT NULL, registered_at TIMESTAMPTZ NOT NULL, - git_url TIMESTAMPTZ NOT NULL + git_url TEXT NOT NULL ); ``` diff --git a/modules/model/src/main/scala/gs/maintainer/model/ArtifactGroup.scala b/modules/model/src/main/scala/gs/maintainer/model/ArtifactGroup.scala new file mode 100644 index 0000000..bb615f1 --- /dev/null +++ b/modules/model/src/main/scala/gs/maintainer/model/ArtifactGroup.scala @@ -0,0 +1,40 @@ +package gs.maintainer.model + +import cats.Eq +import cats.Show + +/** + * Represents the _group_ part of the artifact coordinates. + */ +opaque type ArtifactGroup = String + +/** + * Represents the _group_ part of the artifact coordinates. + */ +object ArtifactGroup: + + /** + * Instantiate a new [[ArtifactGroup]] based on the given candidate string. + * + * @param candidate The string to validate. + * @return The new [[ArtifactGroup]], or an error if the candidate was invalid. + */ + def validate(candidate: String): Either[ModelError, ArtifactGroup] = + if candidate.isBlank() then + Left(ModelError.InvalidArtifactGroup(candidate)) + else + Right(candidate) + + given CanEqual[ArtifactGroup, ArtifactGroup] = CanEqual.derived + + given Eq[ArtifactGroup] = (x, y) => x == y + + given Show[ArtifactGroup] = _.toString() + + extension (ag: ArtifactGroup) + /** + * @return The underlying string for this [[ArtifactGroup]]. + */ + def unwrap(): String = ag + +end ArtifactGroup diff --git a/modules/model/src/main/scala/gs/maintainer/model/ArtifactName.scala b/modules/model/src/main/scala/gs/maintainer/model/ArtifactName.scala new file mode 100644 index 0000000..505ebb1 --- /dev/null +++ b/modules/model/src/main/scala/gs/maintainer/model/ArtifactName.scala @@ -0,0 +1,41 @@ +package gs.maintainer.model + +import cats.Eq +import cats.Show + +/** + * Represents the _name_ part of the artifact coordinates. + */ +opaque type ArtifactName = String + +/** + * Represents the _name_ part of the artifact coordinates. + */ +object ArtifactName: + + /** + * Instantiate a new [[ArtifactName]] based on the given candidate string. + * + * @param candidate The string to validate. + * @return The new [[ArtifactName]], or an error if the candidate was invalid. + */ + def validate(candidate: String): Either[ModelError, ArtifactName] = + if candidate.isBlank() then + Left(ModelError.InvalidArtifactName(candidate)) + else + Right(candidate) + + given CanEqual[ArtifactName, ArtifactName] = CanEqual.derived + + given Eq[ArtifactName] = (x, y) => x == y + + given Show[ArtifactName] = _.toString() + + extension (an: ArtifactName) + /** + * @return The underlying string for this [[ArtifactName]]. + */ + def unwrap(): String = an + +end ArtifactName + diff --git a/modules/model/src/main/scala/gs/maintainer/model/ModelError.scala b/modules/model/src/main/scala/gs/maintainer/model/ModelError.scala new file mode 100644 index 0000000..fc50150 --- /dev/null +++ b/modules/model/src/main/scala/gs/maintainer/model/ModelError.scala @@ -0,0 +1,37 @@ +package gs.maintainer.model + +/** + * Base type for all model errors. + */ +sealed trait ModelError + +/** + * Enumerates all model errors. + */ +object ModelError: + + /** + * Produced when attempting to instantiate an [[ArtifactGroup]] and the + * candidate string is invalid (blank). + * + * @param candidate The candidate artifact group. + */ + case class InvalidArtifactGroup(candidate: String) extends ModelError + + /** + * Produced when attempting to instantiate an [[ArtifactName]] and the + * candidate string is invalid (blank). + * + * @param candidate The candidate artifact name. + */ + case class InvalidArtifactName(candidate: String) extends ModelError + + /** + * Produced when parsing a timestamp and the candidate string is not a + * valid timestamp emitted by `ISO_INSTANT`. + * + * @param candidate The candidate timestamp. + */ + case class InvalidTimestamp(candidate: String) extends ModelError + +end ModelError diff --git a/modules/model/src/main/scala/gs/maintainer/model/Project.scala b/modules/model/src/main/scala/gs/maintainer/model/Project.scala new file mode 100644 index 0000000..770d643 --- /dev/null +++ b/modules/model/src/main/scala/gs/maintainer/model/Project.scala @@ -0,0 +1,9 @@ +package gs.maintainer.model + +import gs.uuid.v0.UUID + +case class Project( + id: UUID, + group: ArtifactGroup, + name: ArtifactName +) diff --git a/modules/model/src/main/scala/gs/maintainer/model/RegisteredAt.scala b/modules/model/src/main/scala/gs/maintainer/model/RegisteredAt.scala new file mode 100644 index 0000000..bef90ab --- /dev/null +++ b/modules/model/src/main/scala/gs/maintainer/model/RegisteredAt.scala @@ -0,0 +1,56 @@ +package gs.maintainer.model + +import cats.Eq +import cats.Show +import java.time.Instant +import scala.util.Try + +/** + * Represents the instant a [[Project]] was registered. + */ +opaque type RegisteredAt = Instant + +/** + * Represents the instant a [[Project]] was registered. + */ +object RegisteredAt: + + /** + * Instantiate a new [[RegisteredAt]] from the given timestamp. + * + * @param value The timestamp. + * @return The new [[RegisteredAt]] instance. + */ + def apply(value: Instant): RegisteredAt = value + + /** + * Parse the given string as a [[RegisteredAt]]. This function uses the + * default parser for the `java.time.Instant` type (`ISO_INSTANT`). + * + * @param candidate The candidate string. + * @return The new [[RegisteredAt]] instance, or an error if parsing failed. + */ + def parse(candidate: String): Either[ModelError, RegisteredAt] = + Try(Instant.parse(candidate)) + .toOption + .map(Right(_)) + .getOrElse(Left(ModelError.InvalidTimestamp(candidate))) + + given CanEqual[RegisteredAt, RegisteredAt] = CanEqual.derived + + given Eq[RegisteredAt] = (x, y) => x == y + + given Show[RegisteredAt] = _.unwrap().toString() + + extension (ra: RegisteredAt) + /** + * @return The underlying `java.time.Instant`. + */ + def unwrap(): Instant = ra + + /** + * @return The epoch milliseconds represented by this timestamp. + */ + def toEpochMillis(): Long = ra.toEpochMilli() + +end RegisteredAt