Skip to main content

citum_resolver_api/
error.rs

1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus
4*/
5
6use std::borrow::Cow;
7use thiserror::Error;
8
9/// Error type for style resolution operations at the store layer.
10#[derive(Error, Debug)]
11#[non_exhaustive]
12pub enum ResolverError {
13    /// An underlying I/O error occurred.
14    #[error("io error: {0}")]
15    Io(#[from] std::io::Error),
16    /// The style content is malformed or invalid for the target version.
17    #[error("invalid style: {0}")]
18    InvalidStyle(Cow<'static, str>),
19    /// The requested style could not be located.
20    #[error("style not found: {0}")]
21    StyleNotFound(Cow<'static, str>),
22    /// The requested locale could not be located.
23    #[error("locale not found: {0}")]
24    LocaleNotFound(Cow<'static, str>),
25    /// Failure during YAML deserialization.
26    #[error("yaml error: {0}")]
27    YamlError(String),
28    /// Failure during JSON deserialization.
29    #[cfg(feature = "serde_json")]
30    #[error("json error: {0}")]
31    JsonError(#[from] serde_json::Error),
32    /// Failure during CBOR deserialization.
33    #[error("cbor error: {0}")]
34    CborError(String),
35    /// A network failure occurred during HTTP fetch.
36    #[cfg(feature = "http")]
37    #[error("http error: {0}")]
38    HttpError(String),
39    /// A failure occurred during a Git operation.
40    #[cfg(feature = "http")]
41    #[error("git error: {0}")]
42    GitError(String),
43    /// The URI host or origin is not in the resolver's allowlist.
44    #[error("host not in resolver allowlist: {uri} ({reason})")]
45    Denied {
46        /// The URI that was denied.
47        uri: String,
48        /// Reason for the denial.
49        reason: String,
50    },
51    /// The style's `citum-version` is not compatible with the running engine.
52    #[error(
53        "engine version mismatch for {uri}: engine requires {required}, style declares {declared}"
54    )]
55    VersionMismatch {
56        /// URI of the style with the incompatible version.
57        uri: String,
58        /// Version requirement declared by the style.
59        required: String,
60        /// Version of the running engine.
61        declared: String,
62    },
63    /// The content does not match the expected integrity hash.
64    #[error("integrity failure for {uri}: expected {expected}, got {actual}")]
65    IntegrityFailure {
66        /// URI of the content that failed verification.
67        uri: String,
68        /// The expected CIDv1 string.
69        expected: String,
70        /// The CID computed from the actual content bytes.
71        actual: String,
72    },
73    /// Generic network or transport failure.
74    #[error("network error fetching {uri}: {reason}")]
75    NetworkError {
76        /// URI of the failed fetch.
77        uri: String,
78        /// Reason for the network failure.
79        reason: String,
80    },
81}
82
83/// Error type for style resolution and inheritance processing at the schema layer.
84#[derive(Error, Debug, Clone, PartialEq)]
85#[non_exhaustive]
86pub enum ResolutionError {
87    /// A `profile` style attempted to override template-bearing structure.
88    #[error("profile styles may not override template-bearing field `{location}`")]
89    InvalidProfileOverride {
90        /// Human-readable location hint.
91        location: String,
92    },
93    /// An inheritance loop was detected.
94    #[error("inheritance loop detected at base `{base}`")]
95    InheritanceLoop {
96        /// Base key that closed the cycle.
97        base: String,
98    },
99    /// A `file://` URI could not be resolved.
100    #[error("failed to resolve URI `{uri}`: {reason}")]
101    UriResolutionFailed {
102        /// The URI that failed to resolve.
103        uri: String,
104        /// Reason for failure.
105        reason: String,
106    },
107    /// A Template V3 variant references a missing parent variant.
108    #[error("template variant `{location}` extends missing variant `{selector}`")]
109    MissingTemplateVariantParent {
110        /// Human-readable location hint.
111        location: String,
112        /// Parent selector that could not be found.
113        selector: String,
114    },
115    /// A Template V3 variant parent chain contains a cycle.
116    #[error("template variant inheritance loop at `{location}` through `{selector}`")]
117    TemplateVariantCycle {
118        /// Human-readable location hint.
119        location: String,
120        /// Selector that closed the cycle.
121        selector: String,
122    },
123    /// A Template V3 operation matched no components.
124    #[error("template variant operation in `{location}` matched no component")]
125    TemplateVariantAnchorNotFound {
126        /// Human-readable location hint.
127        location: String,
128    },
129    /// A Template V3 operation matched more than one component.
130    #[error("template variant operation in `{location}` matched multiple components")]
131    TemplateVariantAmbiguousAnchor {
132        /// Human-readable location hint.
133        location: String,
134    },
135    /// A Template V3 add operation does not define exactly one anchor.
136    #[error(
137        "template variant add operation in `{location}` must specify exactly one of before/after"
138    )]
139    InvalidTemplateVariantAdd {
140        /// Human-readable location hint.
141        location: String,
142    },
143    /// The fetched parent style's content did not hash to the value declared
144    /// in `extends-pin`.
145    #[error("extends-pin integrity check failed for `{uri}`: expected {expected}, got {actual}")]
146    IntegrityFailure {
147        /// URI of the parent that failed integrity verification.
148        uri: String,
149        /// CID declared in the child's `extends-pin`.
150        expected: String,
151        /// CID computed from the bytes the resolver actually returned.
152        actual: String,
153    },
154    /// The fetched style declares a `citum-version` requirement that the
155    /// running engine does not satisfy.
156    #[error("style `{uri}` requires citum-version `{required}`; running engine is `{declared}`")]
157    VersionMismatch {
158        /// URI whose `citum-version` requirement was unsatisfiable.
159        uri: String,
160        /// `citum-version` requirement declared by the style's `info` block.
161        required: String,
162        /// Version of the running engine.
163        declared: String,
164    },
165}
166
167impl ResolutionError {
168    /// Convert a [`ResolverError`] into a [`ResolutionError`] for a specific URI.
169    #[must_use]
170    pub fn from_resolver_error(uri: &str, err: ResolverError) -> Self {
171        match err {
172            ResolverError::IntegrityFailure {
173                expected, actual, ..
174            } => ResolutionError::IntegrityFailure {
175                uri: uri.into(),
176                expected,
177                actual,
178            },
179            ResolverError::VersionMismatch {
180                required, declared, ..
181            } => ResolutionError::VersionMismatch {
182                uri: uri.into(),
183                required,
184                declared,
185            },
186            _ => ResolutionError::UriResolutionFailed {
187                uri: uri.into(),
188                reason: err.to_string(),
189            },
190        }
191    }
192}