Skip to main content

dotloom_engine/
error.rs

1//! Engine errors and solve diagnostics as reported to hosts.
2
3use dotloom_constraints::{Certainty, DiagnosticKind, Status};
4use dotloom_document::{ConstraintId, EntityId};
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8use crate::CommandError;
9
10/// A solver diagnostic mapped back to document objects.
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct DiagnosticReport {
14    /// Category.
15    pub kind: DiagnosticKind,
16    /// Certain or suspected.
17    pub certainty: Certainty,
18    /// Document constraints involved.
19    pub constraints: Vec<ConstraintId>,
20    /// Plugin template rules involved `(entity, label)`.
21    pub templates: Vec<(EntityId, String)>,
22    /// Typed edits involved `(entity, parameter)`.
23    pub edits: Vec<(EntityId, String)>,
24    /// Entities involved.
25    pub entities: Vec<EntityId>,
26    /// Rule labels.
27    pub labels: Vec<String>,
28    /// Largest residual (model units / radians).
29    pub residual: Option<f64>,
30    /// Developer message (UIs format their own text from the fields).
31    pub message: String,
32}
33
34/// Nearest feasible value for an edit that could not be satisfied.
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36#[serde(rename_all = "camelCase")]
37pub struct NearestValue {
38    /// Entity.
39    pub entity: EntityId,
40    /// Parameter.
41    pub param: String,
42    /// Requested value.
43    pub requested: f64,
44    /// Closest value that satisfies every hard rule.
45    pub feasible: f64,
46}
47
48/// Why solving rejected a transaction.
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase")]
51pub struct SolveFailure {
52    /// Aggregate status.
53    pub status: Status,
54    /// Diagnostics.
55    pub diagnostics: Vec<DiagnosticReport>,
56    /// Nearest feasible values of the requested edits (linear conflicts).
57    pub nearest: Vec<NearestValue>,
58}
59
60/// Engine errors. A failed request never changes the document.
61#[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)]
62#[serde(tag = "code", rename_all = "camelCase", rename_all_fields = "camelCase")]
63#[non_exhaustive]
64pub enum EngineError {
65    /// `expectedRevision` does not match.
66    #[error("stale request: expected revision {expected}, document is at {actual}")]
67    Stale {
68        /// Expected.
69        expected: u64,
70        /// Actual.
71        actual: u64,
72    },
73    /// Another interaction (drag or pending solve) is in progress.
74    #[error("engine busy: {reason}")]
75    Busy {
76        /// Reason.
77        reason: String,
78    },
79    /// A command was rejected.
80    #[error("{error}")]
81    Command {
82        /// Error.
83        error: CommandError,
84    },
85    /// The resulting document would violate an invariant.
86    #[error("invalid result: {message}")]
87    Invalid {
88        /// Explanation.
89        message: String,
90    },
91    /// Hard rules could not be satisfied.
92    #[error("constraints not satisfied: {:?}", failure.status)]
93    Solve {
94        /// Details.
95        failure: SolveFailure,
96    },
97    /// The independent check found a violated hard rule after solving.
98    #[error("independent check failed for {what}: residual {residual:e} > {tolerance:e}")]
99    Validation {
100        /// Rule.
101        what: String,
102        /// Residual.
103        residual: f64,
104        /// Tolerance.
105        tolerance: f64,
106    },
107    /// The operation was cancelled.
108    #[error("cancelled")]
109    Cancelled,
110    /// Nothing to undo.
111    #[error("nothing to undo")]
112    NothingToUndo,
113    /// Nothing to redo.
114    #[error("nothing to redo")]
115    NothingToRedo,
116    /// No interaction of the requested kind is active.
117    #[error("no active {what}")]
118    NotActive {
119        /// What.
120        what: String,
121    },
122    /// Loading a document failed.
123    #[error("cannot load document: {message}")]
124    Load {
125        /// Explanation.
126        message: String,
127    },
128    /// Plugin registration failed.
129    #[error("plugin error: {message}")]
130    Plugin {
131        /// Explanation.
132        message: String,
133    },
134}
135
136impl From<CommandError> for EngineError {
137    fn from(error: CommandError) -> Self {
138        Self::Command { error }
139    }
140}
141
142impl EngineError {
143    /// Stable machine-readable code.
144    #[must_use]
145    pub fn code(&self) -> &'static str {
146        match self {
147            Self::Stale { .. } => "stale",
148            Self::Busy { .. } => "busy",
149            Self::Command { .. } => "command",
150            Self::Invalid { .. } => "invalid",
151            Self::Solve { .. } => "solve",
152            Self::Validation { .. } => "validation",
153            Self::Cancelled => "cancelled",
154            Self::NothingToUndo => "nothingToUndo",
155            Self::NothingToRedo => "nothingToRedo",
156            Self::NotActive { .. } => "notActive",
157            Self::Load { .. } => "load",
158            Self::Plugin { .. } => "plugin",
159        }
160    }
161
162    /// Solve status if this is a solve failure.
163    #[must_use]
164    pub fn solve_status(&self) -> Option<Status> {
165        match self {
166            Self::Solve { failure } => Some(failure.status),
167            _ => None,
168        }
169    }
170}