///|
/// A continuity project stored independently from its execution records.
pub(all) struct Project {
id : String
name : String
owner : String
}
///|
/// Creates a project with the metadata required by the storage layer.
pub fn Project::new(
id~ : String,
name~ : String,
owner~ : String,
) -> Project raise ModelError {
if id == "" {
raise ModelError::EmptyField("project.id")
}
if name == "" {
raise ModelError::EmptyField("project.name")
}
if owner == "" {
raise ModelError::EmptyField("project.owner")
}
{ id, name, owner, }
}
///|
/// A persisted audit event associated with one project.
pub(all) struct AuditRecord {
id : String
project_id : String
event : AuditEvent
}
///|
/// Creates an audit record and revalidates its event payload.
pub fn AuditRecord::new(
id~ : String,
project_id~ : String,
event~ : AuditEvent,
) -> AuditRecord raise ModelError {
if id == "" {
raise ModelError::EmptyField("audit_record.id")
}
if project_id == "" {
raise ModelError::EmptyField("audit_record.project_id")
}
let valid_event = AuditEvent::new(
sequence=event.sequence,
timestamp_minutes=event.timestamp_minutes,
actor=event.actor,
action=event.action,
step_id=event.step_id,
)
{ id, project_id, event: valid_event, }
}
///|
/// An immutable storage snapshot for projects and their audit records.
pub(all) struct ContinuityStorage {
projects : Array[Project]
audits : Array[AuditRecord]
}
///|
/// Creates an empty storage snapshot.
pub fn ContinuityStorage::new() -> ContinuityStorage {
{ projects: [], audits: [], }
}
///|
fn project_index(projects : Array[Project], id : String) -> Int? {
let mut result : Int? = None
for i, project in projects {
if result is None && project.id == id {
result = Some(i)
}
}
result
}
///|
fn audit_index(audits : Array[AuditRecord], id : String) -> Int? {
let mut result : Int? = None
for i, audit in audits {
if result is None && audit.id == id {
result = Some(i)
}
}
result
}
///|
fn copy_projects(projects : Array[Project]) -> Array[Project] {
let result : Array[Project] = []
for project in projects {
result.push(project)
}
result
}
///|
fn copy_audits(audits : Array[AuditRecord]) -> Array[AuditRecord] {
let result : Array[AuditRecord] = []
for audit in audits {
result.push(audit)
}
result
}
///|
/// Saves a project, replacing the previous version with the same ID.
pub fn ContinuityStorage::save_project(
self : ContinuityStorage,
project~ : Project,
) -> ContinuityStorage raise ModelError {
let valid_project = Project::new(
id=project.id,
name=project.name,
owner=project.owner,
)
let projects = copy_projects(self.projects)
match project_index(projects, valid_project.id) {
Some(index) => projects[index] = valid_project
None => projects.push(valid_project)
}
{ projects, audits: copy_audits(self.audits), }
}
///|
/// Returns a stored project when its ID exists.
pub fn ContinuityStorage::get_project(
self : ContinuityStorage,
project_id~ : String,
) -> Project? {
match project_index(self.projects, project_id) {
Some(index) => Some(self.projects[index])
None => None
}
}
///|
/// Returns all stored projects in insertion order.
pub fn ContinuityStorage::list_projects(
self : ContinuityStorage,
) -> Array[Project] {
copy_projects(self.projects)
}
///|
/// Appends an audit record after validating project ownership and chronology.
pub fn ContinuityStorage::append_audit(
self : ContinuityStorage,
record~ : AuditRecord,
) -> ContinuityStorage raise ModelError {
let valid_record = AuditRecord::new(
id=record.id,
project_id=record.project_id,
event=record.event,
)
if project_index(self.projects, valid_record.project_id) is None {
raise ModelError::ProjectNotFound(valid_record.project_id)
}
if audit_index(self.audits, valid_record.id) is Some(_) {
raise ModelError::DuplicateAuditRecord(valid_record.id)
}
let mut previous_timestamp : Int? = None
for existing in self.audits {
if existing.project_id == valid_record.project_id {
previous_timestamp = Some(existing.event.timestamp_minutes)
}
}
match previous_timestamp {
Some(previous) =>
if valid_record.event.timestamp_minutes < previous {
raise ModelError::StorageValidation(
"audit records must be appended in timestamp order",
)
}
None => ()
}
let audits = copy_audits(self.audits)
audits.push(valid_record)
{ projects: copy_projects(self.projects), audits, }
}
///|
/// Returns a project's audit records in append order.
pub fn ContinuityStorage::audit_timeline(
self : ContinuityStorage,
project_id~ : String,
) -> Array[AuditRecord] raise ModelError {
if project_index(self.projects, project_id) is None {
raise ModelError::ProjectNotFound(project_id)
}
let result : Array[AuditRecord] = []
for audit in self.audits {
if audit.project_id == project_id {
result.push(audit)
}
}
result
}