diff --git a/modules/model/src/main/scala/gs/smolban/model/CreatedBy.scala b/modules/model/src/main/scala/gs/smolban/model/CreatedBy.scala new file mode 100644 index 0000000..6e4f27e --- /dev/null +++ b/modules/model/src/main/scala/gs/smolban/model/CreatedBy.scala @@ -0,0 +1,24 @@ +package gs.smolban.model + +import cats.Show +import gs.smolban.model.users.User + +opaque type CreatedBy = User.Id + +object CreatedBy: + + def apply(timestamp: User.Id): CreatedBy = timestamp + + given CanEqual[CreatedBy, CreatedBy] = CanEqual.derived + + given Show[CreatedBy] = _.toUserId().toUUID().withoutDashes() + + extension (createdAt: CreatedBy) + /** Unwrap this value. + * + * @return + * The underlying `User.Id` value. + */ + def toUserId(): User.Id = createdAt + +end CreatedBy diff --git a/modules/model/src/main/scala/gs/smolban/model/Group.scala b/modules/model/src/main/scala/gs/smolban/model/Group.scala index 3b66350..31994b6 100644 --- a/modules/model/src/main/scala/gs/smolban/model/Group.scala +++ b/modules/model/src/main/scala/gs/smolban/model/Group.scala @@ -22,6 +22,8 @@ case class Group( object Group: + given CanEqual[Group, Group] = CanEqual.derived + /** Unique identifier for a [[Group]]. This is an opaque type for a UUID. */ opaque type Id = UUID diff --git a/modules/model/src/main/scala/gs/smolban/model/Status.scala b/modules/model/src/main/scala/gs/smolban/model/Status.scala new file mode 100644 index 0000000..f62918d --- /dev/null +++ b/modules/model/src/main/scala/gs/smolban/model/Status.scala @@ -0,0 +1,38 @@ +package gs.smolban.model + +/** Enumeration that describes the status of a [[Ticket]] in Smolban. Smolban + * does not yet support custom status/workflow. + * + * @param value + * The string value of the status. + */ +sealed abstract class Status(val value: String) + +object Status: + + /** This ticket is new, and ready to be started. New tickets may be put into + * progress or canceled. + */ + case object Ready extends Status("ready") + + /** This ticket is being worked on actively. In progress tickets can be + * paused, completed, or canceled. + */ + case object InProgress extends Status("in_progress") + + /** This ticket was being worked on, but was temporarily stopped. Paused + * tickets may be put into progress or canceled. + */ + case object Paused extends Status("paused") + + /** This ticket was driven to completion. The work is done. Once in this + * state, the status may no longer change. + */ + case object Complete extends Status("complete") + + /** This ticket was canceled for some reason. Once in this state, the status + * may no longer change. + */ + case object Canceled extends Status("canceled") + +end Status diff --git a/modules/model/src/main/scala/gs/smolban/model/Tag.scala b/modules/model/src/main/scala/gs/smolban/model/Tag.scala new file mode 100644 index 0000000..d836df2 --- /dev/null +++ b/modules/model/src/main/scala/gs/smolban/model/Tag.scala @@ -0,0 +1,21 @@ +package gs.smolban.model + +import cats.Show + +/** Opaque type for a String that represents a unique `Tag` in Smolban. Tags are + * just arbitrary non-empty strings which can be used to annotate [[Ticket]]. + * + * Tags are defined _globally_ in Smolban. + */ +opaque type Tag = String + +object Tag: + + def validate(candidate: String): Option[Tag] = + if candidate.isEmpty() then None else Some(candidate) + + given CanEqual[Tag, Tag] = CanEqual.derived + + given Show[Tag] = t => t + +end Tag diff --git a/modules/model/src/main/scala/gs/smolban/model/Ticket.scala b/modules/model/src/main/scala/gs/smolban/model/Ticket.scala index f373bef..c63de5b 100644 --- a/modules/model/src/main/scala/gs/smolban/model/Ticket.scala +++ b/modules/model/src/main/scala/gs/smolban/model/Ticket.scala @@ -3,9 +3,15 @@ package gs.smolban.model import cats.Show case class Ticket( - id: Long, + id: Ticket.Id, group: Group.Id, - createdAt: CreatedAt + createdAt: CreatedAt, + createdBy: CreatedBy, + title: String, + description: String, + tags: List[Tag], + status: Status, + statusHistory: List[Status] ) object Ticket: diff --git a/modules/model/src/main/scala/gs/smolban/model/users/User.scala b/modules/model/src/main/scala/gs/smolban/model/users/User.scala new file mode 100644 index 0000000..ab2073d --- /dev/null +++ b/modules/model/src/main/scala/gs/smolban/model/users/User.scala @@ -0,0 +1,46 @@ +package gs.smolban.model.users + +import cats.Show +import gs.smolban.model.CreatedAt +import gs.uuid.v0.UUID + +case class User( + id: User.Id, + createdAt: CreatedAt, + username: Username +) + +object User: + + given CanEqual[User, User] = CanEqual.derived + + /** Unique identifier for a [[User]]. This is an opaque type for a UUID. + */ + opaque type Id = UUID + + object Id: + + /** Instantiate a new [[User.Id]]. + * + * @param id + * The underlying UUID. + * @return + * The new [[User.Id]] instance. + */ + def apply(id: UUID): Id = id + + given CanEqual[Id, Id] = CanEqual.derived + + given Show[Id] = _.toUUID().withoutDashes() + + extension (id: Id) + /** Unwrap this User ID. + * + * @return + * The underlying UUID value. + */ + def toUUID(): UUID = id + + end Id + +end User diff --git a/modules/model/src/main/scala/gs/smolban/model/users/Username.scala b/modules/model/src/main/scala/gs/smolban/model/users/Username.scala new file mode 100644 index 0000000..a14b137 --- /dev/null +++ b/modules/model/src/main/scala/gs/smolban/model/users/Username.scala @@ -0,0 +1,47 @@ +package gs.smolban.model.users + +import cats.Show + +/** Opaque type for String that represents a unique username in Smolban. + */ +opaque type Username = String + +object Username: + + /** In Smolban, a [[Username]] must be at most 32 characters long. + */ + val MaximumLength: Int = 32 + + /** In Smolban, a [[Username]] must be at least 3 characters long. + */ + val MinimumLength: Int = 3 + + given CanEqual[Username, Username] = CanEqual.derived + + /** Validate some candidate string, producing a [[Username]] if valid. Smolban + * usernames must be: + * + * - At least 3 characters long. + * - At most 32 characters long. + * - Non-blank -- non-whitespace characters must be used. + * + * @param candidate + * The candidate string to evaluate. + * @return + * The [[Username]], or `None` if the candidate was invalid. + */ + def validate(candidate: String): Option[Username] = + if isValid(candidate) then Some(candidate) else None + + private def isValid(candidate: String): Boolean = + isValidSize(candidate) && isNonBlank(candidate) + + private def isValidSize(candidate: String): Boolean = + candidate.length() >= MinimumLength && candidate.length() <= MaximumLength + + private def isNonBlank(candidate: String): Boolean = + !candidate.isBlank() + + given Show[Username] = u => u + +end Username