#![allow(non_upper_case_globals)]
#[cfg(not(feature = "metadata"))]
compile_error!(
"\n\n\
❌ This example requires 'metadata' features!\n\
\n\
Run with:\n\
cargo run --example browser_server_catalog --features metadata\n\
"
);
#[cfg(feature = "metadata")]
use std::collections::HashMap;
#[cfg(feature = "metadata")]
use waddling_errors_macros::{diag, setup};
#[cfg(feature = "metadata")]
setup! {
components = crate::components,
primaries = crate::primaries,
sequences = crate::sequences,
}
#[cfg(feature = "metadata")]
pub mod components {
use waddling_errors_macros::component;
component! {
Api {
docs: "API server errors",
}
}
}
#[cfg(feature = "metadata")]
pub mod primaries {
use waddling_errors_macros::primary;
primary! {
Auth {
docs: "Authentication errors",
}
}
}
#[cfg(feature = "metadata")]
pub mod sequences {
use waddling_errors_macros::sequence;
sequence! {
TOKEN_EXPIRED(1) {
description: "JWT token has expired",
typical_severity: "Error",
hints: ["Use refresh token", "Re-login"],
},
INVALID_CREDENTIALS(2) {
description: "Username or password incorrect",
typical_severity: "Error",
hints: ["Check username and password", "Reset password if needed"],
},
RATE_LIMIT(3) {
description: "Too many requests",
typical_severity: "Warning",
hints: ["Wait before retrying", "Reduce request frequency"],
},
}
}
#[cfg(feature = "metadata")]
diag! {
strict(component, primary, sequence, naming, duplicates, sequence_values, string_values),
<catalog>
E.Api.Auth.TOKEN_EXPIRED: {
message: "Your session has expired at {{expiry}}. Please login again.",
fields: [expiry],
hints: ["Click 'Login' button", "Use refresh token if available"],
tags: ["auth", "session", "security"],
},
E.Api.Auth.INVALID_CREDENTIALS: {
message: "Invalid username or password for user '{{pii/username}}'.",
pii: [username],
hints: ["Check your credentials", "Reset password if forgotten"],
tags: ["auth", "login"],
},
W.Api.Auth.RATE_LIMIT: {
message: "Rate limit exceeded. Try again in {{retry_after}} seconds.",
fields: [retry_after],
hints: ["Wait before retrying", "Contact support if persistent"],
tags: ["rate-limit", "throttling"],
}
}
#[cfg(feature = "metadata")]
#[derive(Debug, Clone)]
struct CompactError {
h: String,
f: HashMap<String, String>,
}
#[cfg(feature = "metadata")]
impl CompactError {
fn new(hash: &str) -> Self {
Self {
h: hash.to_string(),
f: HashMap::new(),
}
}
fn with_field(mut self, key: &str, value: &str) -> Self {
self.f.insert(key.to_string(), value.to_string());
self
}
fn to_json(&self) -> String {
let mut json = format!("{{\"h\":\"{}\"", self.h);
if !self.f.is_empty() {
json.push_str(",\"f\":{");
let mut first = true;
for (k, v) in &self.f {
if !first {
json.push(',');
}
json.push_str(&format!("\"{}\":\"{}\"", k, v));
first = false;
}
json.push('}');
}
json.push('}');
json
}
fn byte_size(&self) -> usize {
self.to_json().len()
}
}
#[cfg(feature = "metadata")]
#[derive(Debug, Clone)]
struct CatalogEntry {
code: String,
severity: String,
message: String,
hints: Vec<String>,
}
#[cfg(feature = "metadata")]
struct BrowserCatalog {
#[allow(dead_code)]
language: String,
entries: HashMap<String, CatalogEntry>,
}
#[cfg(feature = "metadata")]
impl BrowserCatalog {
fn new(language: &str) -> Self {
Self {
language: language.to_string(),
entries: HashMap::new(),
}
}
fn add_entry(&mut self, hash: &str, entry: CatalogEntry) {
self.entries.insert(hash.to_string(), entry);
}
fn expand(&self, compact: &CompactError) -> Option<ExpandedError> {
let entry = self.entries.get(&compact.h)?;
let mut message = entry.message.clone();
for (key, value) in &compact.f {
let placeholder = format!("{{{{{}}}}}", key);
message = message.replace(&placeholder, value);
let pii_placeholder = format!("{{{{pii/{}}}}}", key);
message = message.replace(&pii_placeholder, value);
}
Some(ExpandedError {
hash: compact.h.clone(),
code: entry.code.clone(),
severity: entry.severity.clone(),
message,
hints: entry.hints.clone(),
})
}
}
#[derive(Debug)]
struct ExpandedError {
#[allow(dead_code)]
hash: String,
code: String,
severity: String,
message: String,
hints: Vec<String>,
}
impl ExpandedError {
fn display(&self) -> String {
let mut output = "╔═══════════════════════════════════════╗\n".to_string();
output.push_str(&format!(
"║ [{}] {}\n",
self.severity.to_uppercase(),
self.code
));
output.push_str("╠═══════════════════════════════════════╣\n");
output.push_str(&format!("║ {}\n", self.message));
if !self.hints.is_empty() {
output.push_str("╠═══════════════════════════════════════╣\n");
output.push_str("║ 💡 Suggestions:\n");
for hint in &self.hints {
output.push_str(&format!("║ • {}\n", hint));
}
}
output.push_str("╚═══════════════════════════════════════╝\n");
output
}
}
#[cfg(feature = "metadata")]
fn main() {
println!("🌐 Browser-Server Catalog Example");
println!("═══════════════════════════════════════════════════════\n");
println!("📥 STEP 1: Browser Loads Catalogs");
println!("───────────────────────────────────────────────────────\n");
let mut catalog_en = BrowserCatalog::new("en");
catalog_en.add_entry(
E_API_AUTH_TOKEN_EXPIRED_HASH,
CatalogEntry {
code: "E.API.AUTH.TOKEN_EXPIRED".to_string(),
severity: "Error".to_string(),
message: "Your session has expired at {{expiry}}. Please login again.".to_string(),
hints: vec![
"Click 'Login' button".to_string(),
"Use refresh token if available".to_string(),
],
},
);
let mut catalog_es = BrowserCatalog::new("es");
catalog_es.add_entry(
E_API_AUTH_TOKEN_EXPIRED_HASH,
CatalogEntry {
code: "E.API.AUTH.TOKEN_EXPIRED".to_string(),
severity: "Error".to_string(),
message: "Tu sesión ha expirado a las {{expiry}}. Por favor, inicia sesión nuevamente."
.to_string(),
hints: vec![
"Haz clic en el botón 'Iniciar sesión'".to_string(),
"Usa el token de actualización si está disponible".to_string(),
],
},
);
let mut catalog_fr = BrowserCatalog::new("fr");
catalog_fr.add_entry(
E_API_AUTH_TOKEN_EXPIRED_HASH,
CatalogEntry {
code: "E.API.AUTH.TOKEN_EXPIRED".to_string(),
severity: "Error".to_string(),
message: "Votre session a expiré à {{expiry}}. Veuillez vous reconnecter.".to_string(),
hints: vec![
"Cliquez sur le bouton 'Connexion'".to_string(),
"Utilisez le jeton de rafraîchissement si disponible".to_string(),
],
},
);
println!("✅ Loaded 3 catalogs: English, Spanish, French");
println!(" (In real app: downloaded from /api/catalog.json?lang=XX)\n");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("📤 STEP 2: Server Sends Compact Error");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
println!("🖥️ SERVER: Token validation failed for user session");
println!(" Creating compact error...\n");
let compact = CompactError::new(E_API_AUTH_TOKEN_EXPIRED_HASH)
.with_field("expiry", "2024-11-19 15:30:00 UTC");
println!("📦 SERVER → BROWSER (over network):");
println!(" {}", compact.to_json());
println!(
" Size: {} bytes (vs ~300 bytes for full error!)\n",
compact.byte_size()
);
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("📥 STEP 3: Browser Receives & Expands");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
println!("🌍 Browser #1: English User");
println!(" Receives: {}", compact.to_json());
println!(" Expands with English catalog:\n");
if let Some(expanded) = catalog_en.expand(&compact) {
println!("{}", expanded.display());
}
println!("🌍 Browser #2: Spanish User");
println!(" Receives: {}", compact.to_json());
println!(" Expands with Spanish catalog:\n");
if let Some(expanded) = catalog_es.expand(&compact) {
println!("{}", expanded.display());
}
println!("🌍 Browser #3: French User");
println!(" Receives: {}", compact.to_json());
println!(" Expands with French catalog:\n");
if let Some(expanded) = catalog_fr.expand(&compact) {
println!("{}", expanded.display());
}
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("💰 Value Proposition");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
let compact_size = compact.byte_size();
let full_size = 350;
println!("📊 Bandwidth Savings:");
println!(" Traditional (full error): ~{} bytes", full_size);
println!(" With catalog (compact): {} bytes", compact_size);
println!(
" Savings: {:.1}% ({} bytes per error)\n",
((full_size - compact_size) as f64 / full_size as f64) * 100.0,
full_size - compact_size
);
println!("🌐 i18n Benefits:");
println!(" • Server sends language-agnostic hash");
println!(" • Browser picks language catalog");
println!(" • No server-side translation needed!");
println!(" • User sees error in their language instantly\n");
println!("🚀 Performance:");
println!(" • Catalog loaded once (cached)");
println!(" • Compact messages = faster API responses");
println!(" • Works offline (catalog bundled)\n");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("✅ Key Takeaways");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
println!("1. 🎯 Define errors ONCE on server with <catalog>");
println!(" diag! {{");
println!(" <catalog> // Auto-generates catalog");
println!(" E.API.AUTH.TOKEN_EXPIRED: {{ ... }}");
println!(" }}\n");
println!(
"2. 📦 Server sends compact: {{\"h\":\"{}\",\"f\":{{...}}}}",
E_API_AUTH_TOKEN_EXPIRED_HASH
);
println!(
" ({} bytes instead of {} bytes)\n",
compact_size, full_size
);
println!("3. 🌍 Browser expands with language-specific catalog");
println!(" - English browser → English message");
println!(" - Spanish browser → Spanish message");
println!(" - Same hash, different languages!\n");
println!("4. ⚡ Benefits:");
println!(" • 80%+ bandwidth savings");
println!(" • Client-side i18n (no server translation)");
println!(" • Offline-capable (catalog cached)");
println!(" • Type-safe hashes (compile-time constants)");
println!(" • Zero runtime cost on server\n");
println!("🎉 This is the power of the catalog pattern!");
}
#[cfg(not(feature = "metadata"))]
fn main() {}