# Catalog Guide - Network Efficiency & Internationalization
## Overview
The Catalog Renderer generates compact lookup tables that map error hashes to their documentation. This enables efficient error transmission over networks, multi-language support, and offline-first applications.
**Key Benefits:**
- ๐ **80% smaller payloads** - Send hash + fields instead of full error messages
- ๐ **Multi-language support** - Same hash, different language catalogs
- ๐ฑ **Offline-first** - Cache catalog locally for offline error display
- ๐ **Battery/bandwidth savings** - Critical for mobile and IoT devices
- ๐ **Faster transmission** - 40 bytes vs 200+ bytes per error
---
## Use Cases
### 1. IoT Devices
**Problem**: IoT devices have limited bandwidth and battery.
**Solution**: Send compact error codes, expand on server.
```text
Device โ Server
โโโโโโโโโโโโโโโ
Payload: "jGKFp,45.2" (12 bytes)
Server expands with catalog:
"E.SENSOR.TEMP.OVERHEAT: Temperature 45.2ยฐC exceeds threshold"
```
**Savings**: 12 bytes vs 65+ bytes (81% reduction)
### 2. Mobile Applications
**Problem**: Mobile apps need to minimize data usage and support offline mode.
**Solution**: Download catalog once, receive compact error hashes from API.
```text
App Launch:
โโโโโโโโโโ
1. Download catalog once (50 KB gzipped)
2. Cache locally with version check
API Requests:
โโโโโโโโโโโโโ
Server response: {"h": "jGKFp", "f": {"temp": "45.2"}} (40 bytes)
App expands using cached catalog
Offline Mode:
โโโโโโโโโโโโโ
App uses cached catalog to display errors
```
**Savings**: 40 bytes per error vs 200+ bytes (80% reduction)
### 3. Multi-Language Support (i18n)
**Problem**: Same error needs different messages per language.
**Solution**: Generate separate catalogs per language, client picks appropriate one.
```text
Server generates:
โโโโโโโโโโโโโโโโ
- catalog-en.json (English)
- catalog-es.json (Spanish)
- catalog-fr.json (French)
- catalog-de.json (German)
Client receives: {"h": "jGKFp", "f": {"temp": "45.2"}}
English catalog: "Temperature 45.2ยฐC exceeds threshold"
Spanish catalog: "Temperatura 45.2ยฐC supera el umbral"
French catalog: "Tempรฉrature 45.2ยฐC dรฉpasse le seuil"
German catalog: "Temperatur 45.2ยฐC รผberschreitet Schwellenwert"
```
**Same hash works with all languages!**
### 4. Microservices Architecture
**Problem**: Multiple services need consistent error codes.
**Solution**: Central error catalog, services send hashes, API gateway expands.
```text
Service A โ Gateway โ Client
โโโโโโโโโโโโโโโโโโโโโโโโโโ
Service: {"h": "jGKFp", "f": {...}}
Gateway: Expands with catalog โ Full error message
Client: Receives human-readable error
```
### 5. Offline-First Progressive Web Apps
**Problem**: PWA needs to display errors when offline.
**Solution**: Service worker caches catalog, expands errors locally.
```text
Online:
โโโโโโโ
1. Service worker downloads latest catalog
2. Stores in IndexedDB with version
Offline:
โโโโโโโโ
1. API call fails โ fallback to cached response
2. Response contains error hash
3. Service worker expands using cached catalog
4. User sees proper error message
```
---
## Catalog Formats
### Full Format
Complete documentation with all fields. Best for development/debugging.
```rust
use waddling_errors::doc_generator::{CatalogRenderer, CatalogFormat};
let catalog = CatalogRenderer::new(CatalogFormat::Full);
registry.render(vec![Box::new(catalog)], "target/catalog")?;
```
**Output** (`catalog-pub.json`):
```json
{
"version": "1.0.0",
"generated": "2024-11-19T15:00:00Z",
"algorithm": "base62-ahash-v1",
"role": "Public",
"errors": {
"jGKFp": {
"code": "E.AUTH.TOKEN.EXPIRED",
"severity": "Error",
"message": "JWT token expired at {expiry}",
"description": "Your session has expired. Please log in again.",
"hints": ["Use refresh token endpoint", "Check token expiration time"],
"tags": ["auth", "security", "session"],
"docs_url": "https://docs.example.com/errors/E.AUTH.TOKEN.EXPIRED"
}
}
}
```
**Size**: ~200 bytes per error
### Compact Format
Minimal fields with abbreviated keys. Best for production.
```rust
let catalog = CatalogRenderer::new(CatalogFormat::Compact);
registry.render(vec![Box::new(catalog)], "target/catalog")?;
```
**Output**:
```json
{
"v": "1.0.0",
"a": "base62-ahash-v1",
"r": "Public",
"e": {
"jGKFp": {
"c": "E.AUTH.TOKEN.EXPIRED",
"s": "E",
"m": "JWT token expired at {expiry}",
"d": "Your session has expired. Please log in again.",
"h": ["Use refresh token endpoint"]
}
}
}
```
**Size**: ~120 bytes per error (40% smaller than Full)
### Minimal Format
Only essential fields. Best for IoT/embedded systems.
```rust
let catalog = CatalogRenderer::new(CatalogFormat::Minimal);
registry.render(vec![Box::new(catalog)], "target/catalog")?;
```
**Output**:
```json
{
"jGKFp": ["E.AUTH.TOKEN.EXPIRED", "JWT token expired at {expiry}"]
}
```
**Size**: ~60 bytes per error (70% smaller than Full)
---
## Server-Side Implementation
### Generating Catalogs
```rust
use waddling_errors::doc_generator::{DocRegistry, CatalogRenderer, CatalogFormat};
fn generate_catalogs() -> Result<(), Box<dyn std::error::Error>> {
let mut registry = DocRegistry::new("MyAPI", "1.0.0");
// Register all your errors
register_all_errors(&mut registry)?;
// Generate catalogs for all roles
let catalog = CatalogRenderer::new(CatalogFormat::Compact);
registry.render_all_roles(vec![Box::new(catalog)], "target/catalog")?;
// Generates:
// catalog-pub.json - Public errors
// catalog-dev.json - Developer errors
// catalog-int.json - Internal errors
Ok(())
}
```
### Serving Catalogs
**Option 1: Static Files**
```rust
// Axum example
use axum::{Router, routing::get};
use tower_http::services::ServeDir;
let app = Router::new()
.nest_service("/catalogs", ServeDir::new("target/catalog"));
```
**Option 2: Embedded in Binary**
```rust
// Embed at compile time
const CATALOG: &str = include_str!("../target/catalog/catalog-pub.json");
async fn get_catalog() -> impl IntoResponse {
(
StatusCode::OK,
[(header::CONTENT_TYPE, "application/json")],
CATALOG
)
}
```
**Option 3: CDN Distribution**
```bash
# Upload to CDN
aws s3 cp target/catalog/catalog-pub.json s3://cdn.example.com/v1.0.0/
# Client downloads from CDN
curl https://cdn.example.com/v1.0.0/catalog-pub.json
```
### API Response Format
**Server sends compact error:**
```rust
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct ApiError {
h: String, // Hash
f: HashMap<String, String>, // Fields
#[serde(skip_serializing_if = "Option::is_none")]
ts: Option<u64>, // Timestamp
}
// Usage
let error = ApiError {
h: "jGKFp".to_string(),
f: [("expiry", "2024-11-19T15:00:00Z")].into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
ts: Some(1700406000),
};
// JSON: {"h":"jGKFp","f":{"expiry":"2024-11-19T15:00:00Z"},"ts":1700406000}
// Size: ~72 bytes
```
---
## Client-Side Implementation
### JavaScript/TypeScript
```typescript
// catalog-client.ts
interface CatalogEntry {
c: string; // code
s: string; // severity
m: string; // message
d?: string; // description
h?: string[]; // hints
}
interface Catalog {
v: string;
a: string;
r: string;
e: Record<string, CatalogEntry>;
}
class ErrorCatalog {
constructor(version: string) {
this.version = version;
}
async load(): Promise<void> {
// Try cache first
const cached = localStorage.getItem(`catalog-${this.version}`);
if (cached) {
this.catalog = JSON.parse(cached);
return;
}
// Download from server
const response = await fetch(`/catalogs/catalog-pub.json`);
this.catalog = await response.json();
// Cache for offline use
localStorage.setItem(`catalog-${this.version}`, JSON.stringify(this.catalog));
}
expand(hash: string, fields: Record<string, string>): string {
if (!this.catalog) {
throw new Error("Catalog not loaded");
}
const entry = this.catalog.e[hash];
if (!entry) {
return `Unknown error: ${hash}`;
}
// Replace field placeholders
let message = entry.m;
for (const [key, value] of Object.entries(fields)) {
message = message.replace(`{${key}}`, value);
}
return message;
}
getDetails(hash: string) {
if (!this.catalog) {
throw new Error("Catalog not loaded");
}
return this.catalog.e[hash];
}
}
// Usage
const catalog = new ErrorCatalog("1.0.0");
await catalog.load();
// API returns: {"h":"jGKFp","f":{"expiry":"2024-11-19T15:00:00Z"}}
const apiError = await response.json();
const message = catalog.expand(apiError.h, apiError.f);
console.log(message); // "JWT token expired at 2024-11-19T15:00:00Z"
```
### Swift (iOS)
```swift
// ErrorCatalog.swift
struct CatalogEntry: Codable {
let c: String // code
let s: String // severity
let m: String // message
let d: String? // description
let h: [String]? // hints
}
struct Catalog: Codable {
let v: String
let a: String
let r: String
let e: [String: CatalogEntry]
}
class ErrorCatalog {
private var catalog: Catalog?
func load() async throws {
// Try cache first
if let cached = UserDefaults.standard.data(forKey: "error_catalog") {
catalog = try JSONDecoder().decode(Catalog.self, from: cached)
return
}
// Download from server
let url = URL(string: "https://api.example.com/catalogs/catalog-pub.json")!
let (data, _) = try await URLSession.shared.data(from: url)
catalog = try JSONDecoder().decode(Catalog.self, from: data)
// Cache for offline use
UserDefaults.standard.set(data, forKey: "error_catalog")
}
func expand(hash: String, fields: [String: String]) -> String {
guard let catalog = catalog,
let entry = catalog.e[hash] else {
return "Unknown error: \(hash)"
}
var message = entry.m
for (key, value) in fields {
message = message.replacingOccurrences(of: "{\(key)}", with: value)
}
return message
}
}
// Usage
let catalog = ErrorCatalog()
try await catalog.load()
// API returns: {"h":"jGKFp","f":{"expiry":"2024-11-19T15:00:00Z"}}
let message = catalog.expand(hash: apiError.h, fields: apiError.f)
print(message) // "JWT token expired at 2024-11-19T15:00:00Z"
```
### Kotlin (Android)
```kotlin
// ErrorCatalog.kt
data class CatalogEntry(
val c: String, // code
val s: String, // severity
val m: String, // message
val d: String?, // description
val h: List<String>? // hints
)
data class Catalog(
val v: String,
val a: String,
val r: String,
val e: Map<String, CatalogEntry>
)
class ErrorCatalog(private val context: Context) {
private var catalog: Catalog? = null
suspend fun load() {
// Try cache first
val prefs = context.getSharedPreferences("error_catalog", Context.MODE_PRIVATE)
val cached = prefs.getString("catalog", null)
if (cached != null) {
catalog = Gson().fromJson(cached, Catalog::class.java)
return
}
// Download from server
val response = ktorClient.get("https://api.example.com/catalogs/catalog-pub.json")
catalog = response.body()
// Cache for offline use
prefs.edit().putString("catalog", Gson().toJson(catalog)).apply()
}
fun expand(hash: String, fields: Map<String, String>): String {
val entry = catalog?.e?.get(hash) ?: return "Unknown error: $hash"
var message = entry.m
fields.forEach { (key, value) ->
message = message.replace("{$key}", value)
}
return message
}
}
// Usage
val catalog = ErrorCatalog(context)
catalog.load()
// API returns: {"h":"jGKFp","f":{"expiry":"2024-11-19T15:00:00Z"}}
val message = catalog.expand(apiError.h, apiError.f)
println(message) // "JWT token expired at 2024-11-19T15:00:00Z"
```
---
## Multi-Language Support
### Generating Language-Specific Catalogs
```rust
use waddling_errors::doc_generator::{DocRegistry, CatalogRenderer, CatalogFormat};
fn generate_i18n_catalogs() -> Result<(), Box<dyn std::error::Error>> {
// English catalog
let mut registry_en = DocRegistry::new("MyAPI", "1.0.0");
register_errors_en(&mut registry_en)?;
let catalog = CatalogRenderer::new(CatalogFormat::Compact);
registry_en.render(vec![Box::new(catalog)], "target/catalog/en")?;
// Spanish catalog
let mut registry_es = DocRegistry::new("MyAPI", "1.0.0");
register_errors_es(&mut registry_es)?;
let catalog = CatalogRenderer::new(CatalogFormat::Compact);
registry_es.render(vec![Box::new(catalog)], "target/catalog/es")?;
// French catalog
let mut registry_fr = DocRegistry::new("MyAPI", "1.0.0");
register_errors_fr(&mut registry_fr)?;
let catalog = CatalogRenderer::new(CatalogFormat::Compact);
registry_fr.render(vec![Box::new(catalog)], "target/catalog/fr")?;
Ok(())
}
fn register_errors_en(registry: &mut DocRegistry) -> Result<(), Box<dyn std::error::Error>> {
registry.register_code_extended(
&ERR_TOKEN_EXPIRED,
"JWT token expired at {expiry}",
&["Use refresh token endpoint"],
&["auth"],
Some(Role::Public),
&[], None, &[],
)?;
Ok(())
}
fn register_errors_es(registry: &mut DocRegistry) -> Result<(), Box<dyn std::error::Error>> {
registry.register_code_extended(
&ERR_TOKEN_EXPIRED,
"Token JWT expirado en {expiry}",
&["Usar endpoint de actualizaciรณn de token"],
&["auth"],
Some(Role::Public),
&[], None, &[],
)?;
Ok(())
}
fn register_errors_fr(registry: &mut DocRegistry) -> Result<(), Box<dyn std::error::Error>> {
registry.register_code_extended(
&ERR_TOKEN_EXPIRED,
"Token JWT expirรฉ ร {expiry}",
&["Utiliser le point de terminaison de rafraรฎchissement du token"],
&["auth"],
Some(Role::Public),
&[], None, &[],
)?;
Ok(())
}
```
### Client Language Selection
```typescript
class MultiLangCatalog {
private catalogs: Map<string, Catalog> = new Map();
private currentLang: string = 'en';
async loadLanguage(lang: string): Promise<void> {
if (this.catalogs.has(lang)) {
this.currentLang = lang;
return;
}
const response = await fetch(`/catalogs/${lang}/catalog-pub.json`);
const catalog = await response.json();
this.catalogs.set(lang, catalog);
this.currentLang = lang;
}
expand(hash: string, fields: Record<string, string>): string {
const catalog = this.catalogs.get(this.currentLang);
if (!catalog) {
throw new Error(`Catalog not loaded for language: ${this.currentLang}`);
}
const entry = catalog.e[hash];
if (!entry) {
return `Unknown error: ${hash}`;
}
let message = entry.m;
for (const [key, value] of Object.entries(fields)) {
message = message.replace(`{${key}}`, value);
}
return message;
}
setLanguage(lang: string): void {
if (!this.catalogs.has(lang)) {
throw new Error(`Language not loaded: ${lang}`);
}
this.currentLang = lang;
}
}
// Usage
const catalog = new MultiLangCatalog();
await catalog.loadLanguage('en');
await catalog.loadLanguage('es');
// Switch to Spanish
catalog.setLanguage('es');
const message = catalog.expand(apiError.h, apiError.f);
// "Token JWT expirado en 2024-11-19T15:00:00Z"
```
---
## Performance Optimizations
### Compression
Catalogs compress very well with gzip:
```bash
# Original: 50 KB
gzip catalog-pub.json
# Compressed: ~8 KB (84% reduction)
```
### Versioning & Caching
```rust
// Include version in filename
let catalog = CatalogRenderer::new(CatalogFormat::Compact)
.with_version("1.0.0");
registry.render(vec![Box::new(catalog)], "target/catalog")?;
// Generates: catalog-pub-v1.0.0.json
```
HTTP headers for caching:
```rust
async fn serve_catalog() -> impl IntoResponse {
let catalog = include_str!("../target/catalog/catalog-pub-v1.0.0.json");
(
StatusCode::OK,
[
(header::CONTENT_TYPE, "application/json"),
(header::CACHE_CONTROL, "public, max-age=86400, immutable"),
(header::ETAG, "v1.0.0"),
],
catalog
)
}
```
### Differential Updates
For large catalogs, send only changes:
```typescript
interface CatalogDiff {
added: Record<string, CatalogEntry>;
modified: Record<string, CatalogEntry>;
removed: string[];
}
async function updateCatalog(currentVersion: string): Promise<void> {
const response = await fetch(`/catalogs/diff/${currentVersion}/latest`);
const diff: CatalogDiff = await response.json();
// Apply diff to cached catalog
const catalog = getCachedCatalog();
Object.assign(catalog.e, diff.added);
Object.assign(catalog.e, diff.modified);
diff.removed.forEach(hash => delete catalog.e[hash]);
saveCatalog(catalog);
}
```
---
## Best Practices
### DO โ
1. **Version your catalogs** - Include version in filename/URL
2. **Compress responses** - Use gzip/brotli compression
3. **Cache aggressively** - Catalogs rarely change
4. **Validate hashes** - Ensure hash exists before expanding
5. **Fallback to hash** - Display hash if catalog missing
6. **Use Compact format** - For production APIs
7. **Generate per-language** - Don't mix languages in one catalog
8. **Include timestamps** - Track when catalog was generated
### DON'T โ
1. **Don't send full errors** - Defeats the purpose
2. **Don't skip caching** - Downloads on every request waste bandwidth
3. **Don't use Full format** - Too large for production
4. **Don't mix roles** - Keep Public/Developer/Internal separate
5. **Don't hardcode hashes** - Use hash constants from code
6. **Don't ignore version** - Old clients need compatible catalogs
7. **Don't skip validation** - Malformed catalogs break clients
---
## Monitoring & Analytics
### Track Catalog Usage
```rust
// Server-side metrics
#[derive(Serialize)]
struct CatalogMetrics {
downloads: u64,
cache_hits: u64,
version: String,
language: String,
}
async fn download_catalog(lang: String, version: String) -> impl IntoResponse {
// Track metrics
metrics::increment_counter!("catalog_downloads", "lang" => lang.clone());
// Serve catalog
let catalog = load_catalog(&lang, &version);
(StatusCode::OK, Json(catalog))
}
```
### Error Expansion Metrics
```typescript
// Client-side analytics
function expandWithMetrics(hash: string, fields: Record<string, string>): string {
const start = performance.now();
try {
const message = catalog.expand(hash, fields);
// Track successful expansion
analytics.track('error_expanded', {
hash,
duration: performance.now() - start,
success: true
});
return message;
} catch (error) {
// Track failures
analytics.track('error_expanded', {
hash,
duration: performance.now() - start,
success: false,
error: error.message
});
return `Unknown error: ${hash}`;
}
}
```
---
## Example: Complete IoT System
```rust
// device.rs (IoT device - no_std)
#![no_std]
use waddling_errors::prelude::*;
// Send only hash + fields
pub fn send_error_compact(hash: &str, temp: f32) {
let payload = format!("{},{}", hash, temp); // "jGKFp,45.2"
radio_transmit(payload.as_bytes()); // 12 bytes
}
// server.rs (Server - std)
use waddling_errors::doc_generator::{DocRegistry, CatalogRenderer};
fn expand_device_error(payload: &str) -> String {
let parts: Vec<&str> = payload.split(',').collect();
let hash = parts[0];
let temp = parts[1];
// Load catalog
let catalog = load_catalog("en");
// Expand error
catalog.expand(hash, [("temp", temp)].into())
}
fn main() {
// Generate catalog at build time
let mut registry = DocRegistry::new("IoTDevice", "1.0.0");
register_device_errors(&mut registry).unwrap();
let catalog = CatalogRenderer::new(CatalogFormat::Minimal);
registry.render(vec![Box::new(catalog)], "target/catalog").unwrap();
// Simulate receiving device error
let device_payload = "jGKFp,45.2";
let expanded = expand_device_error(device_payload);
println!("{}", expanded);
// "Temperature 45.2ยฐC exceeds threshold"
}
```
---
## Conclusion
The Catalog Renderer transforms waddling-errors from a documentation tool into a network-efficient error communication system. By separating error metadata (catalog) from error instances (hash + fields), you achieve:
- **Massive bandwidth savings** (80%+)
- **Multi-language support** without code changes
- **Offline-first capabilities**
- **Consistent error codes** across all platforms
This makes waddling-errors ideal for:
- IoT and embedded systems
- Mobile applications
- Microservices architectures
- Offline-first PWAs
- Multi-language applications
For more information, see:
- [DOC_GENERATION_GUIDE.md](DOC_GENERATION_GUIDE.md) - Full documentation generation workflow
- [examples/browser_server_catalog.rs](../../waddling-errors-macros/examples/browser_server_catalog.rs) - Complete working example
- [API Documentation](https://docs.rs/waddling-errors) - Full API reference