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}