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}