Skip to main content

entrouter_universal/
universal_struct.rs

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