1use std::{fmt, path::PathBuf};
2use thiserror::Error;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct FlowErrorLocation {
6 pub path: Option<String>,
7 pub source_path: Option<PathBuf>,
8 pub line: Option<usize>,
9 pub col: Option<usize>,
10 pub json_pointer: Option<String>,
11}
12
13impl FlowErrorLocation {
14 pub fn new<P: Into<Option<String>>>(path: P, line: Option<usize>, col: Option<usize>) -> Self {
15 FlowErrorLocation {
16 path: path.into(),
17 line,
18 col,
19 source_path: None,
20 json_pointer: None,
21 }
22 }
23
24 pub fn at_path(path: impl Into<String>) -> Self {
25 FlowErrorLocation::new(Some(path.into()), None, None)
26 }
27
28 pub fn at_path_with_position(
29 path: impl Into<String>,
30 line: Option<usize>,
31 col: Option<usize>,
32 ) -> Self {
33 FlowErrorLocation::new(Some(path.into()), line, col)
34 }
35
36 pub fn with_source_path(mut self, source_path: Option<&std::path::Path>) -> Self {
37 self.source_path = source_path.map(|p| p.to_path_buf());
38 self
39 }
40
41 pub fn with_json_pointer(mut self, pointer: Option<impl Into<String>>) -> Self {
42 self.json_pointer = pointer.map(|p| p.into());
43 self
44 }
45
46 pub fn describe(&self) -> Option<String> {
47 if self.path.is_none() && self.line.is_none() && self.col.is_none() {
48 return None;
49 }
50 let mut parts = String::new();
51 if let Some(path) = &self.path {
52 parts.push_str(path);
53 }
54 match (self.line, self.col) {
55 (Some(line), Some(column)) => {
56 if !parts.is_empty() {
57 parts.push(':');
58 }
59 parts.push_str(&format!("{line}:{column}"));
60 }
61 (Some(line), None) => {
62 if !parts.is_empty() {
63 parts.push(':');
64 }
65 parts.push_str(&line.to_string());
66 }
67 (None, Some(column)) => {
68 if !parts.is_empty() {
69 parts.push(':');
70 }
71 parts.push_str(&column.to_string());
72 }
73 _ => {}
74 }
75 Some(parts)
76 }
77}
78
79impl fmt::Display for FlowErrorLocation {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 if self.path.is_none() && self.line.is_none() && self.col.is_none() {
82 return Ok(());
83 }
84 write!(f, " at ")?;
85 if let Some(path) = &self.path {
86 write!(f, "{path}")?;
87 if self.line.is_some() || self.col.is_some() {
88 write!(f, ":")?;
89 }
90 }
91 match (self.line, self.col) {
92 (Some(line), Some(column)) => write!(f, "{line}:{column}")?,
93 (Some(line), None) => write!(f, "{line}")?,
94 (None, Some(column)) => write!(f, "{column}")?,
95 _ => {}
96 }
97 Ok(())
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct SchemaErrorDetail {
103 pub message: String,
104 pub location: FlowErrorLocation,
105}
106
107#[derive(Debug, Error)]
108pub enum FlowError {
109 #[error("YAML parse error{location}: {message}")]
110 Yaml {
111 message: String,
112 location: FlowErrorLocation,
113 },
114 #[error("Schema validation failed{location}:\n{message}")]
115 Schema {
116 message: String,
117 details: Vec<SchemaErrorDetail>,
118 location: FlowErrorLocation,
119 },
120 #[error("Unknown flow type '{flow_type}'{location}")]
121 UnknownFlowType {
122 flow_type: String,
123 location: FlowErrorLocation,
124 },
125 #[error("Invalid identifier for {kind} '{value}'{location}: {detail}")]
126 InvalidIdentifier {
127 kind: &'static str,
128 value: String,
129 detail: String,
130 location: FlowErrorLocation,
131 },
132 #[error(
133 "Node '{node_id}' must contain exactly one component key like 'qa.process' plus optional 'routing'{location}"
134 )]
135 NodeComponentShape {
136 node_id: String,
137 location: FlowErrorLocation,
138 },
139 #[error(
140 "Invalid component key '{component}' in node '{node_id}' (expected namespace.adapter.operation or builtin like 'questions'/'template'){location}"
141 )]
142 BadComponentKey {
143 component: String,
144 node_id: String,
145 location: FlowErrorLocation,
146 },
147 #[error("Invalid routing block in node '{node_id}'{location}: {message}")]
148 Routing {
149 node_id: String,
150 message: String,
151 location: FlowErrorLocation,
152 },
153 #[error("Missing node '{target}' referenced in routing from '{node_id}'{location}")]
154 MissingNode {
155 target: String,
156 node_id: String,
157 location: FlowErrorLocation,
158 },
159 #[error("Internal error{location}: {message}")]
160 Internal {
161 message: String,
162 location: FlowErrorLocation,
163 },
164}
165
166#[allow(clippy::result_large_err)]
167pub type Result<T> = std::result::Result<T, FlowError>;