Skip to main content

entrouter_universal/
universal_struct.rs

1// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2//  Entrouter Universal - Per-Field Struct Wrapping
3//
4//  Wraps every field of a struct individually.
5//  If Redis mangles just one field, you know exactly which one.
6//
7//  Usage:
8//    let wrapped = UniversalStruct::wrap_fields(vec![
9//        ("token",   "abc123..."),
10//        ("user_id", "john"),
11//        ("amount",  "99.99"),
12//    ]);
13//
14//    let result = wrapped.verify_all();
15//    // tells you exactly which field got touched
16// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
17
18use crate::{decode_str, encode_str, fingerprint_str, UniversalError};
19use serde::{Deserialize, Serialize};
20use std::collections::HashMap;
21
22/// A single field wrapped with its Base64 encoding and SHA-256 fingerprint.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct WrappedField {
25    /// Field name
26    pub name: String,
27    /// Base64 encoded value
28    pub d: String,
29    /// SHA-256 fingerprint of original value
30    pub f: String,
31}
32
33impl WrappedField {
34    /// Wrap a named value, producing its Base64 encoding and fingerprint.
35    pub fn wrap(name: &str, value: &str) -> Self {
36        Self {
37            name: name.to_string(),
38            d: encode_str(value),
39            f: fingerprint_str(value),
40        }
41    }
42
43    /// Decode and verify this field, returning the original value on success.
44    pub fn verify(&self) -> Result<String, UniversalError> {
45        let decoded = decode_str(&self.d)?;
46        let actual_fp = fingerprint_str(&decoded);
47        if actual_fp != self.f {
48            return Err(UniversalError::IntegrityViolation {
49                expected: self.f.clone(),
50                actual: actual_fp,
51            });
52        }
53        Ok(decoded)
54    }
55
56    /// Returns `true` if verification passes.
57    pub fn is_intact(&self) -> bool {
58        self.verify().is_ok()
59    }
60}
61
62/// A collection of individually-wrapped fields.
63///
64/// Each field carries its own Base64 encoding and SHA-256 fingerprint,
65/// so a single corrupted field can be identified without re-verifying
66/// the rest.
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct UniversalStruct {
69    pub fields: Vec<WrappedField>,
70}
71
72/// Per-field verification result.
73#[derive(Debug, Clone, PartialEq)]
74pub struct FieldVerifyResult {
75    /// The field name.
76    pub name: String,
77    /// `true` if the field passed integrity verification.
78    pub intact: bool,
79    /// The decoded value, if verification succeeded.
80    pub value: Option<String>,
81    /// Error message, if verification failed.
82    pub error: Option<String>,
83}
84
85impl std::fmt::Display for FieldVerifyResult {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        if self.intact {
88            write!(f, "{}: Intact", self.name)
89        } else {
90            write!(
91                f,
92                "{}: Violated ({})",
93                self.name,
94                self.error.as_deref().unwrap_or("unknown")
95            )
96        }
97    }
98}
99
100/// Aggregated result of verifying every field in a [`UniversalStruct`].
101#[derive(Debug, Clone, PartialEq)]
102pub struct StructVerifyResult {
103    /// `true` if every field passed verification.
104    pub all_intact: bool,
105    /// Individual per-field results.
106    pub fields: Vec<FieldVerifyResult>,
107    /// Names of fields that failed verification.
108    pub violations: Vec<String>,
109}
110
111impl std::fmt::Display for StructVerifyResult {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        if self.all_intact {
114            write!(f, "All {} fields intact", self.fields.len())
115        } else {
116            write!(f, "Violations in: {}", self.violations.join(", "))
117        }
118    }
119}
120
121impl UniversalStruct {
122    /// Wrap a list of (name, value) field pairs
123    #[must_use]
124    pub fn wrap_fields(fields: &[(&str, &str)]) -> Self {
125        Self {
126            fields: fields
127                .iter()
128                .map(|(name, value)| WrappedField::wrap(name, value))
129                .collect(),
130        }
131    }
132
133    /// Verify all fields - returns detailed per-field results
134    pub fn verify_all(&self) -> StructVerifyResult {
135        let mut all_intact = true;
136        let mut violations = Vec::new();
137        let fields = self
138            .fields
139            .iter()
140            .map(|f| match f.verify() {
141                Ok(value) => FieldVerifyResult {
142                    name: f.name.clone(),
143                    intact: true,
144                    value: Some(value),
145                    error: None,
146                },
147                Err(e) => {
148                    all_intact = false;
149                    violations.push(f.name.clone());
150                    FieldVerifyResult {
151                        name: f.name.clone(),
152                        intact: false,
153                        value: None,
154                        error: Some(e.to_string()),
155                    }
156                }
157            })
158            .collect();
159
160        StructVerifyResult {
161            all_intact,
162            fields,
163            violations,
164        }
165    }
166
167    /// Get a verified field value by name
168    pub fn get(&self, name: &str) -> Result<String, UniversalError> {
169        self.fields
170            .iter()
171            .find(|f| f.name == name)
172            .ok_or_else(|| {
173                UniversalError::MalformedEnvelope(format!("field '{}' not found", name))
174            })?
175            .verify()
176    }
177
178    /// Get all verified fields as a HashMap
179    pub fn to_map(&self) -> Result<HashMap<String, String>, UniversalError> {
180        let result = self.verify_all();
181        if !result.all_intact {
182            return Err(UniversalError::IntegrityViolation {
183                expected: "all fields intact".to_string(),
184                actual: format!("violations in: {}", result.violations.join(", ")),
185            });
186        }
187        Ok(result
188            .fields
189            .into_iter()
190            .filter_map(|f| f.value.map(|v| (f.name, v)))
191            .collect())
192    }
193
194    /// Assert all fields intact - panics with field names if violated
195    pub fn assert_intact(&self) {
196        let result = self.verify_all();
197        if !result.all_intact {
198            panic!(
199                "Entrouter Universal: field integrity violations in: {}",
200                result.violations.join(", ")
201            );
202        }
203    }
204
205    /// Print a full field report
206    pub fn report(&self) -> String {
207        let result = self.verify_all();
208        let mut out = String::new();
209        out.push_str("━━━━ Entrouter Universal Field Report ━━━━\n");
210        out.push_str(&format!("All intact: {}\n\n", result.all_intact));
211        for field in &result.fields {
212            let status = if field.intact { "✅" } else { "❌ VIOLATED" };
213            out.push_str(&format!(
214                "  {}: {} - {}\n",
215                field.name,
216                status,
217                field.value.as_deref().unwrap_or("-")
218            ));
219            if let Some(err) = &field.error {
220                out.push_str(&format!("    Error: {}\n", err));
221            }
222        }
223        out.push_str("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
224        out
225    }
226
227    /// Serialize this struct to a JSON string.
228    pub fn to_json(&self) -> Result<String, UniversalError> {
229        serde_json::to_string(self).map_err(|e| UniversalError::SerializationError(e.to_string()))
230    }
231
232    /// Deserialize a struct from a JSON string.
233    pub fn from_json(s: &str) -> Result<Self, UniversalError> {
234        serde_json::from_str(s).map_err(|e| UniversalError::SerializationError(e.to_string()))
235    }
236}