1use crate::{DiagnosticCode, InvalidDiagnosticCode, Severity};
4
5#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
7#[repr(u32)]
8#[non_exhaustive]
9#[allow(
10 missing_docs,
11 reason = "each variant is documented by its public catalogue definition"
12)]
13pub enum RuleId {
14 ComposeModelInvalid = 1_000_001,
15 ComposeProfileRequired = 1_000_002,
16 ComposeProfileMismatch = 1_000_003,
17 ComposeIntentUnsupported = 1_000_004,
18 ComposeValueInvalid = 1_000_005,
19 ComposeTargetInvalid = 1_000_006,
20 ComposeOutputUnsupported = 1_000_007,
21 ComposeGenerationFailed = 1_000_008,
22 ComposeCompatibilityConstraint = 1_000_009,
23 ComposeUnsetVariable = 1_000_101,
24 ComposeRequiredVariable = 1_000_102,
25 ComposeInterpolationInvalid = 1_000_103,
26 ComposeInterpolationNestingLimit = 1_000_104,
27 ComposeUnresolvedVariable = 1_000_105,
28 ComposeNativeError = 1_000_197,
29 ComposeNativeWarning = 1_000_198,
30 ComposeNativeNote = 1_000_199,
31 OrchestrationFailed = 3_001_000,
32 InputReadFailed = 3_001_001,
33 ComposeProjectRootInvalid = 3_001_002,
34 PodmanTargetSelectionInvalid = 3_001_003,
35 InterpolationInputInvalid = 3_001_004,
36 ComposeLoadFailed = 3_001_005,
37 ConversionFailed = 3_001_006,
38 QuadletParseFailed = 3_001_007,
39 OutputDirectoryNotEmpty = 3_002_001,
40 OutputPathInvalid = 3_002_002,
41 OutputWriteFailed = 3_002_003,
42 ReportWriteFailed = 3_003_001,
43 SupportBundleWriteFailed = 3_003_002,
44 QuadletTargetInvalid = 5_000_001,
45 QuadletOpenEndedTarget = 5_000_002,
46 QuadletOutputUnsupported = 5_000_003,
47 QuadletValueInvalid = 5_000_004,
48 QuadletGenerationFailed = 5_000_005,
49 QuadletCapabilityUnavailable = 5_000_006,
50 QuadletGroupingApproximation = 5_000_007,
51 QuadletDependencyUnsupported = 5_000_008,
52 QuadletRestartApproximation = 5_000_009,
53 QuadletEnvironmentFileApproximation = 5_000_010,
54 QuadletGroupingInvalid = 5_000_011,
55 QuadletDependencyInvalid = 5_000_012,
56 QuadletCapabilityDeprecated = 5_000_013,
57 QuadletUnresolvedVariable = 5_000_014,
58 QuadletSourceInvalid = 5_001_001,
59 QuadletModelInvalid = 5_001_002,
60 QuadletInputUnsupported = 5_001_003,
61 QuadletInputApproximation = 5_001_004,
62 QuadletNativeSyntax = 5_001_101,
63 QuadletNativeModel = 5_001_102,
64 QuadletNativeDocumentSet = 5_001_103,
65 QuadletNativeFailure = 5_001_104,
66}
67
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70pub struct DiagnosticRule {
71 id: RuleId,
72 code: &'static str,
73 name: &'static str,
74 default_severity: Severity,
75 description: &'static str,
76 help: &'static str,
77}
78
79impl DiagnosticRule {
80 #[must_use]
82 pub const fn id(self) -> RuleId {
83 self.id
84 }
85
86 #[must_use]
88 pub const fn code(self) -> &'static str {
89 self.code
90 }
91
92 #[must_use]
94 pub const fn name(self) -> &'static str {
95 self.name
96 }
97
98 #[must_use]
100 pub const fn default_severity(self) -> Severity {
101 self.default_severity
102 }
103
104 #[must_use]
106 pub fn owner(self) -> &'static str {
107 match self.code.as_bytes().get(..3) {
108 Some(b"BFC") => "Compose adapter",
109 Some(b"BFO") => "BoxFerry orchestration",
110 Some(b"BFQ") => "Quadlet adapter",
111 _ => "BoxFerry engine",
112 }
113 }
114
115 #[must_use]
117 pub const fn description(self) -> &'static str {
118 self.description
119 }
120
121 #[must_use]
123 pub const fn help(self) -> &'static str {
124 self.help
125 }
126
127 pub fn diagnostic_code(self) -> Result<DiagnosticCode, InvalidDiagnosticCode> {
133 DiagnosticCode::new(self.code)
134 }
135}
136
137macro_rules! rule {
138 ($id:ident, $code:literal, $name:literal, $severity:ident, $description:literal, $help:literal) => {
139 DiagnosticRule {
140 id: RuleId::$id,
141 code: $code,
142 name: $name,
143 default_severity: Severity::$severity,
144 description: $description,
145 help: $help,
146 }
147 };
148}
149
150pub const RULES: &[DiagnosticRule] = &[
152 rule!(
153 ComposeModelInvalid,
154 "BFC0001",
155 "compose-model-invalid",
156 Error,
157 "Compose intent could not be represented by the neutral application model.",
158 "Correct the reported Compose value or remove the conflicting declaration."
159 ),
160 rule!(
161 ComposeProfileRequired,
162 "BFC0002",
163 "compose-profile-selection-required",
164 Error,
165 "Profiled Compose services require an explicit profile selection.",
166 "Select profiles explicitly with --profile, or use --all-profiles only after reviewing mutually exclusive services."
167 ),
168 rule!(
169 ComposeProfileMismatch,
170 "BFC0003",
171 "compose-profile-selection-mismatch",
172 Error,
173 "The Compose profile selection is invalid or belongs to another merged project.",
174 "Recreate the profile selection from the same merged Compose project."
175 ),
176 rule!(
177 ComposeIntentUnsupported,
178 "BFC0004",
179 "compose-intent-unsupported",
180 Warning,
181 "Compose intent is not represented by the current neutral-model subset.",
182 "Review the named feature; use --loss-policy partial only when omitting that intent is acceptable."
183 ),
184 rule!(
185 ComposeValueInvalid,
186 "BFC0005",
187 "compose-value-invalid",
188 Error,
189 "A Compose value cannot be represented safely in the neutral model.",
190 "Correct the reported value before converting again."
191 ),
192 rule!(
193 ComposeTargetInvalid,
194 "BFC0006",
195 "compose-target-invalid",
196 Error,
197 "The requested Compose output target is invalid.",
198 "Select a supported Compose target profile and compatible version inputs."
199 ),
200 rule!(
201 ComposeOutputUnsupported,
202 "BFC0007",
203 "compose-output-unsupported",
204 Warning,
205 "Neutral application intent is not represented in generated Compose output.",
206 "Review the named subject; use --loss-policy partial only when the omitted intent is acceptable."
207 ),
208 rule!(
209 ComposeGenerationFailed,
210 "BFC0008",
211 "compose-generation-failed",
212 Error,
213 "ComposeLens rejected generated Compose output.",
214 "Correct the reported neutral value or target choice and retry."
215 ),
216 rule!(
217 ComposeCompatibilityConstraint,
218 "BFC0009",
219 "compose-compatibility-constraint",
220 Warning,
221 "Generated Compose syntax has a target-specific compatibility constraint.",
222 "Review the target-specific classification before authorizing non-exact output."
223 ),
224 rule!(
225 ComposeUnsetVariable,
226 "BFC0101",
227 "compose-unset-variable",
228 Warning,
229 "A Compose interpolation variable is not set.",
230 "Provide the missing value with --env-file FILE or --env NAME=VALUE."
231 ),
232 rule!(
233 ComposeRequiredVariable,
234 "BFC0102",
235 "compose-required-variable",
236 Error,
237 "A required Compose interpolation variable is not set.",
238 "Provide the required value with --env-file FILE or --env NAME=VALUE."
239 ),
240 rule!(
241 ComposeInterpolationInvalid,
242 "BFC0103",
243 "compose-interpolation-invalid",
244 Error,
245 "A Compose interpolation expression is invalid.",
246 "Correct the interpolation expression and retry."
247 ),
248 rule!(
249 ComposeInterpolationNestingLimit,
250 "BFC0104",
251 "compose-interpolation-nesting-limit",
252 Error,
253 "A Compose interpolation expression exceeds the supported nesting limit.",
254 "Simplify the interpolation expression before converting."
255 ),
256 rule!(
257 ComposeUnresolvedVariable,
258 "BFC0105",
259 "compose-unresolved-variable",
260 Warning,
261 "A Compose variable expression remains unresolved at the adapter boundary.",
262 "Use --interpolate and provide missing values with --env-file FILE or --env NAME=VALUE; partial authorization applies only when the affected intent can be omitted."
263 ),
264 rule!(
265 ComposeNativeError,
266 "BFC0197",
267 "compose-native-error",
268 Error,
269 "ComposeLens reported a native Compose error.",
270 "Use the retained source code and source span to correct the Compose document."
271 ),
272 rule!(
273 ComposeNativeWarning,
274 "BFC0198",
275 "compose-native-warning",
276 Warning,
277 "ComposeLens reported a native Compose warning.",
278 "Review the retained source code and source span before accepting the document."
279 ),
280 rule!(
281 ComposeNativeNote,
282 "BFC0199",
283 "compose-native-note",
284 Note,
285 "ComposeLens reported native Compose information.",
286 "Review the retained source code when more context is needed."
287 ),
288 rule!(
289 OrchestrationFailed,
290 "BFO1000",
291 "orchestration-failed",
292 Error,
293 "BoxFerry could not complete the requested orchestration stage.",
294 "Review the reason and correct the command inputs before retrying."
295 ),
296 rule!(
297 InputReadFailed,
298 "BFO1001",
299 "input-read-failed",
300 Error,
301 "An explicitly selected input could not be read.",
302 "Check that the input exists, is a readable regular file, and is not a symlink."
303 ),
304 rule!(
305 ComposeProjectRootInvalid,
306 "BFO1002",
307 "compose-project-root-invalid",
308 Error,
309 "The Compose project root could not be resolved.",
310 "Provide an existing absolute --project-directory or correct the input location."
311 ),
312 rule!(
313 PodmanTargetSelectionInvalid,
314 "BFO1003",
315 "podman-target-selection-invalid",
316 Error,
317 "The requested Podman target range could not be resolved.",
318 "Use major.minor or major.minor.patch values within the reviewed range."
319 ),
320 rule!(
321 InterpolationInputInvalid,
322 "BFO1004",
323 "interpolation-input-invalid",
324 Error,
325 "Compose interpolation inputs could not be resolved safely.",
326 "Correct the named --env-file or --env input and retry."
327 ),
328 rule!(
329 ComposeLoadFailed,
330 "BFO1005",
331 "compose-load-failed",
332 Error,
333 "Compose input could not be loaded or merged.",
334 "Correct the reported Compose source diagnostic before retrying."
335 ),
336 rule!(
337 ConversionFailed,
338 "BFO1006",
339 "conversion-failed",
340 Error,
341 "Source import or target conversion failed.",
342 "Correct the reported rule occurrences before retrying."
343 ),
344 rule!(
345 QuadletParseFailed,
346 "BFO1007",
347 "quadlet-parse-failed",
348 Error,
349 "Quadlet input could not be parsed into the native document boundary.",
350 "Correct the retained native diagnostics before retrying."
351 ),
352 rule!(
353 OutputDirectoryNotEmpty,
354 "BFO2001",
355 "output-directory-not-empty",
356 Error,
357 "The selected output directory is not empty.",
358 "Empty the selected --output-directory or choose a new path."
359 ),
360 rule!(
361 OutputPathInvalid,
362 "BFO2002",
363 "output-path-invalid",
364 Error,
365 "The selected output path is not a usable non-symlink directory.",
366 "Choose an absent path or an existing empty, non-symlink directory."
367 ),
368 rule!(
369 OutputWriteFailed,
370 "BFO2003",
371 "output-write-failed",
372 Error,
373 "Generated output could not be written safely.",
374 "Check parent-directory existence, permissions, free space, and path conflicts."
375 ),
376 rule!(
377 ReportWriteFailed,
378 "BFO3001",
379 "report-write-failed",
380 Error,
381 "The structured report file could not be written safely.",
382 "Choose a new writable --report-file path."
383 ),
384 rule!(
385 SupportBundleWriteFailed,
386 "BFO3002",
387 "support-bundle-write-failed",
388 Error,
389 "The diagnostic support bundle could not be written safely.",
390 "Choose a writable --error-report-directory with space for a new archive."
391 ),
392 rule!(
393 QuadletTargetInvalid,
394 "BFQ0001",
395 "quadlet-target-invalid",
396 Error,
397 "The requested Quadlet target or Podman range is invalid.",
398 "Select the podman target and a version range covered by QuadletLens."
399 ),
400 rule!(
401 QuadletOpenEndedTarget,
402 "BFQ0002",
403 "quadlet-open-ended-target",
404 Note,
405 "An omitted Podman maximum extends beyond verified compatibility evidence.",
406 "Set --podman-maximum-version for a finite compatibility claim."
407 ),
408 rule!(
409 QuadletOutputUnsupported,
410 "BFQ0003",
411 "quadlet-output-unsupported",
412 Warning,
413 "Neutral application intent is not represented by the current Quadlet subset.",
414 "Review the named subject; use --loss-policy partial only when omission is acceptable."
415 ),
416 rule!(
417 QuadletValueInvalid,
418 "BFQ0004",
419 "quadlet-value-invalid",
420 Error,
421 "A neutral value cannot be emitted safely as native Quadlet syntax.",
422 "Correct or explicitly map the reported value before converting again."
423 ),
424 rule!(
425 QuadletGenerationFailed,
426 "BFQ0005",
427 "quadlet-generation-failed",
428 Error,
429 "QuadletLens rejected generated native output.",
430 "Correct the reported value or dependency and retry."
431 ),
432 rule!(
433 QuadletCapabilityUnavailable,
434 "BFQ0006",
435 "quadlet-capability-unavailable",
436 Warning,
437 "A required Quadlet capability is unavailable for part of the target range.",
438 "Narrow the Podman target range or accept the documented partial result."
439 ),
440 rule!(
441 QuadletGroupingApproximation,
442 "BFQ0007",
443 "quadlet-grouping-approximation",
444 Warning,
445 "Generated Quadlet grouping changes source service isolation.",
446 "Use --quadlet-grouping pod only when the shared-pod semantics are acceptable."
447 ),
448 rule!(
449 QuadletDependencyUnsupported,
450 "BFQ0008",
451 "quadlet-dependency-unsupported",
452 Warning,
453 "A service dependency cannot be represented exactly by Quadlet and systemd.",
454 "Review the dependency condition and complete unsupported behavior manually."
455 ),
456 rule!(
457 QuadletRestartApproximation,
458 "BFQ0009",
459 "quadlet-restart-policy-approximation",
460 Warning,
461 "Container restart behavior is approximated by the systemd service manager.",
462 "Use --loss-policy approximate only after accepting the documented systemd behavior difference."
463 ),
464 rule!(
465 QuadletEnvironmentFileApproximation,
466 "BFQ0010",
467 "quadlet-environment-file-approximation",
468 Warning,
469 "Environment-file parsing is delegated to Podman.",
470 "Verify the environment file with the target Podman version before deployment."
471 ),
472 rule!(
473 QuadletGroupingInvalid,
474 "BFQ0011",
475 "quadlet-grouping-invalid",
476 Error,
477 "The requested Quadlet grouping cannot preserve the application topology.",
478 "Keep separate containers or correct the reported grouping conflict."
479 ),
480 rule!(
481 QuadletDependencyInvalid,
482 "BFQ0012",
483 "quadlet-dependency-invalid",
484 Error,
485 "The service or image-artifact dependency graph is invalid.",
486 "Correct missing or cyclic dependencies before converting."
487 ),
488 rule!(
489 QuadletCapabilityDeprecated,
490 "BFQ0013",
491 "quadlet-capability-deprecated",
492 Note,
493 "A required Quadlet capability is deprecated for part of the target range.",
494 "Review the target range and plan migration away from the deprecated capability."
495 ),
496 rule!(
497 QuadletUnresolvedVariable,
498 "BFQ0014",
499 "quadlet-unresolved-source-variable",
500 Error,
501 "A source variable expression cannot be emitted as a Quadlet value.",
502 "Resolve source variables before conversion; for Compose input, use --interpolate and provide missing values with --env-file FILE or --env NAME=VALUE."
503 ),
504 rule!(
505 QuadletSourceInvalid,
506 "BFQ1001",
507 "quadlet-source-invalid",
508 Error,
509 "The Quadlet document set is invalid.",
510 "Correct native Quadlet syntax and document-set errors before converting."
511 ),
512 rule!(
513 QuadletModelInvalid,
514 "BFQ1002",
515 "quadlet-model-invalid",
516 Error,
517 "Quadlet intent cannot be represented by the neutral application model.",
518 "Correct the reported native value or relationship."
519 ),
520 rule!(
521 QuadletInputUnsupported,
522 "BFQ1003",
523 "quadlet-input-unsupported",
524 Warning,
525 "Quadlet intent is outside the current neutral-model importer subset.",
526 "Review the named key; use --loss-policy partial only when omission is acceptable."
527 ),
528 rule!(
529 QuadletInputApproximation,
530 "BFQ1004",
531 "quadlet-input-approximation",
532 Warning,
533 "Quadlet intent is approximated in the neutral model.",
534 "Review the documented semantic difference before authorizing approximate output."
535 ),
536 rule!(
537 QuadletNativeSyntax,
538 "BFQ1101",
539 "quadlet-native-syntax",
540 Error,
541 "QuadletLens reported a native syntax diagnostic.",
542 "Use the retained source code and source span to correct the Quadlet unit."
543 ),
544 rule!(
545 QuadletNativeModel,
546 "BFQ1102",
547 "quadlet-native-model",
548 Error,
549 "QuadletLens reported a native model diagnostic.",
550 "Use the retained source code and source span to correct the Quadlet value."
551 ),
552 rule!(
553 QuadletNativeDocumentSet,
554 "BFQ1103",
555 "quadlet-native-document-set",
556 Error,
557 "QuadletLens reported a native document-set diagnostic.",
558 "Correct missing, ambiguous, or invalid cross-document relationships."
559 ),
560 rule!(
561 QuadletNativeFailure,
562 "BFQ1104",
563 "quadlet-native-failure",
564 Error,
565 "QuadletLens could not construct the native input boundary.",
566 "Correct the reported input and native stage before retrying."
567 ),
568];
569
570impl RuleId {
571 #[must_use]
573 pub const fn definition(self) -> &'static DiagnosticRule {
574 match self {
575 Self::ComposeModelInvalid => &RULES[0],
576 Self::ComposeProfileRequired => &RULES[1],
577 Self::ComposeProfileMismatch => &RULES[2],
578 Self::ComposeIntentUnsupported => &RULES[3],
579 Self::ComposeValueInvalid => &RULES[4],
580 Self::ComposeTargetInvalid => &RULES[5],
581 Self::ComposeOutputUnsupported => &RULES[6],
582 Self::ComposeGenerationFailed => &RULES[7],
583 Self::ComposeCompatibilityConstraint => &RULES[8],
584 Self::ComposeUnsetVariable => &RULES[9],
585 Self::ComposeRequiredVariable => &RULES[10],
586 Self::ComposeInterpolationInvalid => &RULES[11],
587 Self::ComposeInterpolationNestingLimit => &RULES[12],
588 Self::ComposeUnresolvedVariable => &RULES[13],
589 Self::ComposeNativeError => &RULES[14],
590 Self::ComposeNativeWarning => &RULES[15],
591 Self::ComposeNativeNote => &RULES[16],
592 Self::OrchestrationFailed => &RULES[17],
593 Self::InputReadFailed => &RULES[18],
594 Self::ComposeProjectRootInvalid => &RULES[19],
595 Self::PodmanTargetSelectionInvalid => &RULES[20],
596 Self::InterpolationInputInvalid => &RULES[21],
597 Self::ComposeLoadFailed => &RULES[22],
598 Self::ConversionFailed => &RULES[23],
599 Self::QuadletParseFailed => &RULES[24],
600 Self::OutputDirectoryNotEmpty => &RULES[25],
601 Self::OutputPathInvalid => &RULES[26],
602 Self::OutputWriteFailed => &RULES[27],
603 Self::ReportWriteFailed => &RULES[28],
604 Self::SupportBundleWriteFailed => &RULES[29],
605 Self::QuadletTargetInvalid => &RULES[30],
606 Self::QuadletOpenEndedTarget => &RULES[31],
607 Self::QuadletOutputUnsupported => &RULES[32],
608 Self::QuadletValueInvalid => &RULES[33],
609 Self::QuadletGenerationFailed => &RULES[34],
610 Self::QuadletCapabilityUnavailable => &RULES[35],
611 Self::QuadletGroupingApproximation => &RULES[36],
612 Self::QuadletDependencyUnsupported => &RULES[37],
613 Self::QuadletRestartApproximation => &RULES[38],
614 Self::QuadletEnvironmentFileApproximation => &RULES[39],
615 Self::QuadletGroupingInvalid => &RULES[40],
616 Self::QuadletDependencyInvalid => &RULES[41],
617 Self::QuadletCapabilityDeprecated => &RULES[42],
618 Self::QuadletUnresolvedVariable => &RULES[43],
619 Self::QuadletSourceInvalid => &RULES[44],
620 Self::QuadletModelInvalid => &RULES[45],
621 Self::QuadletInputUnsupported => &RULES[46],
622 Self::QuadletInputApproximation => &RULES[47],
623 Self::QuadletNativeSyntax => &RULES[48],
624 Self::QuadletNativeModel => &RULES[49],
625 Self::QuadletNativeDocumentSet => &RULES[50],
626 Self::QuadletNativeFailure => &RULES[51],
627 }
628 }
629}
630
631#[must_use]
633pub fn find_rule(value: &str) -> Option<&'static DiagnosticRule> {
634 RULES
635 .iter()
636 .find(|rule| rule.code.eq_ignore_ascii_case(value) || rule.name.eq_ignore_ascii_case(value))
637}
638
639#[cfg(test)]
640mod tests {
641 use std::collections::BTreeSet;
642
643 use super::{RULES, RuleId, find_rule};
644
645 #[test]
646 fn catalogue_codes_names_and_typed_indices_are_unique_and_valid() -> Result<(), String> {
647 let mut codes = BTreeSet::new();
648 let mut names = BTreeSet::new();
649 let mut ids = BTreeSet::new();
650 for rule in RULES {
651 rule.diagnostic_code().map_err(|error| error.to_string())?;
652 assert!(ids.insert(rule.id()), "duplicate typed id {:?}", rule.id());
653 assert_eq!(rule.id().definition(), rule);
654 assert!(codes.insert(rule.code()), "duplicate code {}", rule.code());
655 assert!(names.insert(rule.name()), "duplicate name {}", rule.name());
656 assert!(!rule.description().is_empty());
657 assert!(!rule.help().is_empty());
658 assert_ne!(
659 rule.owner(),
660 "BoxFerry engine",
661 "uncatalogued namespace for {}",
662 rule.code()
663 );
664 }
665 assert!(RULES.windows(2).all(|pair| pair[0].code() < pair[1].code()));
666 assert_eq!(RuleId::OutputWriteFailed.definition().code(), "BFO2003");
667 assert_eq!(find_rule("quadlet-restart-policy-approximation"), find_rule("BFQ0009"));
668 Ok(())
669 }
670}