Files
meguri/src/generation_service.rs
T

164 lines
5.2 KiB
Rust
Raw Normal View History

2026-09-24 14:35:05 +03:00
//! 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;
use serde_json::{Value, json};
use uuid::Uuid;
#[derive(Debug, Clone, Deserialize)]
pub struct Draft {
pub revision: 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?;
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(Person {
id: user.id,
user_id: Some(user.id),
display_name: user.display_name.clone(),
email: None,
country: Some(user.country.clone()),
aliases: vec![],
2026-09-24 14:35:05 +03:00
});
}
}
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 report = json!({"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}))
}