1use crate::engine::DisplayModel;
2use crate::resolver::ResolvedDescriptor;
3use std::ops::Deref;
4
5#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
6#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
7pub enum DiagnosticSeverity {
8 Info,
9 Warning,
10}
11
12#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
13#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
14pub struct FormatDiagnostic {
15 pub code: String,
16 pub severity: DiagnosticSeverity,
17 pub message: String,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub(crate) enum RenderDiagnosticKind {
22 InterpolatedIntentSkipped,
23 DefinitionReferenceUnresolved,
24 ValueUnresolved,
25 TokenMetadataNotFound,
26 TokenTickerNotFound,
27 NftCollectionAddressMissing,
28 NftCollectionNameNotFound,
29 NestedCalldataDegraded,
30 NestedDescriptorNotFound,
31 NestedCalldataInvalidType,
32 InteroperableAddressNameFallback,
33 GenericRenderWarning,
34}
35
36impl RenderDiagnosticKind {
37 fn code(self) -> &'static str {
38 match self {
39 Self::InterpolatedIntentSkipped => "interpolated_intent_skipped",
40 Self::DefinitionReferenceUnresolved => "definition_reference_unresolved",
41 Self::ValueUnresolved => "value_unresolved",
42 Self::TokenMetadataNotFound => "token_metadata_not_found",
43 Self::TokenTickerNotFound => "token_ticker_not_found",
44 Self::NftCollectionAddressMissing => "nft_collection_address_missing",
45 Self::NftCollectionNameNotFound => "nft_collection_name_not_found",
46 Self::NestedCalldataDegraded => "nested_calldata_degraded",
47 Self::NestedDescriptorNotFound => "nested_descriptor_not_found",
48 Self::NestedCalldataInvalidType => "nested_calldata_invalid_type",
49 Self::InteroperableAddressNameFallback => "interoperable_address_name_fallback",
50 Self::GenericRenderWarning => "render_warning",
51 }
52 }
53
54 fn severity(self) -> DiagnosticSeverity {
55 DiagnosticSeverity::Warning
56 }
57}
58
59pub(crate) fn render_warning(
60 kind: RenderDiagnosticKind,
61 message: impl Into<String>,
62) -> FormatDiagnostic {
63 FormatDiagnostic {
64 code: kind.code().to_string(),
65 severity: kind.severity(),
66 message: message.into(),
67 }
68}
69
70#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
71#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
72pub enum FallbackReason {
73 DescriptorNotFound,
74 FormatNotFound,
75 NestedCallNotClearSigned,
76 InsufficientContext,
77}
78
79#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
80#[derive(Debug, Clone, serde::Serialize)]
81pub enum FormatOutcome {
82 ClearSigned {
83 model: DisplayModel,
84 diagnostics: Vec<FormatDiagnostic>,
85 },
86 Fallback {
87 model: DisplayModel,
88 reason: FallbackReason,
89 diagnostics: Vec<FormatDiagnostic>,
90 },
91}
92
93impl FormatOutcome {
94 pub fn model(&self) -> &DisplayModel {
95 match self {
96 Self::ClearSigned { model, .. } | Self::Fallback { model, .. } => model,
97 }
98 }
99
100 pub fn diagnostics(&self) -> &[FormatDiagnostic] {
101 match self {
102 Self::ClearSigned { diagnostics, .. } | Self::Fallback { diagnostics, .. } => {
103 diagnostics
104 }
105 }
106 }
107
108 pub fn fallback_reason(&self) -> Option<&FallbackReason> {
109 match self {
110 Self::ClearSigned { .. } => None,
111 Self::Fallback { reason, .. } => Some(reason),
112 }
113 }
114
115 pub fn is_clear_signed(&self) -> bool {
116 matches!(self, Self::ClearSigned { .. })
117 }
118
119 pub fn into_model(self) -> DisplayModel {
120 match self {
121 Self::ClearSigned { model, .. } | Self::Fallback { model, .. } => model,
122 }
123 }
124}
125
126impl Deref for FormatOutcome {
127 type Target = DisplayModel;
128
129 fn deref(&self) -> &Self::Target {
130 self.model()
131 }
132}
133
134#[derive(Debug, Clone)]
135pub enum ResolvedDescriptorResolution {
136 Found(Vec<ResolvedDescriptor>),
137 NotFound,
138}
139
140impl ResolvedDescriptorResolution {
141 pub fn as_slice(&self) -> &[ResolvedDescriptor] {
142 match self {
143 Self::Found(descriptors) => descriptors,
144 Self::NotFound => &[],
145 }
146 }
147}
148
149impl Deref for ResolvedDescriptorResolution {
150 type Target = [ResolvedDescriptor];
151
152 fn deref(&self) -> &Self::Target {
153 self.as_slice()
154 }
155}
156
157#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
158#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
159pub enum DescriptorResolutionOutcome {
160 Found(Vec<String>),
161 NotFound,
162}
163
164#[derive(Debug, Default, Clone)]
165pub(crate) struct RenderState {
166 diagnostics: Vec<FormatDiagnostic>,
167 fallback_reason: Option<FallbackReason>,
168}
169
170impl RenderState {
171 pub(crate) fn diagnostics(&self) -> &[FormatDiagnostic] {
172 &self.diagnostics
173 }
174
175 pub(crate) fn fallback_reason(&self) -> Option<FallbackReason> {
176 self.fallback_reason.clone()
177 }
178
179 pub(crate) fn warn(&mut self, code: &str, message: impl Into<String>) {
180 self.push(code, DiagnosticSeverity::Warning, message.into());
181 }
182
183 pub(crate) fn push_diagnostic(&mut self, diagnostic: FormatDiagnostic) {
184 self.diagnostics.push(diagnostic);
185 }
186
187 pub(crate) fn mark_nested_fallback(&mut self) {
188 if self.fallback_reason.is_none() {
189 self.fallback_reason = Some(FallbackReason::NestedCallNotClearSigned);
190 }
191 }
192
193 pub(crate) fn outcome(
194 self,
195 model: DisplayModel,
196 fallback_reason: Option<FallbackReason>,
197 ) -> FormatOutcome {
198 let diagnostics = self.diagnostics;
199 match fallback_reason.or(self.fallback_reason) {
200 Some(reason) => FormatOutcome::Fallback {
201 model,
202 reason,
203 diagnostics,
204 },
205 None => FormatOutcome::ClearSigned { model, diagnostics },
206 }
207 }
208
209 fn push(&mut self, code: &str, severity: DiagnosticSeverity, message: String) {
210 self.diagnostics.push(FormatDiagnostic {
211 code: code.to_string(),
212 severity,
213 message,
214 });
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::{render_warning, RenderDiagnosticKind};
221
222 #[test]
223 fn render_warning_code_does_not_depend_on_message_text() {
224 let first = render_warning(
225 RenderDiagnosticKind::TokenMetadataNotFound,
226 "token metadata not found for field 'Amount'",
227 );
228 let second = render_warning(
229 RenderDiagnosticKind::TokenMetadataNotFound,
230 "missing token metadata for field 'Amount'",
231 );
232
233 assert_eq!(first.code, "token_metadata_not_found");
234 assert_eq!(second.code, "token_metadata_not_found");
235 }
236
237 #[test]
238 fn render_warning_kinds_map_to_stable_codes() {
239 let cases = [
240 (
241 RenderDiagnosticKind::InterpolatedIntentSkipped,
242 "interpolated_intent_skipped",
243 ),
244 (
245 RenderDiagnosticKind::DefinitionReferenceUnresolved,
246 "definition_reference_unresolved",
247 ),
248 (RenderDiagnosticKind::ValueUnresolved, "value_unresolved"),
249 (
250 RenderDiagnosticKind::TokenMetadataNotFound,
251 "token_metadata_not_found",
252 ),
253 (
254 RenderDiagnosticKind::TokenTickerNotFound,
255 "token_ticker_not_found",
256 ),
257 (
258 RenderDiagnosticKind::NftCollectionAddressMissing,
259 "nft_collection_address_missing",
260 ),
261 (
262 RenderDiagnosticKind::NftCollectionNameNotFound,
263 "nft_collection_name_not_found",
264 ),
265 (
266 RenderDiagnosticKind::NestedCalldataDegraded,
267 "nested_calldata_degraded",
268 ),
269 (
270 RenderDiagnosticKind::NestedDescriptorNotFound,
271 "nested_descriptor_not_found",
272 ),
273 (
274 RenderDiagnosticKind::NestedCalldataInvalidType,
275 "nested_calldata_invalid_type",
276 ),
277 (
278 RenderDiagnosticKind::InteroperableAddressNameFallback,
279 "interoperable_address_name_fallback",
280 ),
281 ];
282
283 for (kind, expected_code) in cases {
284 let diagnostic = render_warning(kind, "example");
285 assert_eq!(diagnostic.code, expected_code);
286 }
287 }
288}