Skip to main content

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>;