polyspec_crudui_validator/compose/errors.rs
1//! Composition load errors (SPEC §5, §7; unresolved_behavior).
2//!
3//! Composition is a pre-processing pass that runs BEFORE validation/render: the
4//! parser expands `$ref`/`$patch` into a single spec first (G5). An unresolved
5//! composition is NOT a validation failure (`valid:false`) — it is a LOAD
6//! FAILURE: the spec itself does not come into existence. Never let an
7//! unresolved `$ref` pass as `valid:true`.
8//!
9//! Load-error classes:
10//! (1) $ref file missing
11//! (2) $ref format error
12//! (3) detectKey absent
13//! (4) $ref cycle (A→B→A)
14//! (5) $patch target/op error
15
16use std::fmt;
17
18/// Machine-readable load-error codes. One per unresolved-behavior class — these
19/// strings are the cross-language contract (`expectError.code` in cases.json).
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ComposeErrorCode {
22 /// $ref points at a file that does not exist.
23 RefFileNotFound,
24 /// $ref string is malformed: `(…` with no closing `).keys`, or empty path.
25 RefFormatError,
26 /// A detectKey path segment (or the trailing `properties`) is absent.
27 RefDetectKeyNotFound,
28 /// $ref cycle detected (A→B→A).
29 RefCycle,
30 /// $ref value is neither a string nor an array of strings.
31 RefValueType,
32 /// $patch is not an object of deep-path → value entries.
33 PatchShape,
34 /// $patch op targets a path whose intermediate node is a non-object scalar.
35 PatchPathConflict,
36 /// $patch remove targets a path that does not exist (strict remove).
37 PatchRemoveTargetMissing,
38 /// A forbidden meta key survived into the composed single spec at some depth
39 /// (SPEC §6). The recursive forbidden-scan runs after composition and x-strip and
40 /// before validation; a forbidden key anywhere is a LOAD failure, never
41 /// `valid:true`. `trace` carries the dotted path to the offending key.
42 ForbiddenMetaKey,
43 /// A rule parameter outside the validation-rule definitions; `trace` is the
44 /// field's declaration path.
45 InvalidRuleParameter,
46 /// A `pattern` or `match` parameter outside the CRUDUI pattern language;
47 /// `trace` is the field's declaration path.
48 InvalidRulePattern,
49 /// A `validate` or `messages` key that is not a registered rule name;
50 /// `trace` is the field's declaration path.
51 UnknownRule,
52 /// A string or member name of the specification or a composition file that
53 /// is not a sequence of Unicode scalar values (docs/spec/input-text.md);
54 /// `trace` is its path.
55 InvalidText,
56}
57
58impl ComposeErrorCode {
59 /// Stable wire string (matches the JS `ComposeErrorCode` union member).
60 pub fn as_str(self) -> &'static str {
61 match self {
62 ComposeErrorCode::RefFileNotFound => "REF_FILE_NOT_FOUND",
63 ComposeErrorCode::RefFormatError => "REF_FORMAT_ERROR",
64 ComposeErrorCode::RefDetectKeyNotFound => "REF_DETECT_KEY_NOT_FOUND",
65 ComposeErrorCode::RefCycle => "REF_CYCLE",
66 ComposeErrorCode::RefValueType => "REF_VALUE_TYPE",
67 ComposeErrorCode::PatchShape => "PATCH_SHAPE",
68 ComposeErrorCode::PatchPathConflict => "PATCH_PATH_CONFLICT",
69 ComposeErrorCode::PatchRemoveTargetMissing => "PATCH_REMOVE_TARGET_MISSING",
70 ComposeErrorCode::ForbiddenMetaKey => "FORBIDDEN_META_KEY",
71 ComposeErrorCode::InvalidRuleParameter => "INVALID_RULE_PARAMETER",
72 ComposeErrorCode::InvalidRulePattern => "INVALID_RULE_PATTERN",
73 ComposeErrorCode::UnknownRule => "UNKNOWN_RULE",
74 ComposeErrorCode::InvalidText => "INVALID_TEXT",
75 }
76 }
77}
78
79impl fmt::Display for ComposeErrorCode {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 f.write_str(self.as_str())
82 }
83}
84
85/// A composition load failure — distinct from a later validation error. Mirrors
86/// the JS `ComposeLoadError` (code + message + trace path stack for cycles).
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct ComposeLoadError {
89 /// Stable machine-readable code (one per unresolved-behavior class).
90 pub code: ComposeErrorCode,
91 /// Human-readable message (not part of the cross-language contract).
92 pub message: String,
93 /// The composition path stack when the error occurred (for cycle/trace).
94 pub trace: Vec<String>,
95}
96
97impl ComposeLoadError {
98 /// Build a load error with no trace stack.
99 pub fn new(code: ComposeErrorCode, message: impl Into<String>) -> Self {
100 ComposeLoadError {
101 code,
102 message: message.into(),
103 trace: Vec::new(),
104 }
105 }
106
107 /// Build a load error carrying the resolution-chain trace (cycle/not-found).
108 pub fn with_trace(
109 code: ComposeErrorCode,
110 message: impl Into<String>,
111 trace: Vec<String>,
112 ) -> Self {
113 ComposeLoadError {
114 code,
115 message: message.into(),
116 trace,
117 }
118 }
119}
120
121impl fmt::Display for ComposeLoadError {
122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123 write!(f, "{}: {}", self.code, self.message)
124 }
125}
126
127impl std::error::Error for ComposeLoadError {}
128
129/// Convenience alias for composition results.
130pub type ComposeResult<T> = Result<T, ComposeLoadError>;