From ed5f96361b96d982003bcc16d2fa96770cc6306a Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 7 Apr 2024 11:42:37 -0400 Subject: [PATCH] Improve built-in services --- usdpl-back/src/rpc/mod.rs | 2 +- usdpl-back/src/rpc/registry.rs | 2 +- usdpl-back/src/services_impl/dev_tools.rs | 16 +- usdpl-back/src/services_impl/translations.rs | 43 +++- usdpl-build/protos/debug.proto | 18 +- usdpl-build/protos/translations.proto | 18 +- usdpl-build/src/front/service_generator.rs | 197 +++++++++++++++++-- usdpl-core/src/lib.rs | 1 + usdpl-front/src/lib.rs | 30 --- 9 files changed, 276 insertions(+), 51 deletions(-) diff --git a/usdpl-back/src/rpc/mod.rs b/usdpl-back/src/rpc/mod.rs index 506bfd6..ed902a3 100644 --- a/usdpl-back/src/rpc/mod.rs +++ b/usdpl-back/src/rpc/mod.rs @@ -1,5 +1,5 @@ mod registry; -pub use registry::{ServiceRegistry, StaticServiceRegistry}; +pub use registry::{/*ServiceRegistry, */StaticServiceRegistry}; mod websocket_stream; pub use websocket_stream::ws_stream; diff --git a/usdpl-back/src/rpc/registry.rs b/usdpl-back/src/rpc/registry.rs index 3ca2ee8..c1d0b81 100644 --- a/usdpl-back/src/rpc/registry.rs +++ b/usdpl-back/src/rpc/registry.rs @@ -37,7 +37,7 @@ impl<'a> ServiceRegistry<'a> { pub fn with_builtins() -> Self { let mut reg = Self::default(); reg.register(crate::services::usdpl::DevToolsServer::new(crate::services_impl::DevTools{})) - .register(crate::services::usdpl::TranslationsServer::new(crate::services_impl::Translations{})); + .register(crate::services::usdpl::TranslationsServer::new(crate::services_impl::Translations::new())); reg } } diff --git a/usdpl-back/src/services_impl/dev_tools.rs b/usdpl-back/src/services_impl/dev_tools.rs index 1be811f..a8048fe 100644 --- a/usdpl-back/src/services_impl/dev_tools.rs +++ b/usdpl-back/src/services_impl/dev_tools.rs @@ -17,6 +17,20 @@ impl<'a> generated::IDevTools<'a> for DevTools { lvl if lvl == generated::LogLevel::Error as _ => log::error!("{}", input.msg), lvl => return Err(Box::::from(format!("Unexpected input log level {}", lvl))) } - Ok(generated::Empty{ ok: true }) + Ok(generated::Empty{}) + } + + async fn echo( + &mut self, + input: generated::Ok, + ) -> Result> { + Ok(input) + } + + async fn version( + &mut self, + _: generated::Empty + ) -> Result> { + Ok(generated::VersionString { version: env!("CARGO_PKG_VERSION").to_owned() }) } } diff --git a/usdpl-back/src/services_impl/translations.rs b/usdpl-back/src/services_impl/translations.rs index d51734e..74401c3 100644 --- a/usdpl-back/src/services_impl/translations.rs +++ b/usdpl-back/src/services_impl/translations.rs @@ -1,7 +1,30 @@ use crate::services::usdpl as generated; /// Built-in translation service implementation -pub(crate) struct Translations {} +pub(crate) struct Translations { + catalogs: async_lock::RwLock>, +} + +impl Translations { + pub fn new() -> Self { + Self { + catalogs: async_lock::RwLock::new(std::collections::HashMap::new()), + } + } + + async fn get_catalog(&self, lang: &str) -> Result { + let catalogs_rlock = self.catalogs.read().await; + if let Some(catalog) = catalogs_rlock.get(lang) { + Ok(catalog.to_owned()) + } else { + drop(catalogs_rlock); + let catalog = load_locale(lang)?; + let mut catalogs_wlock = self.catalogs.write().await; + catalogs_wlock.insert(lang.to_owned(), catalog.clone()); + Ok(catalog) + } + } +} #[async_trait::async_trait] impl<'a> generated::ITranslations<'a> for Translations { @@ -9,12 +32,12 @@ impl<'a> generated::ITranslations<'a> for Translations { &mut self, input: generated::LanguageRequest, ) -> Result> { - let catalog = load_locale(&input.lang).map_err(|e| Box::new(e) as _)?; + let catalog = self.get_catalog(&input.lang).await.map_err(|e| Box::new(e) as _)?; let catalog_map = catalog.nalltext(); let mut map = std::collections::HashMap::with_capacity(catalog_map.len()); for (key, val) in catalog_map.into_iter() { if val.len() > 1 { - log::warn!("Translations key {} for language {} has plural entries which aren't currently supported", key, input.lang); + log::warn!("Translations key {} for language {} has plural entries which aren't supported by Translations::get_language(...)", key, input.lang); } if let Some(val_0) = val.get(0) { map.insert(key.to_owned(), val_0.to_owned()); @@ -22,6 +45,20 @@ impl<'a> generated::ITranslations<'a> for Translations { } Ok(generated::TranslationsReply { translations: map }) } + + async fn get_translation( + &mut self, + input: generated::TranslationRequest, + ) -> Result> { + let catalog = self.get_catalog(&input.lang).await.map_err(|e| Box::new(e) as _)?; + let translated = if input.count == 0 { + catalog.gettext(&input.msg_id) + } else { + catalog.ngettext(&input.msg_id, &input.msg_id, input.count) + }.to_owned(); + let is_default = translated == input.msg_id; + Ok(generated::TranslatedString { translated, is_default, uuid: input.uuid }) + } } fn load_locale(lang: &str) -> Result { diff --git a/usdpl-build/protos/debug.proto b/usdpl-build/protos/debug.proto index 86aaf0b..e611fe7 100644 --- a/usdpl-build/protos/debug.proto +++ b/usdpl-build/protos/debug.proto @@ -2,10 +2,16 @@ syntax = "proto3"; package usdpl; -// The translation service +// The tools service service DevTools { - // Retrieves all translations for the provided 4-letter code + // Write a message to the back-end log rpc Log (LogMessage) returns (Empty); + + // Echo a message through the back-end + rpc Echo (Ok) returns (Ok); + + // Retrieve the USDPL version + rpc Version (Empty) returns (VersionString); } enum LogLevel { @@ -22,6 +28,12 @@ message LogMessage { string msg = 2; } -message Empty { +message Empty {} + +message Ok { bool ok = 1; } + +message VersionString { + string version = 1; +} diff --git a/usdpl-build/protos/translations.proto b/usdpl-build/protos/translations.proto index 3fc4e59..4c6fd89 100644 --- a/usdpl-build/protos/translations.proto +++ b/usdpl-build/protos/translations.proto @@ -5,7 +5,9 @@ package usdpl; // The translation service service Translations { // Retrieves all translations for the provided 4-letter code - rpc GetLanguage (LanguageRequest) returns (TranslationsReply) {} + rpc GetLanguage (LanguageRequest) returns (TranslationsReply); + /// Retrieve a specific translation + rpc GetTranslation(TranslationRequest) returns (TranslatedString); } // The request message containing the language code @@ -17,3 +19,17 @@ message LanguageRequest { message TranslationsReply { map translations = 1; } + +// The request message for a specific translated string +message TranslationRequest { + string lang = 1; + string msg_id = 2; + uint64 count = 3; + uint64 uuid = 4; +} + +message TranslatedString { + string translated = 1; + bool is_default = 2; + uint64 uuid = 3; +} diff --git a/usdpl-build/src/front/service_generator.rs b/usdpl-build/src/front/service_generator.rs index a4201f1..f41d4c4 100644 --- a/usdpl-build/src/front/service_generator.rs +++ b/usdpl-build/src/front/service_generator.rs @@ -93,8 +93,36 @@ fn generate_service_methods( Some(x2.into_wasm()) }, Err(e) => { - // log error - log::error!("service:{}|method:{}|error:{}", self.service.descriptor(), #method_name_str, e); + if let Some(e_handler) = &self.error_handler { + let error_info = js_sys::Object::new(); + js_sys::Reflect::set( + &error_info, + &JsValue::from("service"), + &JsValue::from(self.service.descriptor()) + ).unwrap(); + js_sys::Reflect::set( + &error_info, + &JsValue::from("method"), + &JsValue::from(#method_name_str) + ).unwrap(); + js_sys::Reflect::set( + &error_info, + &JsValue::from("error"), + &JsValue::from("todo"/*TODO*/) + ).unwrap(); + if let Err(call_e) = e_handler.call1(&JsValue::UNDEFINED, &error_info) { + // log error + log::error!("service:{}|method:{}|error:{}|js error:{}", self.service.descriptor(), #method_name_str, e, + if let Some(s) = call_e.as_string() { + s + } else { + format!("{:?}", call_e) + }); + } + } else { + // log error + log::error!("service:{}|method:{}|error:{}", self.service.descriptor(), #method_name_str, e); + } None } } @@ -116,8 +144,36 @@ fn generate_service_methods( Some(x2.into_wasm()) }, Err(e) => { - // log error - log::error!("service:{}|method:{}|error:{}", self.service.descriptor(), #method_name_str, e); + if let Some(e_handler) = &self.error_handler { + let error_info = js_sys::Object::new(); + js_sys::Reflect::set( + &error_info, + &JsValue::from("service"), + &JsValue::from(self.service.descriptor()) + ).unwrap(); + js_sys::Reflect::set( + &error_info, + &JsValue::from("method"), + &JsValue::from(#method_name_str) + ).unwrap(); + js_sys::Reflect::set( + &error_info, + &JsValue::from("error"), + &JsValue::from("todo"/*TODO*/) + ).unwrap(); + if let Err(call_e) = e_handler.call1(&JsValue::UNDEFINED, &error_info) { + // log error + log::error!("service:{}|method:{}|error:{}|js error:{}", self.service.descriptor(), #method_name_str, e, + if let Some(s) = call_e.as_string() { + s + } else { + format!("{:?}", call_e) + }); + } + } else { + // log error + log::error!("service:{}|method:{}|error:{}", self.service.descriptor(), #method_name_str, e); + } None } } @@ -179,7 +235,35 @@ fn generate_service_methods( while let Some(next_result) = x.next().await { match next_result { Err(e) => { - log::error!("service:{}|method:{}|error:{}", self.service.descriptor(), #method_name_str, e); + if let Some(e_handler) = &self.error_handler { + let error_info = js_sys::Object::new(); + js_sys::Reflect::set( + &error_info, + &JsValue::from("service"), + &JsValue::from(self.service.descriptor()) + ).unwrap(); + js_sys::Reflect::set( + &error_info, + &JsValue::from("method"), + &JsValue::from(#method_name_str) + ).unwrap(); + js_sys::Reflect::set( + &error_info, + &JsValue::from("error"), + &JsValue::from("todo"/*TODO*/) + ).unwrap(); + if let Err(call_e) = e_handler.call1(&JsValue::UNDEFINED, &error_info) { + // log error + log::error!("service:{}|method:{}|error:{}|js error:{}", self.service.descriptor(), #method_name_str, e, + if let Some(s) = call_e.as_string() { + s + } else { + format!("{:?}", call_e) + }); + } + } else { + log::error!("service:{}|method:{}|error:{}", self.service.descriptor(), #method_name_str, e); + } }, Ok(item) => { #[inline(always)] @@ -208,8 +292,36 @@ fn generate_service_methods( } }, Err(e) => { - // log error - log::error!("service:{}|method:{}|error:{}", self.service.descriptor(), #method_name_str, e); + if let Some(e_handler) = &self.error_handler { + let error_info = js_sys::Object::new(); + js_sys::Reflect::set( + &error_info, + &JsValue::from("service"), + &JsValue::from(self.service.descriptor()) + ).unwrap(); + js_sys::Reflect::set( + &error_info, + &JsValue::from("method"), + &JsValue::from(#method_name_str) + ).unwrap(); + js_sys::Reflect::set( + &error_info, + &JsValue::from("error"), + &JsValue::from("todo"/*TODO*/) + ).unwrap(); + if let Err(call_e) = e_handler.call1(&JsValue::UNDEFINED, &error_info) { + // log error + log::error!("service:{}|method:{}|error:{}|js error:{}", self.service.descriptor(), #method_name_str, e, + if let Some(s) = call_e.as_string() { + s + } else { + format!("{:?}", call_e) + }); + } + } else { + // log error + log::error!("service:{}|method:{}|error:{}", self.service.descriptor(), #method_name_str, e); + } } } } @@ -229,7 +341,35 @@ fn generate_service_methods( while let Some(next_result) = x.next().await { match next_result { Err(e) => { - log::error!("service:{}|method:{}|error:{}", self.service.descriptor(), #method_name_str, e); + if let Some(e_handler) = &self.error_handler { + let error_info = js_sys::Object::new(); + js_sys::Reflect::set( + &error_info, + &JsValue::from("service"), + &JsValue::from(self.service.descriptor()) + ).unwrap(); + js_sys::Reflect::set( + &error_info, + &JsValue::from("method"), + &JsValue::from(#method_name_str) + ).unwrap(); + js_sys::Reflect::set( + &error_info, + &JsValue::from("error"), + &JsValue::from("todo"/*TODO*/) + ).unwrap(); + if let Err(call_e) = e_handler.call1(&JsValue::UNDEFINED, &error_info) { + // log error + log::error!("service:{}|method:{}|error:{}|js error:{}", self.service.descriptor(), #method_name_str, e, + if let Some(s) = call_e.as_string() { + s + } else { + format!("{:?}", call_e) + }); + } + } else { + log::error!("service:{}|method:{}|error:{}", self.service.descriptor(), #method_name_str, e); + } }, Ok(item) => { #[inline(always)] @@ -258,8 +398,36 @@ fn generate_service_methods( } }, Err(e) => { - // log error - log::error!("service:{}|method:{}|error:{}", self.service.descriptor(), #method_name_str, e); + if let Some(e_handler) = &self.error_handler { + let error_info = js_sys::Object::new(); + js_sys::Reflect::set( + &error_info, + &JsValue::from("service"), + &JsValue::from(self.service.descriptor()) + ).unwrap(); + js_sys::Reflect::set( + &error_info, + &JsValue::from("method"), + &JsValue::from(#method_name_str) + ).unwrap(); + js_sys::Reflect::set( + &error_info, + &JsValue::from("error"), + &JsValue::from("todo"/*TODO*/) + ).unwrap(); + if let Err(call_e) = e_handler.call1(&JsValue::UNDEFINED, &error_info) { + // log error + log::error!("service:{}|method:{}|error:{}|js error:{}", self.service.descriptor(), #method_name_str, e, + if let Some(s) = call_e.as_string() { + s + } else { + format!("{:?}", call_e) + }); + } + } else { + // log error + log::error!("service:{}|method:{}|error:{}", self.service.descriptor(), #method_name_str, e); + } None } } @@ -1088,11 +1256,12 @@ impl IServiceGenerator for WasmServiceGenerator { #service_types - /// WASM/JS-compatible wrapper of the Rust nRPC service + /// WASM/JS-compatible wrapper of a Rust nRPC service #[wasm_bindgen] pub struct #service_js_name { //#[wasm_bindgen(skip)] service: super::#service_struct_name<'static, WebSocketHandler>, + error_handler: Option, } #[wasm_bindgen] @@ -1106,9 +1275,15 @@ impl IServiceGenerator for WasmServiceGenerator { log::info!("Initialized ws service {} on port {}", #service_str_name, port); Self { service: implementation, + error_handler: None, } } + #[wasm_bindgen] + pub fn on_error(&mut self, handler: Option) { + self.error_handler = handler; + } + #service_methods } } diff --git a/usdpl-core/src/lib.rs b/usdpl-core/src/lib.rs index 18ddfb9..1152dd5 100644 --- a/usdpl-core/src/lib.rs +++ b/usdpl-core/src/lib.rs @@ -12,6 +12,7 @@ mod api_decky; /// This contains functionality used in both the back-end and front-end. pub mod api { #[cfg(not(any(feature = "decky")))] + #[allow(unused_imports)] pub use super::api_any::*; pub use super::api_common::*; #[cfg(all(feature = "decky", not(any(feature = "any"))))] diff --git a/usdpl-front/src/lib.rs b/usdpl-front/src/lib.rs index ebaf687..959b65c 100644 --- a/usdpl-front/src/lib.rs +++ b/usdpl-front/src/lib.rs @@ -32,8 +32,6 @@ const DEFAULT_LOGGER: console_logs::BuiltInLogger = console_logs::BuiltInLogger: static mut CACHE: Option> = None; -static mut TRANSLATIONS: Option>> = None; - #[cfg(feature = "encrypt")] fn encryption_key() -> Vec { hex::decode(obfstr::obfstr!(env!("USDPL_ENCRYPTION_KEY"))).unwrap() @@ -111,31 +109,3 @@ pub fn get_value(key: String) -> JsValue { .unwrap_or(JsValue::UNDEFINED) } } - -/// Translate a phrase, equivalent to tr_n(msg_id, 0) -#[wasm_bindgen] -pub fn tr(msg_id: String) -> String { - if let Some(translations) = unsafe { TRANSLATIONS.as_ref().unwrap().get(&msg_id) } { - if let Some(translated) = translations.get(0) { - translated.to_owned() - } else { - msg_id - } - } else { - msg_id - } -} - -/// Translate a phrase, retrieving the plural form for `n` items -#[wasm_bindgen] -pub fn tr_n(msg_id: String, n: usize) -> String { - if let Some(translations) = unsafe { TRANSLATIONS.as_ref().unwrap().get(&msg_id) } { - if let Some(translated) = translations.get(n) { - translated.to_owned() - } else { - msg_id - } - } else { - msg_id - } -}