wowlab_engine_domain/rotation/validate/
errors.rs1use std::fmt::Write as _;
2
3use serde::{Deserialize, Serialize};
4#[cfg(feature = "wasm")]
5use tsify::Tsify;
6
7#[derive(Clone, Debug, Deserialize, Serialize)]
9#[serde(rename_all = "camelCase")]
10#[cfg_attr(feature = "wasm", derive(Tsify))]
11#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
12pub struct ValidationResult {
13 pub valid: bool,
14 pub errors: Vec<ValidationError>,
15 pub warnings: Vec<ValidationWarning>,
16}
17
18impl ValidationResult {
19 #[must_use]
21 pub fn ok() -> Self {
22 Self {
23 valid: true,
24 errors: Vec::new(),
25 warnings: Vec::new(),
26 }
27 }
28
29 #[must_use]
31 pub fn with_errors(errors: Vec<ValidationError>) -> Self {
32 Self {
33 valid: errors.is_empty(),
34 errors,
35 warnings: Vec::new(),
36 }
37 }
38}
39
40#[derive(Clone, Debug, Deserialize, Serialize, thiserror::Error)]
42#[serde(tag = "type", rename_all = "camelCase")]
43#[cfg_attr(feature = "wasm", derive(Tsify))]
44#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
45#[non_exhaustive]
46pub enum ValidationError {
48 #[error("undefined variable '{name}'")]
49 UndefinedVariable { name: String },
50 #[error("undefined list '{name}'")]
51 UndefinedList { name: String },
52 #[error("circular list reference: {}", path.join(" -> "))]
53 CircularReference { path: Vec<String> },
54 #[error("action list '{list_name}' is empty")]
55 EmptyActionList { list_name: String },
56 #[error(
57 "invalid expression: {message}{context}",
58 context = invalid_expression_context(
59 list_name.as_deref(),
60 action_index.as_ref().copied(),
61 slug.as_deref()
62 )
63 )]
64 InvalidExpression {
65 message: String,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 list_name: Option<String>,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 action_index: Option<usize>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
71 slug: Option<String>,
72 },
73 #[error("duplicate variable '{name}'")]
74 DuplicateVariable { name: String },
75 #[error("duplicate list '{name}'")]
76 DuplicateList { name: String },
77 #[error(
78 "type mismatch on '{name}' ({op}): expected {expected}, got {got}{context}",
79 context = type_mismatch_context(list_name.as_deref(), action_index.as_ref().copied())
80 )]
81 TypeMismatch {
82 name: String,
83 op: String,
84 expected: String,
85 got: String,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
87 list_name: Option<String>,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 action_index: Option<usize>,
90 },
91 #[error("unknown field '{name}' in domain '{domain}'")]
92 UnknownField { domain: String, name: String },
93 #[error("condition nesting depth {depth} exceeds maximum {max}")]
94 MaxDepthExceeded { depth: usize, max: usize },
95 #[error("expanded action count {actions} exceeds maximum {max}")]
96 ActionExpansionLimitExceeded { actions: usize, max: usize },
97 #[error("unsupported rotation syntax: {construct}")]
98 UnsupportedSyntax { construct: String },
99}
100
101fn invalid_expression_context(
102 list_name: Option<&str>,
103 action_index: Option<usize>,
104 slug: Option<&str>,
105) -> String {
106 let Some(list_name) = list_name else {
107 return String::new();
108 };
109 let mut context = format!(" (list '{list_name}'");
110
111 if let Some(index) = action_index {
112 let _ = write!(context, ", action {index}");
113 }
114
115 if let Some(slug) = slug {
116 let _ = write!(context, ", spell '{slug}'");
117 }
118
119 context.push(')');
120
121 context
122}
123
124fn type_mismatch_context(list_name: Option<&str>, action_index: Option<usize>) -> String {
125 let Some(list_name) = list_name else {
126 return String::new();
127 };
128 let mut context = format!(" (list '{list_name}'");
129
130 if let Some(index) = action_index {
131 let _ = write!(context, ", action {index}");
132 }
133
134 context.push(')');
135
136 context
137}
138
139#[derive(Clone, Debug, Deserialize, Serialize)]
141#[serde(tag = "type", rename_all = "camelCase")]
142#[cfg_attr(feature = "wasm", derive(Tsify))]
143#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
144#[non_exhaustive]
145pub enum ValidationWarning {
146 UnusedVariable { name: String },
147 UnusedList { name: String },
148 ConstantCondition { value: bool, location: String },
149}