452 lines
13 KiB
Rust
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(),
|
|
})
|
|
}
|
|
}
|
|
}
|