Files
meguri/src/generation_service.rs
T
Ultradesu 5260c2828b
Checks / test (push) Failing after 2m25s
NEXT
2026-10-07 15:30:31 +01:00

266 lines
8.4 KiB
Rust

//! Read-only expansion and atomic activation for one workspace.
use crate::{
domain::*,
engine,
error::{Result, ensure},
generation::{self, Issue, Rule, Variable},
providers,
store::{Store, WorkspaceRecord},
};
use chrono::{Duration, Utc};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use uuid::Uuid;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Draft {
pub revision: i64,
#[serde(default)]
pub shared_revision: Option<i64>,
pub variables: Vec<Variable>,
pub rules: Vec<Rule>,
#[serde(default)]
pub route_position: usize,
#[serde(default)]
pub review_id: Option<String>,
}
struct Prepared {
report: Value,
record: WorkspaceRecord,
issues: Vec<Issue>,
review: String,
}
async fn prepare(db: &Store, actor: Uuid, workspace: Uuid, draft: &Draft) -> Result<Prepared> {
let mut record = db
.prepare(workspace, draft.revision, actor, Permission::EditSchedules)
.await?;
let directory = db.directory(workspace).await?;
crate::store::account_people(&mut record.document, &directory)?;
let members = directory.iter().map(|u| u.id).collect();
let now = Utc::now();
let mut expanded = generation::expand(
&record.document,
workspace,
&draft.variables,
&draft.rules,
draft.route_position,
&record.document.working_interval,
&members,
now,
);
let used: std::collections::BTreeSet<_> = expanded
.document
.schedules
.iter()
.flat_map(|s| match &s.kind {
ScheduleKind::Rotation { participants, .. }
| ScheduleKind::Priority { participants, .. } => participants.clone(),
})
.chain(expanded.document.escalations.iter().flat_map(|e| {
e.steps.iter().filter_map(|s| match s.recipient {
Recipient::User { id } => Some(id),
_ => None,
})
}))
.collect();
for user in &directory {
if used.contains(&user.id) && expanded.document.person(user.id).is_none() {
expanded.document.people.push(user.person.clone());
}
}
if expanded.issues.is_empty() {
if let Err(e) = expanded.document.validate(&members) {
expanded.issues.push(Issue {
path: "People".into(),
message: e.to_string(),
..Issue::default()
});
}
if let Err(e) = providers::validate_capabilities(&expanded.document, now) {
expanded.issues.push(Issue {
path: "Provider".into(),
message: e.to_string(),
..Issue::default()
});
}
// Both shared and manually configured calendars must remain valid under DST.
if let Err(e) = engine::timeline(
&expanded.document,
now,
now + Duration::days(i64::from(expanded.document.horizon_days)),
) {
expanded.issues.push(Issue {
path: "Calendar".into(),
message: e.to_string(),
..Issue::default()
});
}
}
let issues = expanded.issues;
let cancelled = record
.document
.requests
.iter()
.filter(|r| {
r.status == RequestStatus::Pending
&& expanded
.document
.requests
.iter()
.any(|n| n.id == r.id && n.status == RequestStatus::Cancelled)
})
.count();
let result=issues.is_empty().then(||{let d=&expanded.document;json!({"timezone":d.timezone,"people":d.people,"schedules":d.schedules,"escalations":d.escalations,"routes":d.routes,"managed":d.generation.managed})});
let review = generation::stable_id(&json!([
workspace,
draft.revision,
draft.variables,
draft.rules,
draft.route_position
]))
.to_string();
let publication_changes = crate::store::publication_changes(
&expanded.document,
db.published_document(workspace).await?.as_ref(),
)?;
let report = json!({"shared_revision":draft.shared_revision,"publication_changes":publication_changes,"review_id":review,"changes":expanded.changes,"cancelled_requests":cancelled,"result":result,"trace":expanded.trace,"issues":issues});
record.document = expanded.document;
Ok(Prepared {
report,
record,
issues,
review,
})
}
pub async fn preview(db: &Store, actor: Uuid, workspace: Uuid, draft: Draft) -> Result<Value> {
Ok(prepare(db, actor, workspace, &draft).await?.report)
}
pub async fn apply(db: &Store, actor: Uuid, workspace: Uuid, draft: Draft) -> Result<Value> {
let mut prepared = prepare(db, actor, workspace, &draft).await?;
ensure(
prepared.issues.is_empty(),
prepared
.issues
.iter()
.take(8)
.map(|i| format!("{}: {}", i.path, i.message))
.collect::<Vec<_>>()
.join("; "),
)?;
ensure(
draft.review_id.as_deref() == Some(prepared.review.as_str()),
crate::i18n::text("err_the_preview_is_stale_check_the_updated_impact_be_85cacd"),
)?;
db.commit(
&mut prepared.record,
actor,
Permission::EditSchedules,
"generation.applied",
prepared.report,
None,
)
.await?;
Ok(json!({"ok":true}))
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Transfer {
pub revision: i64,
pub shared_revision: i64,
#[serde(default)]
pub text: String,
#[serde(default)]
pub review_id: Option<String>,
}
async fn transfer_base(
db: &Store,
actor: Uuid,
workspace: Uuid,
request: &Transfer,
) -> Result<(WorkspaceRecord, Draft)> {
let record = db
.prepare(
workspace,
request.revision,
actor,
Permission::EditSchedules,
)
.await?;
let shared = db.shared_draft(workspace).await?;
ensure(
shared["revision"].as_i64() == Some(request.shared_revision),
crate::i18n::text("err_shared_draft_stale"),
)?;
let mut draft: Draft = if shared["payload"].is_null() {
Draft {
revision: record.revision,
shared_revision: Some(request.shared_revision),
variables: record.document.generation.variables.clone(),
rules: record.document.generation.rules.clone(),
route_position: record.document.generation.route_position,
review_id: None,
}
} else {
serde_json::from_value(shared["payload"].clone())?
};
draft.revision = record.revision;
draft.shared_revision = Some(request.shared_revision);
Ok((record, draft))
}
pub async fn export_config(
db: &Store,
actor: Uuid,
workspace: Uuid,
request: Transfer,
) -> Result<Value> {
let (record, draft) = transfer_base(db, actor, workspace, &request).await?;
Ok(
json!({"text":crate::portable::format_config(&crate::portable::export(&record.document,&draft)?)}),
)
}
pub async fn import_config(
db: &Store,
actor: Uuid,
workspace: Uuid,
request: Transfer,
apply: bool,
) -> Result<Value> {
let (record, base) = transfer_base(db, actor, workspace, &request).await?;
let (draft, inputs) =
crate::portable::import(workspace, &record.document, &base, &request.text)?;
let prepared = prepare(db, actor, workspace, &draft).await?;
let review = generation::stable_id(&json!([
prepared.review,
request.shared_revision,
request.text
]))
.to_string();
if apply {
ensure(
prepared.issues.is_empty(),
crate::i18n::text("transfer_fix_errors"),
)?;
ensure(
request.review_id.as_deref() == Some(review.as_str()),
crate::i18n::text("err_the_preview_is_stale_check_the_updated_impact_be_85cacd"),
)?;
return db
.save_shared_draft(
workspace,
actor,
request.shared_revision,
serde_json::to_value(draft)?,
)
.await;
}
let contacts: Vec<_> = prepared
.record
.document
.people
.iter()
.filter(|p| record.document.person(p.id).is_none() && p.user_id.is_none())
.collect();
Ok(json!({"review_id":review,"inputs":inputs,"contacts":contacts,"preview":prepared.report}))
}