1use arvalez_ir::CoreIr;
2
3#[derive(Debug, Clone)]
5pub struct OpenApiDiagnostic {
6 pub kind: DiagnosticKind,
8 pub pointer: Option<String>,
10 pub source_preview: Option<String>,
12 pub context: Option<String>,
14 pub line: Option<usize>,
16}
17
18impl OpenApiDiagnostic {
19 pub fn from_pointer(
20 kind: DiagnosticKind,
21 pointer: impl Into<String>,
22 source_preview: Option<String>,
23 line: Option<usize>,
24 ) -> Self {
25 OpenApiDiagnostic {
26 kind,
27 pointer: Some(pointer.into()),
28 source_preview,
29 context: None,
30 line,
31 }
32 }
33
34 pub fn from_named_context(kind: DiagnosticKind, context: impl Into<String>) -> Self {
35 OpenApiDiagnostic {
36 kind,
37 pointer: None,
38 source_preview: None,
39 context: Some(context.into()),
40 line: None,
41 }
42 }
43
44 pub fn simple(kind: DiagnosticKind) -> Self {
45 OpenApiDiagnostic {
46 kind,
47 pointer: None,
48 source_preview: None,
49 context: None,
50 line: None,
51 }
52 }
53
54 pub fn note(&self) -> Option<&str> {
56 let note = self.kind.note_text();
57 if note.is_empty() { None } else { Some(note) }
58 }
59
60 pub fn classify(&self) -> (&'static str, String) {
66 match &self.kind {
67 DiagnosticKind::UnknownSchemaKeyword { keyword } => {
68 ("unsupported_schema_keyword", keyword.clone())
69 }
70 DiagnosticKind::UnsupportedSchemaKeyword { keyword } => (
71 Self::unsupported_kind_for_pointer(self.pointer.as_deref(), keyword),
72 keyword.clone(),
73 ),
74 DiagnosticKind::UnsupportedSchemaType { schema_type } => {
75 ("unsupported_schema_type", schema_type.clone())
76 }
77 DiagnosticKind::UnsupportedSchemaShape => (
78 "unsupported_schema_shape",
79 self.pointer
80 .as_deref()
81 .map(diagnostic_pointer_tail)
82 .unwrap_or_else(|| "schema_shape".into()),
83 ),
84 DiagnosticKind::UnsupportedReference { reference } => {
85 ("unsupported_reference", categorize_reference(reference))
86 }
87 DiagnosticKind::AllOfRecursiveCycle { .. } => {
88 ("unsupported_all_of_merge", "recursive_cycle".into())
89 }
90 DiagnosticKind::RecursiveParameterCycle { .. } => (
91 "invalid_openapi_document",
92 "recursive_parameter_cycle".into(),
93 ),
94 DiagnosticKind::RecursiveRequestBodyCycle { .. } => (
95 "invalid_openapi_document",
96 "recursive_request_body_cycle".into(),
97 ),
98 DiagnosticKind::IncompatibleAllOfField { field } => {
99 ("unsupported_all_of_merge", field.clone())
100 }
101 DiagnosticKind::EmptyRequestBodyContent => {
102 ("unsupported_request_body_shape", "empty_content".into())
103 }
104 DiagnosticKind::EmptyParameterName { .. } => {
105 ("invalid_openapi_document", "empty_parameter_name".into())
106 }
107 DiagnosticKind::EmptyPropertyKey { .. } => {
108 ("invalid_openapi_document", "empty_property_key".into())
109 }
110 DiagnosticKind::ParameterMissingSchema { name } => (
111 "invalid_openapi_document",
112 normalize_diagnostic_feature(name),
113 ),
114 DiagnosticKind::UnsupportedParameterLocation { name } => (
115 "invalid_openapi_document",
116 normalize_diagnostic_feature(name),
117 ),
118 DiagnosticKind::MultipleRequestBodyDeclarations { .. } => (
119 "invalid_openapi_document",
120 "multiple_request_body_declarations".into(),
121 ),
122 DiagnosticKind::BodyParameterMissingSchema { name } => (
123 "invalid_openapi_document",
124 normalize_diagnostic_feature(name),
125 ),
126 DiagnosticKind::FormDataParameterMissingSchema { name } => (
127 "invalid_openapi_document",
128 normalize_diagnostic_feature(name),
129 ),
130 }
131 }
132
133 pub fn unsupported_kind_for_pointer(pointer: Option<&str>, feature: &str) -> &'static str {
134 if matches!(
135 feature,
136 "allOf" | "anyOf" | "oneOf" | "not" | "discriminator" | "const"
137 ) {
138 return "unsupported_schema_keyword";
139 }
140 match pointer {
141 Some(p)
142 if p.contains("/components/schemas/")
143 || p.contains("/properties/")
144 || p.ends_with("/schema")
145 || p.contains("/items/") =>
146 {
147 "unsupported_schema_keyword"
148 }
149 Some(p) if p.contains("/parameters/") => "unsupported_parameter_feature",
150 Some(p) if p.contains("/responses/") => "unsupported_response_feature",
151 Some(p) if p.contains("/requestBody/") => "unsupported_request_body_feature",
152 _ => "unsupported_feature",
153 }
154 }
155}
156
157pub fn categorize_reference(reference: &str) -> String {
158 if !reference.starts_with('#') {
160 return "external".into();
161 }
162 let inner = reference.strip_prefix("#/").unwrap_or("");
165 let structural: Vec<&str> = inner
166 .split('/')
167 .filter(|s| !s.is_empty() && !s.chars().all(|c| c.is_ascii_digit()) && !s.contains('~') && !s.contains('%'))
168 .take(2)
169 .collect();
170 if structural.is_empty() {
171 return "unknown".into();
172 }
173 structural.join("_")
174}
175
176pub fn diagnostic_pointer_tail(pointer: &str) -> String {
177 pointer
178 .trim_end_matches('/')
179 .rsplit('/')
180 .next()
181 .map(normalize_diagnostic_feature)
182 .unwrap_or_else(|| "schema_shape".into())
183}
184
185pub fn normalize_diagnostic_feature(value: &str) -> String {
186 value
187 .replace("~1", "/")
188 .replace("~0", "~")
189 .replace('.', "_")
190 .replace('/', "_")
191 .replace('`', "")
192}
193
194impl std::fmt::Display for OpenApiDiagnostic {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 let message = self.kind.message_text();
197 let note = self.kind.note_text();
198
199 if let Some(pointer) = &self.pointer {
200 write!(f, "OpenAPI document issue\nCaused by:\n {message}")?;
201 write!(f, "\n location: {pointer}")?;
202 if let Some(preview) = &self.source_preview {
203 write!(f, "\n preview:")?;
204 for line in preview.lines() {
205 write!(f, "\n {line}")?;
206 }
207 }
208 if !note.is_empty() {
209 write!(f, "\n note: {note}")?;
210 }
211 } else if let Some(context) = &self.context {
212 write!(f, "{context}: {message}")?;
213 if !note.is_empty() {
214 write!(f, "\nnote: {note}")?;
215 }
216 } else {
217 write!(f, "{message}")?;
218 if !note.is_empty() {
219 write!(f, "\nnote: {note}")?;
220 }
221 }
222 Ok(())
223 }
224}
225
226impl std::error::Error for OpenApiDiagnostic {}
227
228#[derive(Debug, Clone)]
230pub enum DiagnosticKind {
231 UnknownSchemaKeyword { keyword: String },
234 UnsupportedSchemaKeyword { keyword: String },
236 UnsupportedSchemaType { schema_type: String },
238 UnsupportedSchemaShape,
240 UnsupportedReference { reference: String },
243 AllOfRecursiveCycle { reference: String },
245 RecursiveParameterCycle { reference: String },
247 RecursiveRequestBodyCycle { reference: String },
249 IncompatibleAllOfField { field: String },
252 EmptyRequestBodyContent,
255 EmptyParameterName { counter: usize },
257 EmptyPropertyKey { counter: usize },
259 ParameterMissingSchema { name: String },
261 UnsupportedParameterLocation { name: String },
263 MultipleRequestBodyDeclarations { note: String },
265 BodyParameterMissingSchema { name: String },
267 FormDataParameterMissingSchema { name: String },
269}
270
271impl DiagnosticKind {
272 fn message_text(&self) -> String {
273 match self {
274 Self::UnknownSchemaKeyword { keyword } => format!("unknown schema keyword `{keyword}`"),
275 Self::UnsupportedSchemaKeyword { keyword } => {
276 format!("`{keyword}` is not supported yet")
277 }
278 Self::UnsupportedSchemaType { schema_type } => {
279 format!("unsupported schema type `{schema_type}`")
280 }
281 Self::UnsupportedSchemaShape => "schema shape is not supported yet".into(),
282 Self::UnsupportedReference { reference } => {
283 format!("unsupported reference `{reference}`")
284 }
285 Self::AllOfRecursiveCycle { reference } => {
286 format!("`allOf` contains a recursive reference cycle involving `{reference}`")
287 }
288 Self::RecursiveParameterCycle { reference } => {
289 format!("parameter reference contains a recursive cycle involving `{reference}`")
290 }
291 Self::RecursiveRequestBodyCycle { reference } => {
292 format!("request body reference contains a recursive cycle involving `{reference}`")
293 }
294 Self::IncompatibleAllOfField { field } => {
295 format!("`allOf` contains incompatible `{field}` declarations")
296 }
297 Self::EmptyRequestBodyContent => "request body has no content entries".into(),
298 Self::EmptyParameterName { counter } => {
299 format!("parameter #{counter} has an empty name")
300 }
301 Self::EmptyPropertyKey { counter } => format!("property #{counter} has an empty name"),
302 Self::ParameterMissingSchema { .. } => "parameter has no schema or type".into(),
303 Self::UnsupportedParameterLocation { .. } => "unsupported parameter location".into(),
304 Self::MultipleRequestBodyDeclarations { .. } => {
305 "multiple request body declarations are not supported".into()
306 }
307 Self::BodyParameterMissingSchema { .. } => "body parameter has no schema".into(),
308 Self::FormDataParameterMissingSchema { .. } => {
309 "formData parameter has no schema or type".into()
310 }
311 }
312 }
313
314 fn note_text(&self) -> &str {
315 match self {
316 Self::UnknownSchemaKeyword { .. }
317 | Self::UnsupportedSchemaKeyword { .. }
318 | Self::UnsupportedSchemaType { .. }
319 | Self::UnsupportedSchemaShape
320 | Self::AllOfRecursiveCycle { .. }
321 | Self::IncompatibleAllOfField { .. }
322 | Self::EmptyParameterName { .. }
323 | Self::EmptyPropertyKey { .. } => {
324 "Use `--ignore-unhandled` to turn this into a warning while keeping generation going."
325 }
326 Self::EmptyRequestBodyContent => {
327 "Arvalez defaulted this request body to `application/octet-stream` with an untyped payload."
328 }
329 Self::ParameterMissingSchema { .. } => {
330 "Arvalez currently expects non-body parameters to declare either `schema` (OpenAPI 3) or `type` (Swagger 2)."
331 }
332 Self::UnsupportedParameterLocation { .. } => {
333 "Arvalez currently supports path, query, header, and cookie parameters here."
334 }
335 Self::MultipleRequestBodyDeclarations { note } => note.as_str(),
336 Self::BodyParameterMissingSchema { .. } => {
337 "Swagger 2 `in: body` parameters must declare a `schema`."
338 }
339 Self::FormDataParameterMissingSchema { .. } => {
340 "Swagger 2 `in: formData` parameters must declare either a `type` or a `schema`."
341 }
342 Self::RecursiveParameterCycle { .. } => {
343 "Arvalez only supports acyclic parameter references."
344 }
345 Self::RecursiveRequestBodyCycle { .. } => {
346 "Arvalez only supports acyclic local `requestBody` references."
347 }
348 Self::UnsupportedReference { .. } => "",
349 }
350 }
351}
352
353#[derive(Debug, Clone)]
354pub struct OpenApiLoadResult {
355 pub ir: CoreIr,
356 pub warnings: Vec<OpenApiDiagnostic>,
357}