use serde::{Deserialize, Serialize};
use solverforge::{
HardSoftScore, SolverLifecycleState, SolverSnapshot, SolverSnapshotAnalysis, SolverStatus,
SolverTelemetry, SolverTerminalReason,
};
use crate::domain::{Container, Item, Plan};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ItemDto {
pub id: String,
pub name: String,
}
impl From<&Item> for ItemDto {
fn from(item: &Item) -> Self {
Self {
id: item.id.clone(),
name: item.name.clone(),
}
}
}
impl ItemDto {
pub fn to_item(&self) -> Item {
Item::new(&self.id, &self.name)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContainerDto {
pub id: String,
pub name: String,
pub items: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanDto {
pub items: Vec<ItemDto>,
pub containers: Vec<ContainerDto>,
#[serde(default)]
pub score: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ConstraintAnalysisDto {
pub name: String,
pub weight: String,
pub score: String,
pub match_count: usize,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AnalyzeResponse {
pub score: String,
pub constraints: Vec<ConstraintAnalysisDto>,
}
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TelemetryDto {
pub elapsed_ms: u64,
pub step_count: u64,
pub moves_evaluated: u64,
pub moves_accepted: u64,
pub score_calculations: u64,
pub moves_per_second: u64,
pub acceptance_rate: f64,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct JobSummaryDto {
pub id: String,
pub job_id: String,
pub lifecycle_state: &'static str,
pub terminal_reason: Option<&'static str>,
pub checkpoint_available: bool,
pub event_sequence: u64,
pub snapshot_revision: Option<u64>,
pub current_score: Option<String>,
pub best_score: Option<String>,
pub telemetry: TelemetryDto,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct JobSnapshotDto {
pub id: String,
pub job_id: String,
pub snapshot_revision: u64,
pub lifecycle_state: &'static str,
pub terminal_reason: Option<&'static str>,
pub current_score: Option<String>,
pub best_score: Option<String>,
pub telemetry: TelemetryDto,
pub solution: PlanDto,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct JobAnalysisDto {
pub id: String,
pub job_id: String,
pub snapshot_revision: u64,
pub lifecycle_state: &'static str,
pub terminal_reason: Option<&'static str>,
pub analysis: AnalyzeResponse,
}
impl PlanDto {
pub fn from_plan(plan: &Plan) -> Self {
let items: Vec<ItemDto> = plan.item_facts.iter().map(ItemDto::from).collect();
let containers: Vec<ContainerDto> = plan
.containers
.iter()
.map(|container| ContainerDto {
id: container.id.clone(),
name: container.name.clone(),
items: container
.items
.iter()
.filter_map(|&idx| plan.item_facts.get(idx))
.map(|item| item.id.clone())
.collect(),
})
.collect();
Self {
items,
containers,
score: plan.score.map(|score| score.to_string()),
}
}
pub fn to_domain(&self) -> Plan {
let item_facts: Vec<Item> = self.items.iter().map(ItemDto::to_item).collect();
let id_to_idx: std::collections::HashMap<&str, usize> = item_facts
.iter()
.enumerate()
.map(|(idx, item)| (item.id.as_str(), idx))
.collect();
let containers: Vec<Container> = self
.containers
.iter()
.map(|container| {
let items: Vec<usize> = container
.items
.iter()
.filter_map(|id| id_to_idx.get(id.as_str()).copied())
.collect();
Container {
id: container.id.clone(),
name: container.name.clone(),
items,
}
})
.collect();
Plan::new(item_facts, containers)
}
}
impl TelemetryDto {
pub fn from_runtime(telemetry: SolverTelemetry) -> Self {
Self {
elapsed_ms: telemetry.elapsed_ms,
step_count: telemetry.step_count,
moves_evaluated: telemetry.moves_evaluated,
moves_accepted: telemetry.moves_accepted,
score_calculations: telemetry.score_calculations,
moves_per_second: telemetry.moves_per_second,
acceptance_rate: telemetry.acceptance_rate,
}
}
}
impl JobSummaryDto {
pub fn from_status(job_id: usize, status: &SolverStatus<HardSoftScore>) -> Self {
Self {
id: job_id.to_string(),
job_id: job_id.to_string(),
lifecycle_state: lifecycle_state_label(status.lifecycle_state),
terminal_reason: status.terminal_reason.map(terminal_reason_label),
checkpoint_available: status.checkpoint_available,
event_sequence: status.event_sequence,
snapshot_revision: status.latest_snapshot_revision,
current_score: status.current_score.map(|score| score.to_string()),
best_score: status.best_score.map(|score| score.to_string()),
telemetry: TelemetryDto::from_runtime(status.telemetry),
}
}
}
impl JobSnapshotDto {
pub fn from_snapshot(snapshot: &SolverSnapshot<Plan>) -> Self {
Self {
id: snapshot.job_id.to_string(),
job_id: snapshot.job_id.to_string(),
snapshot_revision: snapshot.snapshot_revision,
lifecycle_state: lifecycle_state_label(snapshot.lifecycle_state),
terminal_reason: snapshot.terminal_reason.map(terminal_reason_label),
current_score: snapshot.current_score.map(|score| score.to_string()),
best_score: snapshot.best_score.map(|score| score.to_string()),
telemetry: TelemetryDto::from_runtime(snapshot.telemetry),
solution: PlanDto::from_plan(&snapshot.solution),
}
}
}
impl JobAnalysisDto {
pub fn from_snapshot_analysis(
snapshot: &SolverSnapshotAnalysis<HardSoftScore>,
analysis: AnalyzeResponse,
) -> Self {
Self {
id: snapshot.job_id.to_string(),
job_id: snapshot.job_id.to_string(),
snapshot_revision: snapshot.snapshot_revision,
lifecycle_state: lifecycle_state_label(snapshot.lifecycle_state),
terminal_reason: snapshot.terminal_reason.map(terminal_reason_label),
analysis,
}
}
}
pub fn analysis_response(analysis: &solverforge::ScoreAnalysis<HardSoftScore>) -> AnalyzeResponse {
AnalyzeResponse {
score: analysis.score.to_string(),
constraints: analysis
.constraints
.iter()
.map(|constraint| ConstraintAnalysisDto {
name: constraint.name.clone(),
weight: constraint.weight.to_string(),
score: constraint.score.to_string(),
match_count: constraint.match_count,
})
.collect(),
}
}
pub fn lifecycle_state_label(state: SolverLifecycleState) -> &'static str {
match state {
SolverLifecycleState::Solving => "SOLVING",
SolverLifecycleState::PauseRequested => "PAUSE_REQUESTED",
SolverLifecycleState::Paused => "PAUSED",
SolverLifecycleState::Completed => "COMPLETED",
SolverLifecycleState::Cancelled => "CANCELLED",
SolverLifecycleState::Failed => "FAILED",
}
}
pub fn terminal_reason_label(reason: SolverTerminalReason) -> &'static str {
match reason {
SolverTerminalReason::Completed => "completed",
SolverTerminalReason::TerminatedByConfig => "terminated_by_config",
SolverTerminalReason::Cancelled => "cancelled",
SolverTerminalReason::Failed => "failed",
}
}