Rustelo/shared/src/lib.rs
2025-07-07 23:06:11 +01:00

452 lines
13 KiB
Rust

//! # RUSTELO Shared
//!
//! <div align="center">
//! <img src="../logos/rustelo_dev-logo-h.svg" alt="RUSTELO" width="300" />
//! </div>
//!
//! Shared types, utilities, and functionality for the RUSTELO web application framework.
//!
//! ## Overview
//!
//! The shared crate contains common types, utilities, and functionality that are used across
//! both the client and server components of RUSTELO applications. This includes authentication
//! types, content management structures, internationalization support, and configuration utilities.
//!
//! ## Features
//!
//! - **🔐 Authentication Types** - Shared auth structures and utilities
//! - **📄 Content Management** - Common content types and processing
//! - **🌐 Internationalization** - Multi-language support with Fluent
//! - **⚙️ Configuration** - Shared configuration management
//! - **🎨 Menu System** - Navigation and menu configuration
//! - **📋 Type Safety** - Strongly typed interfaces for client-server communication
//!
//! ## Architecture
//!
//! The shared crate is organized into several key modules:
//!
//! - [`auth`] - Authentication types and utilities
//! - [`content`] - Content management types and processing
//!
//! Additional functionality includes:
//! - Menu configuration and internationalization
//! - Fluent resource management
//! - Content file loading utilities
//! - Type-safe configuration structures
//!
//! ## Quick Start
//!
//! ### Menu Configuration
//!
//! ```rust
//! use shared::{MenuConfig, load_menu_toml};
//!
//! // Load menu from TOML file
//! let menu = load_menu_toml().unwrap_or_default();
//!
//! // Access menu items
//! for item in menu.menu {
//! println!("Route: {}, Label (EN): {}", item.route, item.label.en);
//! }
//! ```
//!
//! ### Internationalization
//!
//! ```rust
//! use shared::{get_bundle, t};
//! use std::collections::HashMap;
//!
//! // Get localization bundle
//! let bundle = get_bundle("en").expect("Failed to load English bundle");
//!
//! // Translate text
//! let welcome_msg = t(&bundle, "welcome", None);
//! println!("{}", welcome_msg);
//!
//! // Translate with arguments
//! let mut args = HashMap::new();
//! args.insert("name", "RUSTELO");
//! let greeting = t(&bundle, "greeting", Some(&args));
//! ```
//!
//! ## Type Definitions
//!
//! ### Menu System
//!
//! ```rust
//! use shared::{MenuConfig, MenuItem, MenuLabel};
//!
//! let menu_item = MenuItem {
//! route: "/about".to_string(),
//! label: MenuLabel {
//! en: "About".to_string(),
//! es: "Acerca de".to_string(),
//! },
//! };
//! ```
//!
//! ### Text Localization
//!
//! ```rust
//! use shared::Texts;
//! use std::collections::HashMap;
//!
//! let mut texts = Texts::default();
//! texts.en.insert("welcome".to_string(), "Welcome".to_string());
//! texts.es.insert("welcome".to_string(), "Bienvenido".to_string());
//! ```
//!
//! ## Internationalization Support
//!
//! RUSTELO uses [Fluent](https://projectfluent.org/) for internationalization:
//!
//! - **Resource Loading** - Automatic loading of .ftl files
//! - **Language Fallback** - Graceful fallback to English
//! - **Parameter Substitution** - Dynamic text with variables
//! - **Pluralization** - Proper plural forms for different languages
//!
//! ### Supported Languages
//!
//! - **English (en)** - Primary language
//! - **Spanish (es)** - Secondary language
//! - **Extensible** - Easy to add more languages
//!
//! ## Configuration Management
//!
//! The shared crate provides utilities for loading configuration from various sources:
//!
//! - **TOML Files** - Structured configuration files
//! - **Environment Variables** - Runtime configuration
//! - **Fallback Defaults** - Graceful degradation
//!
//! ## Error Handling
//!
//! All functions return `Result` types for proper error handling:
//!
//! ```rust
//! use shared::load_menu_toml;
//!
//! match load_menu_toml() {
//! Ok(menu) => println!("Loaded {} menu items", menu.menu.len()),
//! Err(e) => eprintln!("Failed to load menu: {}", e),
//! }
//! ```
//!
//! ## Cross-Platform Support
//!
//! The shared crate is designed to work across different targets:
//!
//! - **Server** - Native Rust environments
//! - **Client** - WebAssembly (WASM) environments
//! - **Testing** - Development and CI environments
//!
//! ## Performance Considerations
//!
//! - **Lazy Loading** - Resources loaded on demand
//! - **Caching** - Efficient resource reuse
//! - **Memory Management** - Careful memory usage for WASM
//! - **Bundle Size** - Optimized for small WASM bundles
//!
//! ## Examples
//!
//! ### Creating a Multi-language Menu
//!
//! ```rust
//! use shared::{MenuConfig, MenuItem, MenuLabel};
//!
//! let menu = MenuConfig {
//! menu: vec![
//! MenuItem {
//! route: "/".to_string(),
//! label: MenuLabel {
//! en: "Home".to_string(),
//! es: "Inicio".to_string(),
//! },
//! },
//! MenuItem {
//! route: "/about".to_string(),
//! label: MenuLabel {
//! en: "About".to_string(),
//! es: "Acerca de".to_string(),
//! },
//! },
//! ],
//! };
//! ```
//!
//! ### Loading and Using Fluent Resources
//!
//! ```rust
//! use shared::{get_bundle, t};
//! use std::collections::HashMap;
//!
//! // Load Spanish bundle
//! let bundle = get_bundle("es").expect("Failed to load Spanish bundle");
//!
//! // Simple translation
//! let app_title = t(&bundle, "app-title", None);
//!
//! // Translation with variables
//! let mut args = HashMap::new();
//! args.insert("user", "María");
//! let welcome = t(&bundle, "welcome-user", Some(&args));
//! ```
//!
//! ## Contributing
//!
//! When adding new shared functionality:
//!
//! 1. **Keep it Generic** - Ensure it's useful for both client and server
//! 2. **Document Types** - Add comprehensive documentation
//! 3. **Handle Errors** - Use proper error types and handling
//! 4. **Test Thoroughly** - Add tests for all platforms
//! 5. **Consider Performance** - Optimize for WASM environments
//!
//! ## License
//!
//! This project is licensed under the MIT License - see the [LICENSE](https://github.com/yourusername/rustelo/blob/main/LICENSE) file for details.
#![allow(unused_imports)]
#![allow(dead_code)]
pub mod auth;
pub mod content;
use fluent::{FluentBundle, FluentResource};
use fluent_bundle::FluentArgs;
use serde::Deserialize;
use std::borrow::Cow;
use std::collections::HashMap;
use unic_langid::LanguageIdentifier;
#[derive(Debug, Clone, Deserialize)]
pub struct MenuLabel {
pub en: String,
pub es: String,
}
impl Default for MenuLabel {
fn default() -> Self {
Self {
en: "Menu".to_string(),
es: "Menú".to_string(),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct MenuItem {
pub route: String,
pub label: MenuLabel,
}
impl Default for MenuItem {
fn default() -> Self {
Self {
route: "/".to_string(),
label: MenuLabel::default(),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct MenuConfig {
pub menu: Vec<MenuItem>,
}
impl Default for MenuConfig {
fn default() -> Self {
Self {
menu: vec![
MenuItem {
route: "/".to_string(),
label: MenuLabel {
en: "Home".to_string(),
es: "Inicio".to_string(),
},
},
MenuItem {
route: "/about".to_string(),
label: MenuLabel {
en: "About".to_string(),
es: "Acerca de".to_string(),
},
},
],
}
}
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
pub struct Texts {
pub en: std::collections::HashMap<String, String>,
pub es: std::collections::HashMap<String, String>,
}
impl Default for Texts {
fn default() -> Self {
Self {
en: std::collections::HashMap::new(),
es: std::collections::HashMap::new(),
}
}
}
// Load FTL resources from files instead of hardcoded content
fn load_en_ftl() -> &'static str {
Box::leak(
std::fs::read_to_string(
get_content_path("en.ftl")
.unwrap_or_else(|_| std::path::PathBuf::from("content/en.ftl")),
)
.unwrap_or_else(|_| "app-title = Rustelo App\nwelcome = Welcome".to_string())
.into_boxed_str(),
)
}
fn load_es_ftl() -> &'static str {
Box::leak(
std::fs::read_to_string(
get_content_path("es.ftl")
.unwrap_or_else(|_| std::path::PathBuf::from("content/es.ftl")),
)
.unwrap_or_else(|_| "app-title = Aplicación Rustelo\nwelcome = Bienvenido".to_string())
.into_boxed_str(),
)
}
// Dynamic FTL resources loaded from files
static EN_FTL: std::sync::OnceLock<&str> = std::sync::OnceLock::new();
static ES_FTL: std::sync::OnceLock<&str> = std::sync::OnceLock::new();
fn get_en_ftl() -> &'static str {
EN_FTL.get_or_init(|| load_en_ftl())
}
fn get_es_ftl() -> &'static str {
ES_FTL.get_or_init(|| load_es_ftl())
}
// Content loading utilities
use std::path::PathBuf;
pub fn get_content_path(filename: &str) -> Result<PathBuf, Box<dyn std::error::Error>> {
// Try to get root path from environment or use current directory
let root_path = std::env::var("ROOT_PATH")
.map(PathBuf::from)
.unwrap_or_else(|_| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
let content_path = root_path.join("content").join(filename);
// If the file exists, return the path
if content_path.exists() {
Ok(content_path)
} else {
// Fallback to relative path
let fallback_path = PathBuf::from("content").join(filename);
if fallback_path.exists() {
Ok(fallback_path)
} else {
Err(format!("Content file not found: {}", filename).into())
}
}
}
pub fn get_bundle(lang: &str) -> Result<FluentBundle<FluentResource>, Box<dyn std::error::Error>> {
let langid: LanguageIdentifier = lang.parse().unwrap_or_else(|_| {
"en".parse().unwrap_or_else(|e| {
eprintln!(
"Critical error: Default language 'en' failed to parse: {}",
e
);
// This should never happen, but we'll create a minimal fallback
LanguageIdentifier::from_parts(
unic_langid::subtags::Language::from_bytes(b"en").unwrap_or_else(|e| {
eprintln!("Critical error: failed to create 'en' language: {}", e);
// Fallback to creating a new language identifier from scratch
match "en".parse::<unic_langid::subtags::Language>() {
Ok(lang) => lang,
Err(_) => {
// If even this fails, we'll use the default language
eprintln!("Using default language as final fallback");
unic_langid::subtags::Language::default()
}
}
}),
None,
None,
&[],
)
})
});
let ftl_str = match lang {
"es" => get_es_ftl(),
_ => get_en_ftl(),
};
let res = FluentResource::try_new(ftl_str.to_string())
.map_err(|e| format!("Failed to parse FTL resource: {:?}", e))?;
let mut bundle = FluentBundle::new(vec![langid]);
bundle
.add_resource(res)
.map_err(|e| format!("Failed to add FTL resource to bundle: {:?}", e))?;
Ok(bundle)
}
pub fn t(
bundle: &FluentBundle<FluentResource>,
key: &str,
args: Option<&HashMap<&str, &str>>,
) -> String {
let msg = bundle.get_message(key).and_then(|m| m.value());
if let Some(msg) = msg {
let mut errors = vec![];
let fargs = args.map(|a| {
let mut f = FluentArgs::new();
for (k, v) in a.iter() {
f.set(*k, *v);
}
f
});
bundle
.format_pattern(msg, fargs.as_ref(), &mut errors)
.to_string()
} else {
key.to_string()
}
}
pub fn load_menu_toml() -> Result<MenuConfig, Box<dyn std::error::Error>> {
// Try to load from file system first
match get_content_path("menu.toml") {
Ok(path) => {
let content = std::fs::read_to_string(&path)
.map_err(|e| format!("Failed to read menu.toml from {}: {}", path.display(), e))?;
toml::from_str(&content).map_err(|e| format!("Failed to parse menu.toml: {}", e).into())
}
Err(_) => {
// Return default menu if file not found
Ok(MenuConfig { menu: vec![] })
}
}
}
pub fn load_texts_toml() -> Result<Texts, Box<dyn std::error::Error>> {
// Try to load from file system first
match get_content_path("texts.toml") {
Ok(path) => {
let content = std::fs::read_to_string(&path)
.map_err(|e| format!("Failed to read texts.toml from {}: {}", path.display(), e))?;
toml::from_str(&content)
.map_err(|e| format!("Failed to parse texts.toml: {}", e).into())
}
Err(_) => {
// Return default texts if file not found
Ok(Texts {
en: std::collections::HashMap::new(),
es: std::collections::HashMap::new(),
})
}
}
}