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 and Citum contributors
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    /// A fallback-template diff violates its inheritance contract.
144    #[error("invalid fallback-template diff in `{location}`: {reason}")]
145    InvalidFallbackTemplateDiff {
146        /// Style-aware location of the invalid fallback template.
147        location: String,
148        /// Reason the fallback diff cannot be resolved.
149        reason: String,
150    },
151    /// The fetched parent style's content did not hash to the value declared
152    /// in `extends-pin`.
153    #[error("extends-pin integrity check failed for `{uri}`: expected {expected}, got {actual}")]
154    IntegrityFailure {
155        /// URI of the parent that failed integrity verification.
156        uri: String,
157        /// CID declared in the child's `extends-pin`.
158        expected: String,
159        /// CID computed from the bytes the resolver actually returned.
160        actual: String,
161    },
162    /// The fetched style declares a `citum-version` requirement that the
163    /// running engine does not satisfy.
164    #[error("style `{uri}` requires citum-version `{required}`; running engine is `{declared}`")]
165    VersionMismatch {
166        /// URI whose `citum-version` requirement was unsatisfiable.
167        uri: String,
168        /// `citum-version` requirement declared by the style's `info` block.
169        required: String,
170        /// Version of the running engine.
171        declared: String,
172    },
173}
174
175impl ResolutionError {
176    /// Convert a [`ResolverError`] into a [`ResolutionError`] for a specific URI.
177    #[must_use]
178    pub fn from_resolver_error(uri: &str, err: ResolverError) -> Self {
179        match err {
180            ResolverError::IntegrityFailure {
181                expected, actual, ..
182            } => ResolutionError::IntegrityFailure {
183                uri: uri.into(),
184                expected,
185                actual,
186            },
187            ResolverError::VersionMismatch {
188                required, declared, ..
189            } => ResolutionError::VersionMismatch {
190                uri: uri.into(),
191                required,
192                declared,
193            },
194            _ => ResolutionError::UriResolutionFailed {
195                uri: uri.into(),
196                reason: err.to_string(),
197            },
198        }
199    }
200}