Improve built-in services

This commit is contained in:
NGnius (Graham) 2024-04-07 11:42:37 -04:00
parent b7ed5d1e1c
commit ed5f96361b
9 changed files with 276 additions and 51 deletions

View file

@ -1,5 +1,5 @@
mod registry; mod registry;
pub use registry::{ServiceRegistry, StaticServiceRegistry}; pub use registry::{/*ServiceRegistry, */StaticServiceRegistry};
mod websocket_stream; mod websocket_stream;
pub use websocket_stream::ws_stream; pub use websocket_stream::ws_stream;

View file

@ -37,7 +37,7 @@ impl<'a> ServiceRegistry<'a> {
pub fn with_builtins() -> Self { pub fn with_builtins() -> Self {
let mut reg = Self::default(); let mut reg = Self::default();
reg.register(crate::services::usdpl::DevToolsServer::new(crate::services_impl::DevTools{})) 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 reg
} }
} }

View file

@ -17,6 +17,20 @@ impl<'a> generated::IDevTools<'a> for DevTools {
lvl if lvl == generated::LogLevel::Error as _ => log::error!("{}", input.msg), lvl if lvl == generated::LogLevel::Error as _ => log::error!("{}", input.msg),
lvl => return Err(Box::<dyn std::error::Error + Send + Sync>::from(format!("Unexpected input log level {}", lvl))) lvl => return Err(Box::<dyn std::error::Error + Send + Sync>::from(format!("Unexpected input log level {}", lvl)))
} }
Ok(generated::Empty{ ok: true }) Ok(generated::Empty{})
}
async fn echo(
&mut self,
input: generated::Ok,
) -> Result<generated::Ok, Box<dyn std::error::Error + Send>> {
Ok(input)
}
async fn version(
&mut self,
_: generated::Empty
) -> Result<generated::VersionString, Box<dyn std::error::Error + Send>> {
Ok(generated::VersionString { version: env!("CARGO_PKG_VERSION").to_owned() })
} }
} }

View file

@ -1,7 +1,30 @@
use crate::services::usdpl as generated; use crate::services::usdpl as generated;
/// Built-in translation service implementation /// Built-in translation service implementation
pub(crate) struct Translations {} pub(crate) struct Translations {
catalogs: async_lock::RwLock<std::collections::HashMap<String, gettext_ng::Catalog>>,
}
impl Translations {
pub fn new() -> Self {
Self {
catalogs: async_lock::RwLock::new(std::collections::HashMap::new()),
}
}
async fn get_catalog(&self, lang: &str) -> Result<gettext_ng::Catalog, gettext_ng::Error> {
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] #[async_trait::async_trait]
impl<'a> generated::ITranslations<'a> for Translations { impl<'a> generated::ITranslations<'a> for Translations {
@ -9,12 +32,12 @@ impl<'a> generated::ITranslations<'a> for Translations {
&mut self, &mut self,
input: generated::LanguageRequest, input: generated::LanguageRequest,
) -> Result<generated::TranslationsReply, Box<dyn std::error::Error + Send>> { ) -> Result<generated::TranslationsReply, Box<dyn std::error::Error + Send>> {
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 catalog_map = catalog.nalltext();
let mut map = std::collections::HashMap::with_capacity(catalog_map.len()); let mut map = std::collections::HashMap::with_capacity(catalog_map.len());
for (key, val) in catalog_map.into_iter() { for (key, val) in catalog_map.into_iter() {
if val.len() > 1 { 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) { if let Some(val_0) = val.get(0) {
map.insert(key.to_owned(), val_0.to_owned()); 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 }) Ok(generated::TranslationsReply { translations: map })
} }
async fn get_translation(
&mut self,
input: generated::TranslationRequest,
) -> Result<generated::TranslatedString, Box<dyn std::error::Error + Send>> {
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<gettext_ng::Catalog, gettext_ng::Error> { fn load_locale(lang: &str) -> Result<gettext_ng::Catalog, gettext_ng::Error> {

View file

@ -2,10 +2,16 @@ syntax = "proto3";
package usdpl; package usdpl;
// The translation service // The tools service
service DevTools { service DevTools {
// Retrieves all translations for the provided 4-letter code // Write a message to the back-end log
rpc Log (LogMessage) returns (Empty); 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 { enum LogLevel {
@ -22,6 +28,12 @@ message LogMessage {
string msg = 2; string msg = 2;
} }
message Empty { message Empty {}
message Ok {
bool ok = 1; bool ok = 1;
} }
message VersionString {
string version = 1;
}

View file

@ -5,7 +5,9 @@ package usdpl;
// The translation service // The translation service
service Translations { service Translations {
// Retrieves all translations for the provided 4-letter code // 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 // The request message containing the language code
@ -17,3 +19,17 @@ message LanguageRequest {
message TranslationsReply { message TranslationsReply {
map<string, string> translations = 1; map<string, string> 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;
}

View file

@ -93,8 +93,36 @@ fn generate_service_methods(
Some(x2.into_wasm()) Some(x2.into_wasm())
}, },
Err(e) => { Err(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
log::error!("service:{}|method:{}|error:{}", self.service.descriptor(), #method_name_str, e); log::error!("service:{}|method:{}|error:{}", self.service.descriptor(), #method_name_str, e);
}
None None
} }
} }
@ -116,8 +144,36 @@ fn generate_service_methods(
Some(x2.into_wasm()) Some(x2.into_wasm())
}, },
Err(e) => { Err(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
log::error!("service:{}|method:{}|error:{}", self.service.descriptor(), #method_name_str, e); log::error!("service:{}|method:{}|error:{}", self.service.descriptor(), #method_name_str, e);
}
None None
} }
} }
@ -179,7 +235,35 @@ fn generate_service_methods(
while let Some(next_result) = x.next().await { while let Some(next_result) = x.next().await {
match next_result { match next_result {
Err(e) => { Err(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); log::error!("service:{}|method:{}|error:{}", self.service.descriptor(), #method_name_str, e);
}
}, },
Ok(item) => { Ok(item) => {
#[inline(always)] #[inline(always)]
@ -208,11 +292,39 @@ fn generate_service_methods(
} }
}, },
Err(e) => { Err(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
log::error!("service:{}|method:{}|error:{}", self.service.descriptor(), #method_name_str, e); log::error!("service:{}|method:{}|error:{}", self.service.descriptor(), #method_name_str, e);
} }
} }
} }
}
}); });
}, },
(true, true) => { (true, true) => {
@ -229,7 +341,35 @@ fn generate_service_methods(
while let Some(next_result) = x.next().await { while let Some(next_result) = x.next().await {
match next_result { match next_result {
Err(e) => { Err(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); log::error!("service:{}|method:{}|error:{}", self.service.descriptor(), #method_name_str, e);
}
}, },
Ok(item) => { Ok(item) => {
#[inline(always)] #[inline(always)]
@ -258,8 +398,36 @@ fn generate_service_methods(
} }
}, },
Err(e) => { Err(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
log::error!("service:{}|method:{}|error:{}", self.service.descriptor(), #method_name_str, e); log::error!("service:{}|method:{}|error:{}", self.service.descriptor(), #method_name_str, e);
}
None None
} }
} }
@ -1088,11 +1256,12 @@ impl IServiceGenerator for WasmServiceGenerator {
#service_types #service_types
/// WASM/JS-compatible wrapper of the Rust nRPC service /// WASM/JS-compatible wrapper of a Rust nRPC service
#[wasm_bindgen] #[wasm_bindgen]
pub struct #service_js_name { pub struct #service_js_name {
//#[wasm_bindgen(skip)] //#[wasm_bindgen(skip)]
service: super::#service_struct_name<'static, WebSocketHandler>, service: super::#service_struct_name<'static, WebSocketHandler>,
error_handler: Option<js_sys::Function>,
} }
#[wasm_bindgen] #[wasm_bindgen]
@ -1106,9 +1275,15 @@ impl IServiceGenerator for WasmServiceGenerator {
log::info!("Initialized ws service {} on port {}", #service_str_name, port); log::info!("Initialized ws service {} on port {}", #service_str_name, port);
Self { Self {
service: implementation, service: implementation,
error_handler: None,
} }
} }
#[wasm_bindgen]
pub fn on_error(&mut self, handler: Option<js_sys::Function>) {
self.error_handler = handler;
}
#service_methods #service_methods
} }
} }

View file

@ -12,6 +12,7 @@ mod api_decky;
/// This contains functionality used in both the back-end and front-end. /// This contains functionality used in both the back-end and front-end.
pub mod api { pub mod api {
#[cfg(not(any(feature = "decky")))] #[cfg(not(any(feature = "decky")))]
#[allow(unused_imports)]
pub use super::api_any::*; pub use super::api_any::*;
pub use super::api_common::*; pub use super::api_common::*;
#[cfg(all(feature = "decky", not(any(feature = "any"))))] #[cfg(all(feature = "decky", not(any(feature = "any"))))]

View file

@ -32,8 +32,6 @@ const DEFAULT_LOGGER: console_logs::BuiltInLogger = console_logs::BuiltInLogger:
static mut CACHE: Option<std::collections::HashMap<String, JsValue>> = None; static mut CACHE: Option<std::collections::HashMap<String, JsValue>> = None;
static mut TRANSLATIONS: Option<std::collections::HashMap<String, Vec<String>>> = None;
#[cfg(feature = "encrypt")] #[cfg(feature = "encrypt")]
fn encryption_key() -> Vec<u8> { fn encryption_key() -> Vec<u8> {
hex::decode(obfstr::obfstr!(env!("USDPL_ENCRYPTION_KEY"))).unwrap() hex::decode(obfstr::obfstr!(env!("USDPL_ENCRYPTION_KEY"))).unwrap()
@ -111,31 +109,3 @@ pub fn get_value(key: String) -> JsValue {
.unwrap_or(JsValue::UNDEFINED) .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
}
}