1use serde::Serialize;
4use serde_json::{Value, json};
5use uuid::Uuid;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
9#[serde(rename_all = "snake_case")]
10pub enum EditErrorCode {
11 InvalidOp,
12 UnknownField,
13 MissingField,
14 FieldNotAllowed,
15 InvalidValue,
16 InvalidPath,
17 NotFound,
18 NumericIndex,
19 PositionRequired,
20 PositionNeedsChildren,
21 PositionNeedsSibling,
22 InvalidRange,
23 MoveIntoItself,
24 LabelInvalid,
25 LabelReused,
26 LabelUndefined,
27 UnknownIds,
28 MissingIds,
29 DuplicateIds,
30 ExerciseReadOnly,
31 NotATextBlock,
32 TextNotFound,
33 TextAmbiguous,
34 SplitInsideMarkup,
35 MergeNotAdjacent,
36 ReadOnlyField,
37 DerivedField,
38 WrongTool,
39 PinnedFrontPage,
40 DefaultModule,
41 ModuleNotEmpty,
42 ChapterNeedsPage,
43 DuplicatePath,
44 InvalidUrlPath,
45 InvariantViolated,
46 ExpectedCountMismatch,
47 InvalidRegex,
48 ReorderMismatch,
49 CitationKey,
50 InvalidBibtex,
51 DuplicateTerm,
52 TooLarge,
54 InvalidBlocks,
57}
58
59#[derive(Debug, Clone, PartialEq, Serialize)]
62pub struct EditError {
63 pub code: EditErrorCode,
64 pub message: String,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub op: Option<usize>,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub fix: Option<String>,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub details: Option<Value>,
74}
75
76impl EditError {
77 pub fn new(code: EditErrorCode, message: impl Into<String>) -> Self {
79 Self {
80 code,
81 message: message.into(),
82 op: None,
83 fix: None,
84 details: None,
85 }
86 }
87
88 pub fn with_fix(mut self, fix: impl Into<String>) -> Self {
90 self.fix = Some(fix.into());
91 self
92 }
93
94 pub fn with_details(mut self, details: Value) -> Self {
96 self.details = Some(details);
97 self
98 }
99
100 pub fn at_op(mut self, index: usize) -> Self {
102 self.op.get_or_insert(index);
103 self
104 }
105
106 pub(crate) fn numeric_index(path: &str, items: &str) -> Self {
109 Self::new(
110 EditErrorCode::NumericIndex,
111 format!("\"{path}\" uses a numeric index; {items} are addressed by id"),
112 )
113 }
114
115 pub(crate) fn expected_count_mismatch(
118 expected: usize,
119 found: usize,
120 matches: Vec<Value>,
121 ) -> Self {
122 Self::new(
123 EditErrorCode::ExpectedCountMismatch,
124 format!("expected {expected} matches but found {found}; nothing was changed"),
125 )
126 .with_details(json!({ "matches": matches }))
127 }
128
129 pub(crate) fn position_required(fix: &str) -> Self {
131 Self::new(
132 EditErrorCode::PositionRequired,
133 "this op needs \"position\"",
134 )
135 .with_fix(fix)
136 }
137
138 pub(crate) fn reversed_range(first: Uuid, last: Uuid, siblings: Vec<String>) -> Self {
141 Self::new(
142 EditErrorCode::InvalidRange,
143 format!("the range is reversed: /{last} comes before /{first}"),
144 )
145 .with_fix(format!("swap the ends: from /{last} through /{first}"))
146 .with_details(json!({ "siblings_in_order": siblings }))
147 }
148}
149
150impl std::fmt::Display for EditError {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 match self.op {
153 Some(op) => write!(f, "op {op}: {}", self.message)?,
154 None => write!(f, "{}", self.message)?,
155 }
156 if let Some(fix) = &self.fix {
157 write!(f, " Fix: {fix}")?;
158 }
159 Ok(())
160 }
161}
162
163impl std::error::Error for EditError {}
164
165#[derive(Debug, Clone, PartialEq, Serialize)]
167pub struct Warning {
168 pub code: WarningCode,
169 pub message: String,
170 #[serde(skip_serializing_if = "Option::is_none")]
171 pub op: Option<usize>,
172}
173
174impl Warning {
175 pub fn new(code: WarningCode, message: impl Into<String>) -> Self {
177 Self {
178 code,
179 message: message.into(),
180 op: None,
181 }
182 }
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
187#[serde(rename_all = "snake_case")]
188pub enum WarningCode {
189 IgnoredClientId,
191 ExerciseCopied,
193 NewPageHidden,
195 UrlPathAdjusted,
197 OwnExerciseDeadline,
199 DeadlineOutsideChapterWindow,
201}