so far from the db
This commit is contained in:
parent
a5bea9c7de
commit
5d85d6e6ad
27 changed files with 1136 additions and 262 deletions
43
build.sbt
43
build.sbt
|
|
@ -1,4 +1,4 @@
|
|||
val scala3: String = "3.4.2"
|
||||
val scala3: String = "3.8.1"
|
||||
|
||||
ThisBuild / scalaVersion := scala3
|
||||
ThisBuild / gsProjectName := "smolban"
|
||||
|
|
@ -18,33 +18,36 @@ lazy val sharedSettings = Seq(
|
|||
|
||||
val Deps = new {
|
||||
val Cats = new {
|
||||
val Core: ModuleID = "org.typelevel" %% "cats-core" % "2.10.0"
|
||||
val Effect: ModuleID = "org.typelevel" %% "cats-effect" % "3.5.4"
|
||||
val Core: ModuleID = "org.typelevel" %% "cats-core" % "2.13.0"
|
||||
val Effect: ModuleID = "org.typelevel" %% "cats-effect" % "3.6.3"
|
||||
}
|
||||
|
||||
val Fs2 = new {
|
||||
val Core: ModuleID = "co.fs2" %% "fs2-core" % "3.10.2"
|
||||
val Core: ModuleID = "co.fs2" %% "fs2-core" % "3.12.2"
|
||||
}
|
||||
|
||||
val Doobie = new {
|
||||
val Core: ModuleID = "org.tpolecat" %% "doobie-core" % "1.0.0-M5"
|
||||
val Core: ModuleID = "org.tpolecat" %% "doobie-core" % "1.0.0-RC11"
|
||||
}
|
||||
|
||||
val Http4s = new {
|
||||
val Core: ModuleID = "org.http4s" %% "http4s-core" % "1.0.0-M41"
|
||||
val Dsl: ModuleID = "org.http4s" %% "http4s-dsl" % "1.0.0-M41"
|
||||
val Core: ModuleID = "org.http4s" %% "http4s-core" % "1.0.0-M45"
|
||||
val Dsl: ModuleID = "org.http4s" %% "http4s-dsl" % "1.0.0-M45"
|
||||
val EmberServer: ModuleID =
|
||||
"org.http4s" %% "http4s-ember-server" % "1.0.0-M41"
|
||||
"org.http4s" %% "http4s-ember-server" % "1.0.0-M45"
|
||||
}
|
||||
|
||||
val BouncyCastle = new {
|
||||
val Provider: ModuleID = "org.bouncycastle" % "bcprov-jdk18on" % "1.83"
|
||||
}
|
||||
|
||||
val Gs = new {
|
||||
val Uuid: ModuleID = "gs" %% "gs-uuid-v0" % "0.3.0"
|
||||
val Slug: ModuleID = "gs" %% "gs-slug-v0" % "0.1.3"
|
||||
val Config: ModuleID = "gs" %% "gs-config-v0" % "0.1.1"
|
||||
val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.2.0"
|
||||
val Uuid: ModuleID = "gs" %% "gs-uuid-v0" % "0.4.2"
|
||||
val Config: ModuleID = "gs" %% "gs-config-v0" % "0.2.0"
|
||||
val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.4.1"
|
||||
}
|
||||
|
||||
val MUnit: ModuleID = "org.scalameta" %% "munit" % "1.0.0-RC1"
|
||||
val MUnit: ModuleID = "org.scalameta" %% "munit" % "1.2.1"
|
||||
}
|
||||
|
||||
lazy val testSettings = Seq(
|
||||
|
|
@ -68,11 +71,23 @@ lazy val model = project
|
|||
.settings(
|
||||
libraryDependencies ++= Seq(
|
||||
Deps.Gs.Uuid,
|
||||
Deps.Gs.Slug,
|
||||
Deps.Cats.Core
|
||||
)
|
||||
)
|
||||
|
||||
lazy val auth = project
|
||||
.in(file("modules/auth"))
|
||||
.dependsOn(model)
|
||||
.settings(sharedSettings)
|
||||
.settings(testSettings)
|
||||
.settings(name := s"${gsProjectName.value}-auth")
|
||||
.settings(
|
||||
libraryDependencies ++= Seq(
|
||||
Deps.BouncyCastle.Provider,
|
||||
Deps.Cats.Effect
|
||||
)
|
||||
)
|
||||
|
||||
lazy val db = project
|
||||
.in(file("modules/db"))
|
||||
.dependsOn(model)
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
package gs.smolban.model
|
||||
|
||||
import cats.Show
|
||||
import gs.slug.v0.Slug
|
||||
import gs.uuid.v0.UUID
|
||||
|
||||
/** Groups are the basic unit of organization in Smolban. Each [[Ticket]]
|
||||
* belongs to a single `Group`.
|
||||
*
|
||||
* @param id
|
||||
* The unique identifier for the group.
|
||||
* @param slug
|
||||
* The unique slug for the group.
|
||||
* @param createdAt
|
||||
* The instant at which this group was created.
|
||||
*/
|
||||
case class Group(
|
||||
id: Group.Id,
|
||||
slug: Slug,
|
||||
createdAt: CreatedAt
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
object Id:
|
||||
|
||||
/** Instantiate a new [[Group.Id]].
|
||||
*
|
||||
* @param id
|
||||
* The underlying UUID.
|
||||
* @return
|
||||
* The new [[Group.Id]] instance.
|
||||
*/
|
||||
def apply(id: UUID): Id = id
|
||||
|
||||
given CanEqual[Id, Id] = CanEqual.derived
|
||||
|
||||
given Show[Id] = _.toUUID().withoutDashes()
|
||||
|
||||
extension (id: Id)
|
||||
/** Unwrap this Group ID.
|
||||
*
|
||||
* @return
|
||||
* The underlying UUID value.
|
||||
*/
|
||||
def toUUID(): UUID = id
|
||||
|
||||
end Id
|
||||
|
||||
end Group
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
package gs.smolban.model
|
||||
|
||||
import cats.Show
|
||||
|
||||
/** Represents a smolban "tag", or arbitrary string descriptor.
|
||||
*
|
||||
* @param id
|
||||
* The unique ID for this tag.
|
||||
* @param value
|
||||
* The unique value of this tag.
|
||||
* @param createdAt
|
||||
* The instant this tag was created.
|
||||
*/
|
||||
case class Tag(
|
||||
id: Tag.Id,
|
||||
value: Tag.Value,
|
||||
createdAt: CreatedAt
|
||||
)
|
||||
|
||||
object Tag:
|
||||
|
||||
given CanEqual[Tag, Tag] = CanEqual.derived
|
||||
|
||||
/** Opaque type which represents a [[Tag]] unique identifier. This is a long
|
||||
* integer.
|
||||
*/
|
||||
opaque type Id = Long
|
||||
|
||||
object Id:
|
||||
def apply(id: Long): Id = id
|
||||
|
||||
given CanEqual[Id, Id] = CanEqual.derived
|
||||
|
||||
given Show[Id] = t => t.toString()
|
||||
|
||||
extension (id: Id) def toLong(): Long = id
|
||||
|
||||
end Id
|
||||
|
||||
/** Opaque type which represents a [[Tag]] value. These values are non-empty,
|
||||
* arbitrary strings.
|
||||
*/
|
||||
opaque type Value = String
|
||||
|
||||
object Value:
|
||||
|
||||
/** Validate the candidate string - Tag Values must be non-empty.
|
||||
*
|
||||
* @param candidate
|
||||
* The candidate string.
|
||||
* @return
|
||||
* The valid value, or `None` if an empty string was given.
|
||||
*/
|
||||
def validate(candidate: String): Option[Value] =
|
||||
if candidate.isEmpty() then None else Some(candidate)
|
||||
|
||||
given CanEqual[Value, Value] = CanEqual.derived
|
||||
|
||||
given Show[Value] = t => t
|
||||
|
||||
end Value
|
||||
|
||||
end Tag
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
package gs.smolban.model
|
||||
|
||||
import cats.Show
|
||||
import java.time.Instant
|
||||
|
||||
/** Tickets represent some tracked work.
|
||||
*
|
||||
* @param number
|
||||
* Unique number _within_ the [[Group]] Typically ascending integers.
|
||||
* @param group
|
||||
* Unique identifier of the [[Group]] that owns this ticket.
|
||||
* @param createdAt
|
||||
* The instant at which this ticket was created.
|
||||
* @param title
|
||||
* Arbitrary string title of the ticket.
|
||||
* @param description
|
||||
* Markdown contents of the ticket.
|
||||
* @param tags
|
||||
* List of [[Tag]] applied to this ticket.
|
||||
* @param status
|
||||
* Current [[Ticket.Status]] of this ticket.
|
||||
* @param statusHistory
|
||||
* Linear history of this ticket in terms of status changes.
|
||||
*/
|
||||
case class Ticket(
|
||||
number: Ticket.Number,
|
||||
group: Group.Id,
|
||||
createdAt: CreatedAt,
|
||||
title: String,
|
||||
description: String,
|
||||
tags: List[Tag],
|
||||
status: Ticket.Status,
|
||||
statusHistory: List[(Ticket.Status, Instant)]
|
||||
):
|
||||
lazy val reference: Ticket.Reference = Ticket.Reference(number, group)
|
||||
|
||||
object Ticket:
|
||||
|
||||
/** Composite reference that uniquely addresses any [[Ticket]].
|
||||
*
|
||||
* @param number
|
||||
* The ticket's unique identifier within the [[Group]].
|
||||
* @param group
|
||||
* The unique identifier of the [[Group]].
|
||||
*/
|
||||
case class Reference(
|
||||
number: Ticket.Number,
|
||||
group: Group.Id
|
||||
)
|
||||
|
||||
/** Unique identifier - relative to some [[Group]] - for a [[Ticket]]. This is
|
||||
* an opaque type for a Long. In general, [[Ticket]] identifiers are
|
||||
* sequences within a group.
|
||||
*/
|
||||
opaque type Number = Long
|
||||
|
||||
object Number:
|
||||
|
||||
/** Instantiate a new [[Ticket.Number]].
|
||||
*
|
||||
* @param number
|
||||
* The underlying Long.
|
||||
* @return
|
||||
* The new [[Ticket.Number]] instance.
|
||||
*/
|
||||
def apply(number: Long): Number = number
|
||||
|
||||
given CanEqual[Number, Number] = CanEqual.derived
|
||||
|
||||
given Show[Number] = _.toLong().toString()
|
||||
|
||||
extension (number: Number)
|
||||
/** Unwrap this Ticket ID.
|
||||
*
|
||||
* @return
|
||||
* The underlying Long value.
|
||||
*/
|
||||
def toLong(): Long = number
|
||||
|
||||
end Number
|
||||
|
||||
/** Enumeration that describes the status of a [[Ticket]] in Smolban. Smolban
|
||||
* does not yet support custom status/workflow.
|
||||
*/
|
||||
sealed abstract class Status(val name: String)
|
||||
|
||||
object Status:
|
||||
|
||||
given CanEqual[Status, Status] = CanEqual.derived
|
||||
|
||||
given Show[Status] = _.name
|
||||
|
||||
/** 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")
|
||||
|
||||
val All: List[Status] = List(Ready, InProgress, Paused, Complete, Canceled)
|
||||
|
||||
def parse(candidate: String): Option[Status] =
|
||||
All.find(_.name.equalsIgnoreCase(candidate))
|
||||
|
||||
end Status
|
||||
|
||||
end Ticket
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
package gs.smolban.model.account
|
||||
|
||||
import gs.smolban.model.metadata.CreatedAt
|
||||
import gs.smolban.model.metadata.Description
|
||||
|
||||
/** Represents some Account - any entity that can be represented in Smolban and
|
||||
* take some form of action in Smolban.
|
||||
*
|
||||
* @param accountType
|
||||
* The [[AccountType]].
|
||||
*/
|
||||
sealed abstract class Account(val accountType: AccountType):
|
||||
/** @return
|
||||
* The unique identifier for this [[Account]].
|
||||
*/
|
||||
def id: AccountId
|
||||
|
||||
/** @return
|
||||
* The unique name for this [[Account]].
|
||||
*/
|
||||
def name: AccountName
|
||||
|
||||
/** @return
|
||||
* The current status of this [[Account]].
|
||||
*/
|
||||
def status: AccountStatus
|
||||
|
||||
/** @return
|
||||
* The complete set of [[Permission]] assigned to this account.
|
||||
*/
|
||||
def permissions: PermissionSet
|
||||
|
||||
/** @return
|
||||
* The instant this [[Account]] was created.
|
||||
*/
|
||||
def createdAt: CreatedAt
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def equals(obj: Any): Boolean =
|
||||
obj match
|
||||
case other: Account => id == other.id
|
||||
case _ => false
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def hashCode(): Int = id.unwrap().hashCode()
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def toString(): String =
|
||||
s"${accountType.name}:${id.unwrap().withoutDashes()}"
|
||||
|
||||
/** Account for regular human users.
|
||||
*
|
||||
* @param id
|
||||
* The unique identifier for this [[Account]].
|
||||
* @param name
|
||||
* The unique name for this [[Account]].
|
||||
* @param status
|
||||
* The current status of this [[Account]].
|
||||
* @param permissions
|
||||
* The complete set of [[Permission]] assigned to this account.
|
||||
* @param createdAt
|
||||
* The instant this [[Account]] was created.
|
||||
*/
|
||||
final class User(
|
||||
val id: AccountId,
|
||||
val name: AccountName,
|
||||
val status: AccountStatus,
|
||||
val permissions: PermissionSet,
|
||||
val createdAt: CreatedAt
|
||||
) extends Account(AccountType.User)
|
||||
|
||||
/** Account for arbitrary services and integrations.
|
||||
*
|
||||
* @param id
|
||||
* The unique identifier for this [[Account]].
|
||||
* @param name
|
||||
* The unique name for this [[Account]].
|
||||
* @param status
|
||||
* The current status of this [[Account]].
|
||||
* @param permissions
|
||||
* The complete set of [[Permission]] assigned to this account.
|
||||
* @param description
|
||||
* The purpose of this service account.
|
||||
* @param createdAt
|
||||
* The instant this [[Account]] was created.
|
||||
* @param owner
|
||||
* The account which owns this service account. This will always be a
|
||||
* [[User]].
|
||||
*/
|
||||
final class ServiceAccount(
|
||||
val id: AccountId,
|
||||
val name: AccountName,
|
||||
val status: AccountStatus,
|
||||
val permissions: PermissionSet,
|
||||
val description: Description,
|
||||
val createdAt: CreatedAt,
|
||||
val owner: AccountId
|
||||
) extends Account(AccountType.Service)
|
||||
|
||||
/** Account to specifically represent AI Agents.
|
||||
*
|
||||
* @param id
|
||||
* The unique identifier for this [[Account]].
|
||||
* @param name
|
||||
* The unique name for this [[Account]].
|
||||
* @param status
|
||||
* The current status of this [[Account]].
|
||||
* @param permissions
|
||||
* The complete set of [[Permission]] assigned to this account.
|
||||
* @param description
|
||||
* The purpose of this AI Agent integration.
|
||||
* @param createdAt
|
||||
* The instant this [[Account]] was created.
|
||||
* @param owner
|
||||
* The account which owns this agent account. This will always be a [[User]].
|
||||
*/
|
||||
final class AiAgent(
|
||||
val id: AccountId,
|
||||
val name: AccountName,
|
||||
val status: AccountStatus,
|
||||
val permissions: PermissionSet,
|
||||
val description: Description,
|
||||
val createdAt: CreatedAt,
|
||||
val owner: AccountId
|
||||
) extends Account(AccountType.Agent)
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package gs.smolban.model.account
|
||||
|
||||
import cats.Eq
|
||||
import cats.Show
|
||||
import gs.uuid.v0.UUID
|
||||
|
||||
/** Uniquely identifies a _single account_ within Smolban.
|
||||
*/
|
||||
opaque type AccountId = UUID
|
||||
|
||||
object AccountId:
|
||||
|
||||
/** Instantiate a new [[AccountId]] from the given UUID.
|
||||
*
|
||||
* @param value
|
||||
* The UUID.
|
||||
* @return
|
||||
* The new [[AccountId]].
|
||||
*/
|
||||
def apply(value: UUID): AccountId = value
|
||||
|
||||
/** Generate a new [[AccountId]] using the UUIDv7 algorithm.
|
||||
*
|
||||
* @return
|
||||
* The new [[AccountId]].
|
||||
*/
|
||||
def generate(): AccountId = UUID.v7()
|
||||
|
||||
given CanEqual[AccountId, AccountId] = CanEqual.derived
|
||||
|
||||
given Show[AccountId] = _.unwrap().withoutDashes()
|
||||
|
||||
given Eq[AccountId] = (
|
||||
x,
|
||||
y
|
||||
) => x == y
|
||||
|
||||
extension (accountId: AccountId)
|
||||
/** @return
|
||||
* The underlying UUID value.
|
||||
*/
|
||||
def unwrap(): UUID = accountId
|
||||
|
||||
/** @return
|
||||
* The underlying UUID value.
|
||||
*/
|
||||
def toUUID(): UUID = accountId
|
||||
|
||||
end AccountId
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package gs.smolban.model.account
|
||||
|
||||
import cats.Eq
|
||||
import cats.Show
|
||||
import scala.util.matching.Regex
|
||||
|
||||
/** Opaque type which represents the name of some account. These values are
|
||||
* non-empty, arbitrary strings, with at most 80 characters. These are
|
||||
* descriptive values, but _may_ be used as part of credentials (e.g. for a
|
||||
* "user" account, this can be the username).
|
||||
*/
|
||||
opaque type AccountName = String
|
||||
|
||||
object AccountName:
|
||||
private val regex: Regex = """^.{1,80}$""".r
|
||||
|
||||
/** Validate the candidate string. Account names may be a maximum of 80
|
||||
* characters. Name characters are not restricted.
|
||||
*
|
||||
* @param candidate
|
||||
* The candidate string.
|
||||
* @return
|
||||
* Some new [[AccountName]] instance, or `None` if the candidate is not
|
||||
* valid.
|
||||
*/
|
||||
def validate(candidate: String): Option[AccountName] =
|
||||
if regex.matches(candidate) then Some(candidate) else None
|
||||
|
||||
given CanEqual[AccountName, AccountName] = CanEqual.derived
|
||||
|
||||
given Eq[AccountName] = (
|
||||
x,
|
||||
y
|
||||
) => x == y
|
||||
|
||||
given Show[AccountName] = an => an
|
||||
|
||||
extension (accountName: AccountName)
|
||||
/** @return
|
||||
* The underlying string value.
|
||||
*/
|
||||
def unwrap(): String = accountName
|
||||
|
||||
end AccountName
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package gs.smolban.model.account
|
||||
|
||||
import cats.Eq
|
||||
import cats.Show
|
||||
|
||||
/** Enumeration that describes the status of an [[Account]].
|
||||
*
|
||||
* Smolban accounts are either active or not. They cannot be deleted - to
|
||||
* preserve lineage of all operations.
|
||||
*/
|
||||
sealed abstract class AccountStatus(val name: String):
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def equals(obj: Any): Boolean =
|
||||
obj match
|
||||
case other: AccountStatus => name == other.name
|
||||
case _ => false
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def toString(): String = name
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def hashCode(): Int = name.hashCode()
|
||||
|
||||
object AccountStatus:
|
||||
|
||||
given CanEqual[AccountStatus, AccountStatus] = CanEqual.derived
|
||||
|
||||
given Eq[AccountStatus] = (
|
||||
x,
|
||||
y
|
||||
) => x == y
|
||||
|
||||
given Show[AccountStatus] = _.name
|
||||
|
||||
/** The account is active and can be used freely.
|
||||
*/
|
||||
case object Active extends AccountStatus("active")
|
||||
|
||||
/** The account is not active and cannot be used unless reactivated.
|
||||
*/
|
||||
case object Inactive extends AccountStatus("inactive")
|
||||
|
||||
/** List of all valid account types.
|
||||
*/
|
||||
val All: List[AccountStatus] =
|
||||
List(Active, Inactive)
|
||||
|
||||
/** Parse the given string as an [[AccountStatus]].
|
||||
*
|
||||
* @param candidate
|
||||
* The string to parse.
|
||||
* @return
|
||||
* Some account status value, or `None` if the given string is not a valid
|
||||
* account status.
|
||||
*/
|
||||
def parse(candidate: String): Option[AccountStatus] =
|
||||
All.find(_.name.equalsIgnoreCase(candidate))
|
||||
|
||||
end AccountStatus
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package gs.smolban.model.account
|
||||
|
||||
import cats.Eq
|
||||
import cats.Show
|
||||
|
||||
/** Enumeration that describes the supported account types in Smolban.
|
||||
*/
|
||||
sealed abstract class AccountType(val name: String):
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def equals(obj: Any): Boolean =
|
||||
obj match
|
||||
case other: AccountType => name == other.name
|
||||
case _ => false
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def toString(): String = name
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def hashCode(): Int = name.hashCode()
|
||||
|
||||
object AccountType:
|
||||
|
||||
given CanEqual[AccountType, AccountType] = CanEqual.derived
|
||||
|
||||
given Eq[AccountType] = (
|
||||
x,
|
||||
y
|
||||
) => x == y
|
||||
|
||||
given Show[AccountType] = _.name
|
||||
|
||||
/** Regular human users. Typically access Smolban via some UI.
|
||||
*/
|
||||
case object User extends AccountType("user")
|
||||
|
||||
/** Service accounts.
|
||||
*/
|
||||
case object Service extends AccountType("service_account")
|
||||
|
||||
/** AI agents.
|
||||
*/
|
||||
case object Agent extends AccountType("ai_agent")
|
||||
|
||||
/** List of all valid account types.
|
||||
*/
|
||||
val All: List[AccountType] =
|
||||
List(User, Service, Agent)
|
||||
|
||||
/** Parse the given string as an [[AccountType]].
|
||||
*
|
||||
* @param candidate
|
||||
* The string to parse.
|
||||
* @return
|
||||
* Some account type value, or `None` if the given string is not a valid
|
||||
* account type.
|
||||
*/
|
||||
def parse(candidate: String): Option[AccountType] =
|
||||
All.find(_.name.equalsIgnoreCase(candidate))
|
||||
|
||||
end AccountType
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
package gs.smolban.model.account
|
||||
|
||||
import cats.Eq
|
||||
import cats.Show
|
||||
|
||||
/** Enumeration that describes all permissions supported by Smolban.
|
||||
*
|
||||
* In Smolban: all users may read all tickets. Smolban does not have the notion
|
||||
* of private tickets.
|
||||
*
|
||||
* @param name
|
||||
* The unique name of the permission.
|
||||
*/
|
||||
sealed abstract class Permission(val name: String):
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def equals(obj: Any): Boolean =
|
||||
obj match
|
||||
case other: Permission => name == other.name
|
||||
case _ => false
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def toString(): String = name
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def hashCode(): Int = name.hashCode()
|
||||
|
||||
object Permission:
|
||||
|
||||
given CanEqual[Permission, Permission] = CanEqual.derived
|
||||
|
||||
given Eq[Permission] = (
|
||||
x,
|
||||
y
|
||||
) => x == y
|
||||
|
||||
given Show[Permission] = _.name
|
||||
|
||||
/** Permission to manage tickets within groups.
|
||||
*
|
||||
* ## Scope
|
||||
*
|
||||
* - Global
|
||||
* - Group
|
||||
*
|
||||
* ## Capabilities
|
||||
*
|
||||
* - Create tickets within the given groups.
|
||||
* - Modify tickets within the given groups.
|
||||
*/
|
||||
case object GroupMember extends Permission("group_member")
|
||||
|
||||
/** Permission to manage groups. Superset of `GroupMember`.
|
||||
*
|
||||
* ## Scope
|
||||
*
|
||||
* - Global
|
||||
* - Group
|
||||
*
|
||||
* ## Capabilities
|
||||
*
|
||||
* - Create tickets within the given groups.
|
||||
* - Modify tickets within the given groups.
|
||||
* - Modify group attributes.
|
||||
*/
|
||||
case object ManageGroup extends Permission("manage_group")
|
||||
|
||||
/** Global permission to create groups.
|
||||
*/
|
||||
case object CreateGroup extends Permission("create_group")
|
||||
|
||||
/** Permission to manage user accounts.
|
||||
*
|
||||
* ## Scope
|
||||
*
|
||||
* - Global
|
||||
*
|
||||
* ## Capabilities
|
||||
*
|
||||
* - Deactivate user accounts.
|
||||
* - Revoke user credentials.
|
||||
* - Assign permissions to user accounts.
|
||||
*
|
||||
* Note: Only administrators can reactivate accounts.
|
||||
*/
|
||||
case object ManageUserAccount extends Permission("manage_user_account")
|
||||
|
||||
/** Global permission to create users.
|
||||
*/
|
||||
case object CreateUserAccount extends Permission("create_user_account")
|
||||
|
||||
/** Permission to manage service accounts.
|
||||
*
|
||||
* ## Scope
|
||||
*
|
||||
* - Global
|
||||
*
|
||||
* Users who create service accounts always have permission to manage their
|
||||
* own service accounts.
|
||||
*
|
||||
* ## Capabilities
|
||||
*
|
||||
* - Deactivate service accounts.
|
||||
* - Revoke service account credentials.
|
||||
* - Provision new service account credentials.
|
||||
* - Assign permissions to service accounts.
|
||||
*
|
||||
* Note: Only administrators can reactivate accounts.
|
||||
*/
|
||||
case object ManageServiceAccount extends Permission("manage_service_account")
|
||||
|
||||
/** Global permission to create service accounts.
|
||||
*/
|
||||
case object CreateServiceAccount extends Permission("create_service_account")
|
||||
|
||||
/** Permission to manage AI agent accounts.
|
||||
*
|
||||
* ## Scope
|
||||
*
|
||||
* - Global
|
||||
*
|
||||
* Users who create AI agent accounts always have permission to manage their
|
||||
* own AI agent accounts.
|
||||
*
|
||||
* ## Capabilities
|
||||
*
|
||||
* - Deactivate agent accounts.
|
||||
* - Revoke agent account credentials.
|
||||
* - Provision new agent account credentials.
|
||||
* - Assign permissions to agent accounts.
|
||||
*
|
||||
* Note: Only administrators can reactivate accounts.
|
||||
*/
|
||||
case object ManageAgentAccount extends Permission("manage_agent_account")
|
||||
|
||||
/** Global permission to create AI agent accounts.
|
||||
*/
|
||||
case object CreateAgentAccount extends Permission("create_agent_account")
|
||||
|
||||
/** Global permission to perform any action.
|
||||
*/
|
||||
case object Admin extends Permission("admin")
|
||||
|
||||
/** List of all valid permissions.
|
||||
*/
|
||||
val All: List[Permission] =
|
||||
List(
|
||||
GroupMember,
|
||||
ManageGroup,
|
||||
CreateGroup,
|
||||
ManageUserAccount,
|
||||
CreateUserAccount,
|
||||
ManageServiceAccount,
|
||||
CreateServiceAccount,
|
||||
ManageAgentAccount,
|
||||
CreateAgentAccount,
|
||||
Admin
|
||||
)
|
||||
|
||||
/** Parse the given string as an [[Permission]].
|
||||
*
|
||||
* @param candidate
|
||||
* The string to parse.
|
||||
* @return
|
||||
* Some account status value, or `None` if the given string is not a valid
|
||||
* account status.
|
||||
*/
|
||||
def parse(candidate: String): Option[Permission] =
|
||||
All.find(_.name.equalsIgnoreCase(candidate))
|
||||
|
||||
end Permission
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package gs.smolban.model.account
|
||||
|
||||
import gs.smolban.model.group.GroupName
|
||||
|
||||
/** Defines the complete set of scoped [[Permission]] for some [[Account]].
|
||||
*
|
||||
* @param global
|
||||
* The set of global permissions.
|
||||
* @param group
|
||||
* The set of permissions for each assigned group.
|
||||
*/
|
||||
case class PermissionSet(
|
||||
global: Set[Permission],
|
||||
group: Map[GroupName, Set[Permission]]
|
||||
)
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package gs.smolban.model.group
|
||||
|
||||
import cats.Show
|
||||
import gs.smolban.model.account.AccountId
|
||||
import gs.smolban.model.metadata.CreatedAt
|
||||
import gs.smolban.model.metadata.Title
|
||||
|
||||
/** Groups are the basic unit of organization in Smolban. Each [[Ticket]]
|
||||
* belongs to a single `Group`.
|
||||
*
|
||||
* TODO: Ownership. Members.
|
||||
*
|
||||
* @param id
|
||||
* The unique identifier for the group.
|
||||
* @param name
|
||||
* The unique name for the group. Used in URLs.
|
||||
* @param createdAt
|
||||
* The instant at which this group was created.
|
||||
* @param creator
|
||||
* Uniquely identifies the [[Account]] which created this Group.
|
||||
*/
|
||||
case class Group(
|
||||
name: GroupName,
|
||||
title: Title,
|
||||
description: String,
|
||||
createdAt: CreatedAt,
|
||||
creator: AccountId
|
||||
)
|
||||
|
||||
object Group:
|
||||
|
||||
given CanEqual[Group, Group] = CanEqual.derived
|
||||
|
||||
given Show[Group] = _.name.unwrap()
|
||||
|
||||
end Group
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package gs.smolban.model.group
|
||||
|
||||
import cats.Eq
|
||||
import cats.Show
|
||||
import scala.util.matching.Regex
|
||||
|
||||
/** Unique name for a [[Group]]. This is an opaque type for a String.
|
||||
*
|
||||
* ## Requirements
|
||||
*
|
||||
* - Starts with a character `A-Z`
|
||||
* - Only contains uppercase letters, numbers, and underscores.
|
||||
* - At least 2 characters.
|
||||
* - At most 80 characters.
|
||||
*
|
||||
* Regular Expression: `^[A-Z][A-Z0-9\_]{1,79}$`
|
||||
*/
|
||||
opaque type GroupName = String
|
||||
|
||||
object GroupName:
|
||||
|
||||
private val regex: Regex = """^[A-Z][A-Z0-9\_]{1,79}$""".r
|
||||
|
||||
/** Instantiate a new [[GroupName]].
|
||||
*
|
||||
* @param name
|
||||
* The underlying String.
|
||||
* @return
|
||||
* The new [[GroupName]] instance.
|
||||
*/
|
||||
def validate(name: String): Option[GroupName] =
|
||||
if regex.matches(name) then Some(name) else None
|
||||
|
||||
given CanEqual[GroupName, GroupName] = CanEqual.derived
|
||||
|
||||
given Eq[GroupName] = (
|
||||
x,
|
||||
y
|
||||
) => x == y
|
||||
|
||||
given Show[GroupName] = gn => gn
|
||||
|
||||
extension (name: GroupName)
|
||||
/** Unwrap this Group name.
|
||||
*
|
||||
* @return
|
||||
* The underlying String value.
|
||||
*/
|
||||
def unwrap(): String = name
|
||||
|
||||
end GroupName
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package gs.smolban.model
|
||||
package gs.smolban.model.metadata
|
||||
|
||||
import cats.Show
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
|
||||
/** Describes an instant at which something was created. Opaque type for
|
||||
|
|
@ -19,6 +20,15 @@ object CreatedAt:
|
|||
*/
|
||||
def apply(timestamp: Instant): CreatedAt = timestamp
|
||||
|
||||
/** Instantiate a new [[CreatedAt]] representing the current instant.
|
||||
*
|
||||
* @param clock
|
||||
* The clock used to calculate instants.
|
||||
* @return
|
||||
* The new [[CreatedAt]] instance.
|
||||
*/
|
||||
def now(clock: Clock): CreatedAt = clock.instant()
|
||||
|
||||
/** Instantiate a new [[CreatedAt]].
|
||||
*
|
||||
* @param millis
|
||||
|
|
@ -33,6 +43,13 @@ object CreatedAt:
|
|||
given Show[CreatedAt] = _.toInstant().toString()
|
||||
|
||||
extension (createdAt: CreatedAt)
|
||||
/** Unwrap this value.
|
||||
*
|
||||
* @return
|
||||
* The underlying `Instant` value.
|
||||
*/
|
||||
def unwrap(): Instant = createdAt
|
||||
|
||||
/** Unwrap this value.
|
||||
*
|
||||
* @return
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package gs.smolban.model.metadata
|
||||
|
||||
import cats.Eq
|
||||
import cats.Show
|
||||
|
||||
/** Opaque type which represents any description. Descriptions are arbitrary
|
||||
* strings.
|
||||
*/
|
||||
opaque type Description = String
|
||||
|
||||
object Description:
|
||||
|
||||
/** Instantiate a new description.
|
||||
*
|
||||
* @param value
|
||||
* The description text.
|
||||
* @return
|
||||
* The new [[Description]] instance.
|
||||
*/
|
||||
def apply(value: String): Description = value
|
||||
|
||||
given CanEqual[Description, Description] = CanEqual.derived
|
||||
|
||||
given Eq[Description] = (
|
||||
x,
|
||||
y
|
||||
) => x == y
|
||||
|
||||
given Show[Description] = t => t
|
||||
|
||||
extension (tag: Description)
|
||||
/** @return
|
||||
* The underlying string value.
|
||||
*/
|
||||
def unwrap(): String = tag
|
||||
|
||||
end Description
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package gs.smolban.model.metadata
|
||||
|
||||
import cats.Eq
|
||||
import cats.Show
|
||||
|
||||
/** Represents a smolban "tag", or arbitrary string descriptor. This object
|
||||
* captures additional information about the tag from the system perspective.
|
||||
*
|
||||
* @param value
|
||||
* The unique value of this tag.
|
||||
* @param createdAt
|
||||
* The instant this tag was created.
|
||||
*/
|
||||
case class Tag(
|
||||
value: TagValue,
|
||||
createdAt: CreatedAt
|
||||
):
|
||||
override def toString(): String = value.toString()
|
||||
|
||||
object Tag:
|
||||
|
||||
given CanEqual[Tag, Tag] = CanEqual.derived
|
||||
|
||||
given Eq[TagValue] = (
|
||||
x,
|
||||
y
|
||||
) => x == y
|
||||
|
||||
given Show[Tag] = _.value.unwrap()
|
||||
|
||||
end Tag
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package gs.smolban.model.metadata
|
||||
|
||||
import cats.Eq
|
||||
import cats.Show
|
||||
|
||||
/** Opaque type which represents a [[Tag]] value. These values are non-empty,
|
||||
* arbitrary strings.
|
||||
*/
|
||||
opaque type TagValue = String
|
||||
|
||||
object TagValue:
|
||||
|
||||
/** Validate the candidate string - TagValues must be non-empty.
|
||||
*
|
||||
* @param candidate
|
||||
* The candidate string.
|
||||
* @return
|
||||
* The valid value, or `None` if an empty string was given.
|
||||
*/
|
||||
def validate(candidate: String): Option[TagValue] =
|
||||
if candidate.isEmpty() then None else Some(candidate)
|
||||
|
||||
given CanEqual[TagValue, TagValue] = CanEqual.derived
|
||||
|
||||
given Eq[TagValue] = (
|
||||
x,
|
||||
y
|
||||
) => x == y
|
||||
|
||||
given Show[TagValue] = t => t
|
||||
|
||||
extension (tag: TagValue)
|
||||
/** @return
|
||||
* The underlying string value.
|
||||
*/
|
||||
def unwrap(): String = tag
|
||||
|
||||
end TagValue
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package gs.smolban.model.metadata
|
||||
|
||||
import cats.Eq
|
||||
import cats.Show
|
||||
import scala.util.matching.Regex
|
||||
|
||||
/** Opaque type which represents the title of some object. These values are
|
||||
* non-empty, arbitrary strings, with at most 120 characters. They are intended
|
||||
* for providing brief summaries of intent to users.
|
||||
*/
|
||||
opaque type Title = String
|
||||
|
||||
object Title:
|
||||
private val regex: Regex = """^.{1,120}$""".r
|
||||
|
||||
/** Validate the candidate string. Titles may be a maximum of 120 characters.
|
||||
* Title characters are not restricted.
|
||||
*
|
||||
* @param candidate
|
||||
* The candidate string.
|
||||
* @return
|
||||
* Some new [[Title]] instance, or `None` if the candidate is not valid.
|
||||
*/
|
||||
def validate(candidate: String): Option[Title] =
|
||||
if regex.matches(candidate) then Some(candidate) else None
|
||||
|
||||
given CanEqual[Title, Title] = CanEqual.derived
|
||||
|
||||
given Eq[Title] = (
|
||||
x,
|
||||
y
|
||||
) => x == y
|
||||
|
||||
given Show[Title] = t => t
|
||||
|
||||
extension (tag: Title)
|
||||
/** @return
|
||||
* The underlying string value.
|
||||
*/
|
||||
def unwrap(): String = tag
|
||||
|
||||
end Title
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package gs.smolban.model.ticket
|
||||
|
||||
import gs.smolban.model.metadata.CreatedAt
|
||||
|
||||
/** Some textual comment on a [[Ticket]]. Comments are arbitrary text that
|
||||
* communicate information about a ticket from some actor.
|
||||
*
|
||||
* - Comments are ordered by when they were created.
|
||||
* - Comments _may_ be edited.
|
||||
* - Comments _may_ be deleted.
|
||||
* - Comments have limited size.
|
||||
* - Comments do not have reply / relationship capabilities.
|
||||
*
|
||||
* @param id
|
||||
* Globally unique identifier for this comment.
|
||||
* @param content
|
||||
* The content of the comment.
|
||||
* @param createdAt
|
||||
* Instant the comment was created.
|
||||
*/
|
||||
case class Comment(
|
||||
id: CommentId,
|
||||
content: CommentContent,
|
||||
createdAt: CreatedAt
|
||||
)
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package gs.smolban.model.ticket
|
||||
|
||||
import cats.Eq
|
||||
import cats.Show
|
||||
import scala.util.matching.Regex
|
||||
|
||||
/** Opaque type which represents the content of some [[Comment]]. These values
|
||||
* are non-empty, arbitrary strings, with at most `8192` characters.
|
||||
*
|
||||
* This limitation may be customizable in the future.
|
||||
*/
|
||||
opaque type CommentContent = String
|
||||
|
||||
object CommentContent:
|
||||
private val regex: Regex = """^.{1,8192}$""".r
|
||||
|
||||
/** Validate the candidate string. Comments may be a maximum of 8192
|
||||
* characters. Characters are not restricted.
|
||||
*
|
||||
* @param candidate
|
||||
* The candidate string.
|
||||
* @return
|
||||
* Some new [[CommentContent]] instance, or `None` if the candidate is not
|
||||
* valid.
|
||||
*/
|
||||
def validate(candidate: String): Option[CommentContent] =
|
||||
if regex.matches(candidate) then Some(candidate) else None
|
||||
|
||||
given CanEqual[CommentContent, CommentContent] = CanEqual.derived
|
||||
|
||||
given Eq[CommentContent] = (
|
||||
x,
|
||||
y
|
||||
) => x == y
|
||||
|
||||
given Show[CommentContent] = cc => cc
|
||||
|
||||
extension (content: CommentContent)
|
||||
/** @return
|
||||
* The underlying string value.
|
||||
*/
|
||||
def unwrap(): String = content
|
||||
|
||||
end CommentContent
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package gs.smolban.model.ticket
|
||||
|
||||
import cats.Eq
|
||||
import cats.Show
|
||||
import gs.uuid.v0.UUID
|
||||
|
||||
/** Uniquely identifies a _single semantic comment_ on a ticket.
|
||||
*/
|
||||
opaque type CommentId = UUID
|
||||
|
||||
object CommentId:
|
||||
|
||||
/** Instantiate a new [[CommentId]] from the given UUID.
|
||||
*
|
||||
* @param value
|
||||
* The UUID.
|
||||
* @return
|
||||
* The new [[CommentId]].
|
||||
*/
|
||||
def apply(value: UUID): CommentId = value
|
||||
|
||||
/** Generate a new [[CommentId]] using the UUIDv7 algorithm.
|
||||
*
|
||||
* @return
|
||||
* The new [[CommentId]].
|
||||
*/
|
||||
def generate(): CommentId = UUID.v7()
|
||||
|
||||
given CanEqual[CommentId, CommentId] = CanEqual.derived
|
||||
|
||||
given Show[CommentId] = _.unwrap().withoutDashes()
|
||||
|
||||
given Eq[CommentId] = (
|
||||
x,
|
||||
y
|
||||
) => x == y
|
||||
|
||||
extension (commentId: CommentId)
|
||||
/** @return
|
||||
* The underlying UUID value.
|
||||
*/
|
||||
def unwrap(): UUID = commentId
|
||||
|
||||
/** @return
|
||||
* The underlying UUID value.
|
||||
*/
|
||||
def toUUID(): UUID = commentId
|
||||
|
||||
end CommentId
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package gs.smolban.model.ticket
|
||||
|
||||
import cats.Eq
|
||||
import cats.Show
|
||||
import cats.syntax.all.*
|
||||
import gs.smolban.model.account.AccountId
|
||||
import gs.smolban.model.group.GroupName
|
||||
import gs.smolban.model.metadata.CreatedAt
|
||||
import gs.smolban.model.metadata.Description
|
||||
import gs.smolban.model.metadata.TagValue
|
||||
import gs.smolban.model.metadata.Title
|
||||
import java.util.Objects
|
||||
|
||||
/** Tickets represent some tracked work.
|
||||
*
|
||||
* TODO: Comments, Attachments, Watching
|
||||
*
|
||||
* @param number
|
||||
* Unique number _within_ the [[Group]] Typically ascending integers.
|
||||
* @param group
|
||||
* Unique identifier of the [[Group]] that owns this ticket.
|
||||
* @param createdAt
|
||||
* The instant at which this ticket was created.
|
||||
* @param title
|
||||
* Arbitrary string title of the ticket.
|
||||
* @param description
|
||||
* Markdown contents of the ticket.
|
||||
* @param tags
|
||||
* List of [[Tag]] applied to this ticket.
|
||||
* @param status
|
||||
* Current [[Ticket.Status]] of this ticket.
|
||||
* @param creator
|
||||
* Uniquely identifes the [[Account]] which created this ticket.
|
||||
* @param creator
|
||||
* Uniquely identifes the [[Account]] to which this ticket is assigned.
|
||||
*/
|
||||
case class Ticket(
|
||||
number: TicketNumber,
|
||||
group: GroupName,
|
||||
createdAt: CreatedAt,
|
||||
title: Title,
|
||||
description: Description,
|
||||
tags: List[TagValue],
|
||||
status: TicketStatus,
|
||||
creator: AccountId,
|
||||
assignee: Option[AccountId]
|
||||
):
|
||||
lazy val reference: TicketReference = TicketReference(group, number)
|
||||
|
||||
override def toString(): String = s"${group.show}-${number.show}"
|
||||
|
||||
override def equals(obj: Any): Boolean =
|
||||
obj match
|
||||
case other: Ticket => (group == other.group) && (number == other.number)
|
||||
case _ => false
|
||||
|
||||
override def hashCode(): Int = Objects.hash(group, number)
|
||||
|
||||
object Ticket:
|
||||
|
||||
given CanEqual[Ticket, Ticket] = CanEqual.derived
|
||||
|
||||
given Show[Ticket] = _.toString()
|
||||
|
||||
given Eq[Ticket] = (
|
||||
x,
|
||||
y
|
||||
) => x == y
|
||||
|
||||
end Ticket
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package gs.smolban.model.ticket
|
||||
|
||||
import cats.Show
|
||||
|
||||
/** Unique identifier - relative to some [[gs.smolban.model.group.Group]] - for
|
||||
* a [[Ticket]]. This is an opaque type for a `Long`. In general, [[Ticket]]
|
||||
* identifiers are sequences within a group.
|
||||
*/
|
||||
opaque type TicketNumber = Long
|
||||
|
||||
object TicketNumber:
|
||||
|
||||
/** Instantiate a new [[TicketNumber]].
|
||||
*
|
||||
* @param number
|
||||
* The underlying Long.
|
||||
* @return
|
||||
* The new [[TicketNumber]] instance.
|
||||
*/
|
||||
def apply(number: Long): TicketNumber = number
|
||||
|
||||
given CanEqual[TicketNumber, TicketNumber] = CanEqual.derived
|
||||
|
||||
given Show[TicketNumber] = _.toString()
|
||||
|
||||
extension (number: TicketNumber)
|
||||
/** Unwrap this Ticket ID.
|
||||
*
|
||||
* @return
|
||||
* The underlying Long value.
|
||||
*/
|
||||
def unwrap(): Long = number
|
||||
|
||||
/** Unwrap this Ticket ID.
|
||||
*
|
||||
* @return
|
||||
* The underlying Long value.
|
||||
*/
|
||||
def toLong(): Long = number
|
||||
|
||||
end TicketNumber
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package gs.smolban.model.ticket
|
||||
|
||||
import gs.smolban.model.group.GroupName
|
||||
|
||||
/** Composite reference that uniquely addresses any [[Ticket]].
|
||||
*
|
||||
* @param number
|
||||
* The ticket's unique identifier within the [[Group]].
|
||||
* @param group
|
||||
* The unique identifier of the [[Group]].
|
||||
*/
|
||||
case class TicketReference(
|
||||
group: GroupName,
|
||||
number: TicketNumber
|
||||
)
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package gs.smolban.model.ticket
|
||||
|
||||
import cats.Eq
|
||||
import cats.Show
|
||||
|
||||
/** Enumeration that describes the status of a [[Ticket]] in Smolban.
|
||||
*/
|
||||
sealed abstract class TicketStatus(val name: String):
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def equals(obj: Any): Boolean =
|
||||
obj match
|
||||
case other: TicketStatus => name == other.name
|
||||
case _ => false
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def toString(): String = name
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def hashCode(): Int = name.hashCode()
|
||||
|
||||
object TicketStatus:
|
||||
|
||||
given CanEqual[TicketStatus, TicketStatus] = CanEqual.derived
|
||||
|
||||
given Eq[TicketStatus] = (
|
||||
x,
|
||||
y
|
||||
) => x == y
|
||||
|
||||
given Show[TicketStatus] = _.name
|
||||
|
||||
/** This ticket is not currently active.
|
||||
*/
|
||||
case object Inactive extends TicketStatus("inactive")
|
||||
|
||||
/** This ticket is being worked on actively. In progress tickets can be move
|
||||
* to inactive, completed, or canceled.
|
||||
*/
|
||||
case object InProgress extends TicketStatus("in_progress")
|
||||
|
||||
/** This ticket was driven to completion. The work is done. Once in this
|
||||
* state, the status may no longer change.
|
||||
*/
|
||||
case object Complete extends TicketStatus("complete")
|
||||
|
||||
/** This ticket was canceled for some reason. Once in this state, the status
|
||||
* may no longer change.
|
||||
*/
|
||||
case object Canceled extends TicketStatus("canceled")
|
||||
|
||||
/** List of all valid status values.
|
||||
*/
|
||||
val All: List[TicketStatus] =
|
||||
List(Inactive, InProgress, Complete, Canceled)
|
||||
|
||||
/** Parse the given string as a status.
|
||||
*
|
||||
* @param candidate
|
||||
* The string to parse.
|
||||
* @return
|
||||
* Some status value, or `None` if the given string is not a valid status.
|
||||
*/
|
||||
def parse(candidate: String): Option[TicketStatus] =
|
||||
All.find(_.name.equalsIgnoreCase(candidate))
|
||||
|
||||
end TicketStatus
|
||||
|
|
@ -1 +1 @@
|
|||
sbt.version=1.10.1
|
||||
sbt.version=1.12.1
|
||||
|
|
|
|||
|
|
@ -28,6 +28,6 @@ externalResolvers := Seq(
|
|||
"Garrity Software Releases" at "https://maven.garrity.co/gs"
|
||||
)
|
||||
|
||||
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.8")
|
||||
addSbtPlugin("gs" % "sbt-garrity-software" % "0.3.0")
|
||||
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.4.4")
|
||||
addSbtPlugin("gs" % "sbt-garrity-software" % "0.7.0")
|
||||
addSbtPlugin("gs" % "sbt-gs-calver" % "0.2.0")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue