Skip to main content

clear_signing/
outcome.rs

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}