Skip to main content

weavatrix_refactor_plan/
error.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3use weavatrix_edit::{EditError, ErrorCode as EditErrorCode};
4
5/// Stable categories for refactor-plan profile failures.
6#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
7#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
8pub enum PlanErrorCode {
9    BasePlanInvalid,
10    SchemaMismatch,
11    InvalidPlan,
12    EvidenceMalformed,
13    EvidenceMissing,
14    EvidenceInvalid,
15    EvidenceTooLarge,
16    PlanTooLarge,
17    UnsafePath,
18    OperationConflict,
19    ExtensionConflict,
20    UnsafeNumber,
21    JsonEncoding,
22    NotTextOnly,
23}
24
25impl PlanErrorCode {
26    /// Returns the wire-compatible code.
27    #[must_use]
28    pub const fn as_str(self) -> &'static str {
29        match self {
30            Self::BasePlanInvalid => "BASE_PLAN_INVALID",
31            Self::SchemaMismatch => "SCHEMA_MISMATCH",
32            Self::InvalidPlan => "INVALID_PLAN",
33            Self::EvidenceMalformed => "EVIDENCE_MALFORMED",
34            Self::EvidenceMissing => "EVIDENCE_MISSING",
35            Self::EvidenceInvalid => "EVIDENCE_INVALID",
36            Self::EvidenceTooLarge => "EVIDENCE_TOO_LARGE",
37            Self::PlanTooLarge => "PLAN_TOO_LARGE",
38            Self::UnsafePath => "UNSAFE_PATH",
39            Self::OperationConflict => "OPERATION_CONFLICT",
40            Self::ExtensionConflict => "EXTENSION_CONFLICT",
41            Self::UnsafeNumber => "UNSAFE_NUMBER",
42            Self::JsonEncoding => "JSON_ENCODING",
43            Self::NotTextOnly => "NOT_TEXT_ONLY",
44        }
45    }
46}
47
48impl fmt::Display for PlanErrorCode {
49    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50        formatter.write_str(self.as_str())
51    }
52}
53
54/// A bounded, fail-closed plan profile error.
55#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
56#[serde(rename_all = "camelCase")]
57pub struct PlanError {
58    code: PlanErrorCode,
59    message: Box<str>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    field: Option<Box<str>>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    operation_index: Option<usize>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    edit_index: Option<usize>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    related_edit_index: Option<usize>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    path: Option<Box<str>>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    base_code: Option<EditErrorCode>,
72}
73
74impl PlanError {
75    pub(crate) fn new(code: PlanErrorCode, message: impl Into<String>) -> Self {
76        Self {
77            code,
78            message: message.into().into_boxed_str(),
79            field: None,
80            operation_index: None,
81            edit_index: None,
82            related_edit_index: None,
83            path: None,
84            base_code: None,
85        }
86    }
87
88    pub(crate) fn at_field(mut self, field: impl Into<String>) -> Self {
89        self.field = Some(field.into().into_boxed_str());
90        self
91    }
92
93    pub(crate) const fn at_operation(mut self, operation_index: usize) -> Self {
94        self.operation_index = Some(operation_index);
95        self
96    }
97
98    pub(crate) fn at_path(mut self, path: impl Into<String>) -> Self {
99        self.path = Some(path.into().into_boxed_str());
100        self
101    }
102
103    pub(crate) const fn with_code(mut self, code: PlanErrorCode) -> Self {
104        self.code = code;
105        self
106    }
107
108    pub(crate) fn from_edit(error: &EditError) -> Self {
109        Self {
110            code: PlanErrorCode::BasePlanInvalid,
111            message: error.message().into(),
112            field: None,
113            operation_index: None,
114            edit_index: error.edit_index(),
115            related_edit_index: error.related_edit_index(),
116            path: None,
117            base_code: Some(error.code()),
118        }
119    }
120
121    /// Returns the stable plan-profile error code.
122    #[must_use]
123    pub const fn code(&self) -> PlanErrorCode {
124        self.code
125    }
126
127    /// Returns the human-readable explanation.
128    #[must_use]
129    pub fn message(&self) -> &str {
130        &self.message
131    }
132
133    /// Returns the annotation field associated with the failure, when available.
134    #[must_use]
135    pub fn field(&self) -> Option<&str> {
136        self.field.as_deref()
137    }
138
139    /// Returns the logical operation index associated with the failure.
140    #[must_use]
141    pub const fn operation_index(&self) -> Option<usize> {
142        self.operation_index
143    }
144
145    /// Returns the edit index within the logical operation, when available.
146    #[must_use]
147    pub const fn edit_index(&self) -> Option<usize> {
148        self.edit_index
149    }
150
151    /// Returns the conflicting edit index within the logical operation, when available.
152    #[must_use]
153    pub const fn related_edit_index(&self) -> Option<usize> {
154        self.related_edit_index
155    }
156
157    /// Returns the operation path associated with the failure.
158    #[must_use]
159    pub fn path(&self) -> Option<&str> {
160        self.path.as_deref()
161    }
162
163    /// Returns the underlying `weavatrix-edit` code for a base-plan failure.
164    #[must_use]
165    pub const fn base_code(&self) -> Option<EditErrorCode> {
166        self.base_code
167    }
168}
169
170impl fmt::Display for PlanError {
171    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
172        write!(formatter, "{}: {}", self.code, self.message)
173    }
174}
175
176impl std::error::Error for PlanError {}