use std::str::FromStr;
use js_sys::{Function, Reflect};
use serde::Serialize;
use serde_wasm_bindgen::{from_value, Serializer};
use wasm_bindgen::prelude::*;
use crate::cancellation::CancellationToken;
use crate::error::StoreError;
use crate::models::enums::{DCatEndpoint, DeviceFamily, IdentifierType};
use crate::models::locale::{Lang, LanguageTag, Locale, Market};
use crate::services::display_catalog::{DisplayCatalogHandler, ProgressEmitter, ProgressEvent};
use crate::services::fe3::FE3Handler;
use crate::utilities::helpers as h;
fn js_err<E: std::fmt::Display>(e: E) -> JsError {
JsError::new(&e.to_string())
}
fn to_js<T: Serialize + ?Sized>(value: &T) -> Result<JsValue, serde_wasm_bindgen::Error> {
let serializer = Serializer::new().serialize_large_number_types_as_bigints(true);
value.serialize(&serializer)
}
fn parse_enum<T: serde::de::DeserializeOwned>(label: &str, raw: &str) -> Result<T, JsError> {
serde_json::from_value::<T>(serde_json::Value::String(raw.to_owned()))
.map_err(|_| JsError::new(&format!("invalid {label}: {raw}")))
}
fn parse_endpoint(s: &str) -> Result<DCatEndpoint, JsError> {
parse_enum("endpoint", s)
}
fn parse_id_type(s: &str) -> Result<IdentifierType, JsError> {
IdentifierType::parse_tolerant(s)
.ok_or_else(|| JsError::new(&format!("unknown identifierType: {s}")))
}
fn parse_device_family(s: &str) -> Result<DeviceFamily, JsError> {
parse_enum("deviceFamily", s)
}
fn parse_market(s: &str) -> Result<Market, JsError> {
Market::from_str(s).map_err(|e| JsError::new(&e))
}
fn parse_lang(s: &str) -> Result<Lang, JsError> {
Lang::from_str(s).map_err(|e| JsError::new(&e))
}
fn store_err(e: StoreError) -> JsValue {
let kind = match &e {
StoreError::Http(_) => "http",
StoreError::Json(_) => "json",
StoreError::Xml(_) => "xml",
StoreError::NotFound => "notFound",
StoreError::TimedOut => "timedOut",
StoreError::Cancelled => "cancelled",
StoreError::Other(_) => "other",
};
let err = js_sys::Error::new(&e.to_string());
let _ = Reflect::set(&err, &JsValue::from_str("kind"), &JsValue::from_str(kind));
let causes_arr = js_sys::Array::new();
for c in e.causes().into_iter().skip(1) {
causes_arr.push(&JsValue::from_str(&c));
}
let _ = Reflect::set(&err, &JsValue::from_str("causes"), &causes_arr);
err.into()
}
fn install_js_progress(callback: JsValue, emitter: &mut ProgressEmitter) {
if callback.is_null() || callback.is_undefined() {
emitter.clear();
return;
}
let Ok(func): Result<Function, _> = callback.dyn_into() else {
emitter.clear();
return;
};
emitter.set(Box::new(move |event: ProgressEvent| {
let val = to_js(&event).unwrap_or(JsValue::NULL);
let _ = func.call1(&JsValue::NULL, &val);
}));
}
struct AbortAdapter {
token: CancellationToken,
_closure: Option<Closure<dyn FnMut(JsValue)>>,
}
impl AbortAdapter {
fn from_signal(signal: &JsValue) -> Result<Self, JsError> {
let token = CancellationToken::new();
let already_aborted = Reflect::get(signal, &JsValue::from_str("aborted"))
.map(|v| v.as_bool().unwrap_or(false))
.unwrap_or(false);
if already_aborted {
token.cancel();
return Ok(AbortAdapter {
token,
_closure: None,
});
}
let token_for_closure = token.clone();
let closure = Closure::wrap(Box::new(move |_: JsValue| {
token_for_closure.cancel();
}) as Box<dyn FnMut(JsValue)>);
let add: Function = Reflect::get(signal, &JsValue::from_str("addEventListener"))
.map_err(|_| JsError::new("signal.addEventListener not callable"))?
.dyn_into()
.map_err(|_| JsError::new("signal.addEventListener not a function"))?;
add.call2(
signal,
&JsValue::from_str("abort"),
closure.as_ref().unchecked_ref(),
)
.map_err(|_| JsError::new("failed to attach abort listener"))?;
Ok(AbortAdapter {
token,
_closure: Some(closure),
})
}
}
fn adapt_signal(signal: &Option<JsValue>) -> Result<Option<AbortAdapter>, JsValue> {
match signal {
Some(sig) if !sig.is_null() && !sig.is_undefined() => {
let adapter = AbortAdapter::from_signal(sig)
.map_err(|e| store_err(StoreError::Other(format!("invalid AbortSignal: {e:?}"))))?;
Ok(Some(adapter))
}
_ => Ok(None),
}
}
#[wasm_bindgen(start)]
pub fn wasm_init() {
console_error_panic_hook::set_once();
}
#[wasm_bindgen(typescript_custom_section)]
const TS_APPEND_CONTENT: &'static str = include_str!(concat!(env!("OUT_DIR"), "/wasm_types.d.ts"));
#[wasm_bindgen(js_name = stringToPackageType)]
pub fn string_to_package_type_js(raw: &str) -> Result<JsValue, JsError> {
to_js(&h::string_to_package_type(raw)).map_err(js_err)
}
#[wasm_bindgen(js_name = endpointToBaseUrl)]
pub fn endpoint_to_base_url_js(endpoint: &str) -> Result<String, JsError> {
let e = parse_endpoint(endpoint)?;
Ok(h::endpoint_to_base_url(&e).to_string())
}
#[wasm_bindgen(js_name = endpointToSearchUrl)]
pub fn endpoint_to_search_url_js(endpoint: &str) -> Result<String, JsError> {
let e = parse_endpoint(endpoint)?;
Ok(h::endpoint_to_search_url(&e).to_string())
}
#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct CodeEntry {
code: &'static str,
english_name: &'static str,
}
#[wasm_bindgen(js_name = listMarkets, unchecked_return_type = "CodeEntry[]")]
pub fn list_markets_js() -> Result<JsValue, JsError> {
let entries: Vec<CodeEntry> = Market::all()
.iter()
.map(|m| CodeEntry {
code: m.as_str(),
english_name: m.english_name(),
})
.collect();
to_js(&entries).map_err(js_err)
}
#[wasm_bindgen(js_name = listLanguages, unchecked_return_type = "CodeEntry[]")]
pub fn list_languages_js() -> Result<JsValue, JsError> {
let entries: Vec<CodeEntry> = Lang::all()
.iter()
.map(|l| CodeEntry {
code: l.as_str(),
english_name: l.english_name(),
})
.collect();
to_js(&entries).map_err(js_err)
}
#[wasm_bindgen(js_name = listLanguageTags, unchecked_return_type = "CodeEntry[]")]
pub fn list_language_tags_js() -> Result<JsValue, JsError> {
let entries: Vec<CodeEntry> = LanguageTag::all()
.iter()
.map(|t| CodeEntry {
code: t.as_str(),
english_name: t.english_name(),
})
.collect();
to_js(&entries).map_err(js_err)
}
#[wasm_bindgen(js_name = parseMarket, unchecked_return_type = "CodeEntry")]
pub fn parse_market_js(code: &str) -> Result<JsValue, JsError> {
let m = parse_market(code)?;
to_js(&CodeEntry {
code: m.as_str(),
english_name: m.english_name(),
})
.map_err(js_err)
}
#[wasm_bindgen(js_name = parseLanguage, unchecked_return_type = "CodeEntry")]
pub fn parse_language_js(code: &str) -> Result<JsValue, JsError> {
let l = parse_lang(code)?;
to_js(&CodeEntry {
code: l.as_str(),
english_name: l.english_name(),
})
.map_err(js_err)
}
#[wasm_bindgen(js_name = parseLanguageTag, unchecked_return_type = "CodeEntry")]
pub fn parse_language_tag_js(tag: &str) -> Result<JsValue, JsError> {
let t = LanguageTag::from_str(tag).map_err(|e| JsError::new(&e))?;
to_js(&CodeEntry {
code: t.as_str(),
english_name: t.english_name(),
})
.map_err(js_err)
}
#[wasm_bindgen(js_name = parseIdentifierType, unchecked_return_type = "IdentifierType")]
pub fn parse_identifier_type_js(raw: &str) -> Result<String, JsError> {
let it = IdentifierType::parse_tolerant(raw)
.ok_or_else(|| JsError::new(&format!("unknown identifierType: {raw}")))?;
Ok(it.as_str().to_owned())
}
#[wasm_bindgen(js_name = createDcatUri)]
pub fn create_dcat_uri_js(
endpoint: &str,
id: &str,
id_type: &str,
locale: &LocaleJs,
) -> Result<String, JsError> {
let e = parse_endpoint(endpoint)?;
let t = parse_id_type(id_type)?;
Ok(h::create_dcat_uri(&e, id, &t, &locale.inner))
}
#[wasm_bindgen(js_name = Locale)]
pub struct LocaleJs {
inner: Locale,
}
#[wasm_bindgen(js_class = Locale)]
impl LocaleJs {
#[wasm_bindgen(constructor)]
pub fn new(
#[wasm_bindgen(unchecked_param_type = "Market")] market: &str,
#[wasm_bindgen(unchecked_param_type = "Lang")] language: &str,
include_neutral: bool,
) -> Result<LocaleJs, JsError> {
Ok(LocaleJs {
inner: Locale::new(
parse_market(market)?,
parse_lang(language)?,
include_neutral,
),
})
}
#[wasm_bindgen(js_name = production)]
pub fn production() -> LocaleJs {
LocaleJs {
inner: Locale::production(),
}
}
#[wasm_bindgen(js_name = withFullTag)]
pub fn with_full_tag(&self, enabled: bool) -> LocaleJs {
LocaleJs {
inner: self.inner.clone().with_full_tag(enabled),
}
}
#[wasm_bindgen(getter, js_name = useFullTag)]
pub fn use_full_tag(&self) -> bool {
self.inner.use_full_tag
}
#[wasm_bindgen(js_name = fromTag)]
pub fn from_tag(
#[wasm_bindgen(unchecked_param_type = "LanguageTag")] tag: &str,
include_neutral: bool,
) -> Result<LocaleJs, JsError> {
let parsed = LanguageTag::from_str(tag).map_err(|e| JsError::new(&e))?;
let inner = Locale::from_tag(parsed, include_neutral).map_err(JsError::new)?;
Ok(LocaleJs { inner })
}
#[wasm_bindgen(getter)]
pub fn market(&self) -> String {
self.inner.market.as_str().to_owned()
}
#[wasm_bindgen(getter)]
pub fn language(&self) -> String {
self.inner.language.as_str().to_owned()
}
#[wasm_bindgen(getter, js_name = includeNeutral)]
pub fn include_neutral(&self) -> bool {
self.inner.include_neutral
}
#[wasm_bindgen(js_name = dcatTrail)]
pub fn dcat_trail(&self) -> String {
self.inner.dcat_trail()
}
#[wasm_bindgen(js_name = toJSON, unchecked_return_type = "LocaleJson")]
pub fn to_json(&self) -> Result<JsValue, JsError> {
to_js(&self.inner).map_err(js_err)
}
}
#[wasm_bindgen(js_name = DisplayCatalogHandler)]
pub struct DisplayCatalogHandlerJs {
inner: DisplayCatalogHandler,
}
#[wasm_bindgen(js_class = DisplayCatalogHandler)]
impl DisplayCatalogHandlerJs {
#[wasm_bindgen(constructor)]
pub fn new(endpoint: &str, locale: &LocaleJs) -> Result<DisplayCatalogHandlerJs, JsError> {
let e = parse_endpoint(endpoint)?;
Ok(DisplayCatalogHandlerJs {
inner: DisplayCatalogHandler::new(e, locale.inner.clone()),
})
}
#[wasm_bindgen(js_name = production)]
pub fn production() -> DisplayCatalogHandlerJs {
DisplayCatalogHandlerJs {
inner: DisplayCatalogHandler::production(),
}
}
#[wasm_bindgen(js_name = onProgress)]
pub fn on_progress(
&mut self,
#[wasm_bindgen(unchecked_param_type = "OnProgress | null")] callback: JsValue,
) {
install_js_progress(callback, &mut self.inner.progress);
}
#[wasm_bindgen(
js_name = queryDcat,
unchecked_return_type = "DisplayCatalogModel | null"
)]
pub async fn query_dcat(
&mut self,
id: String,
#[wasm_bindgen(unchecked_param_type = "IdentifierType | string")] id_type: String,
auth_token: Option<String>,
#[wasm_bindgen(unchecked_param_type = "AbortSignal | null")] signal: Option<JsValue>,
) -> Result<JsValue, JsValue> {
let t = parse_id_type(&id_type)?;
let adapter = adapt_signal(&signal)?;
let cancel = adapter.as_ref().map(|a| &a.token);
self.inner
.query_dcat_with_cancel(&id, t, auth_token.as_deref(), cancel)
.await
.map_err(store_err)?;
Ok(to_js(&self.inner.product_listing).map_err(js_err)?)
}
#[wasm_bindgen(
js_name = getPackagesForProduct,
unchecked_return_type = "PackageInstance[]"
)]
pub async fn get_packages_for_product(
&self,
msa_token: Option<String>,
#[wasm_bindgen(unchecked_param_type = "AbortSignal | null")] signal: Option<JsValue>,
) -> Result<JsValue, JsValue> {
let adapter = adapt_signal(&signal)?;
let cancel = adapter.as_ref().map(|a| &a.token);
let packages = self
.inner
.get_packages_for_product_with_cancel(msa_token.as_deref(), cancel)
.await
.map_err(store_err)?;
Ok(to_js(&packages).map_err(js_err)?)
}
#[wasm_bindgen(
js_name = searchDcat,
unchecked_return_type = "DCatSearch"
)]
pub async fn search_dcat(
&mut self,
query: String,
#[wasm_bindgen(unchecked_param_type = "DeviceFamily | string")] device_family: String,
#[wasm_bindgen(unchecked_param_type = "AbortSignal | null")] signal: Option<JsValue>,
) -> Result<JsValue, JsValue> {
let df = parse_device_family(&device_family)?;
let adapter = adapt_signal(&signal)?;
let cancel = adapter.as_ref().map(|a| &a.token);
let result = self
.inner
.search_dcat_with_cancel(&query, df, cancel)
.await
.map_err(store_err)?;
Ok(to_js(&result).map_err(js_err)?)
}
#[wasm_bindgen(
js_name = searchDcatPaged,
unchecked_return_type = "DCatSearch"
)]
pub async fn search_dcat_paged(
&mut self,
query: String,
#[wasm_bindgen(unchecked_param_type = "DeviceFamily | string")] device_family: String,
skip_count: u32,
#[wasm_bindgen(unchecked_param_type = "AbortSignal | null")] signal: Option<JsValue>,
) -> Result<JsValue, JsValue> {
let df = parse_device_family(&device_family)?;
let adapter = adapt_signal(&signal)?;
let cancel = adapter.as_ref().map(|a| &a.token);
let result = self
.inner
.search_dcat_paged_with_cancel(&query, df, skip_count, cancel)
.await
.map_err(store_err)?;
Ok(to_js(&result).map_err(js_err)?)
}
#[wasm_bindgen(getter, js_name = isFound)]
pub fn is_found(&self) -> bool {
self.inner.is_found
}
#[wasm_bindgen(getter, js_name = productListing, unchecked_return_type = "DisplayCatalogModel | null")]
pub fn product_listing(&self) -> Result<JsValue, JsError> {
to_js(&self.inner.product_listing).map_err(js_err)
}
#[wasm_bindgen(getter, js_name = searchResult, unchecked_return_type = "DCatSearch | null")]
pub fn search_result(&self) -> Result<JsValue, JsError> {
to_js(&self.inner.search_result).map_err(js_err)
}
#[wasm_bindgen(getter, js_name = selectedEndpoint, unchecked_return_type = "DCatEndpoint")]
pub fn selected_endpoint(&self) -> Result<JsValue, JsError> {
to_js(&self.inner.selected_endpoint).map_err(js_err)
}
#[wasm_bindgen(getter, js_name = selectedLocale, unchecked_return_type = "LocaleJson")]
pub fn selected_locale(&self) -> Result<JsValue, JsError> {
to_js(&self.inner.selected_locale).map_err(js_err)
}
#[wasm_bindgen(getter)]
pub fn result(&self) -> Result<JsValue, JsError> {
to_js(&self.inner.result).map_err(js_err)
}
#[wasm_bindgen(getter)]
pub fn id(&self) -> Option<String> {
self.inner.id.clone()
}
#[wasm_bindgen(getter)]
pub fn error(&self) -> Option<String> {
self.inner.error.clone()
}
#[wasm_bindgen(getter)]
pub fn title(&self) -> Option<String> {
self.inner.title().map(str::to_owned)
}
#[wasm_bindgen(getter)]
pub fn description(&self) -> Option<String> {
self.inner.description().map(str::to_owned)
}
#[wasm_bindgen(getter, js_name = publisherName)]
pub fn publisher_name(&self) -> Option<String> {
self.inner.publisher_name().map(str::to_owned)
}
#[wasm_bindgen(getter, js_name = wuCategoryId)]
pub fn wu_category_id(&self) -> Option<String> {
self.inner.wu_category_id().map(str::to_owned)
}
#[wasm_bindgen(getter, js_name = lastModifiedDate)]
pub fn last_modified_date(&self) -> Option<String> {
self.inner.last_modified_date().map(str::to_owned)
}
#[wasm_bindgen(getter, unchecked_return_type = "Price | null")]
pub fn price(&self) -> Result<JsValue, JsError> {
to_js(&self.inner.price()).map_err(js_err)
}
#[wasm_bindgen(getter, unchecked_return_type = "Price[]")]
pub fn prices(&self) -> Result<JsValue, JsError> {
to_js(&self.inner.prices()).map_err(js_err)
}
#[wasm_bindgen(getter, unchecked_return_type = "Package[]")]
pub fn packages(&self) -> Result<JsValue, JsError> {
to_js(self.inner.packages()).map_err(js_err)
}
#[wasm_bindgen(getter, unchecked_return_type = "Availability[]")]
pub fn availabilities(&self) -> Result<JsValue, JsError> {
to_js(&self.inner.availabilities()).map_err(js_err)
}
#[wasm_bindgen(getter, unchecked_return_type = "Product[]")]
pub fn products(&self) -> Result<JsValue, JsError> {
to_js(self.inner.products()).map_err(js_err)
}
#[wasm_bindgen(js_name = imagesWithPurpose, unchecked_return_type = "Image[]")]
pub fn images_with_purpose(&self, purpose: &str) -> Result<JsValue, JsError> {
to_js(&self.inner.images_with_purpose(purpose)).map_err(js_err)
}
#[wasm_bindgen(
js_name = queryDcatBatch,
unchecked_return_type = "Product[]"
)]
pub async fn query_dcat_batch(
&mut self,
ids: Vec<String>,
auth_token: Option<String>,
#[wasm_bindgen(unchecked_param_type = "AbortSignal | null")] signal: Option<JsValue>,
) -> Result<JsValue, JsValue> {
let adapter = adapt_signal(&signal)?;
let cancel = adapter.as_ref().map(|a| &a.token);
let id_refs: Vec<&str> = ids.iter().map(String::as_str).collect();
self.inner
.query_dcat_batch_with_cancel(&id_refs, auth_token.as_deref(), cancel)
.await
.map_err(store_err)?;
Ok(to_js(self.inner.products()).map_err(js_err)?)
}
}
#[wasm_bindgen(js_name = Fe3Handler)]
pub struct Fe3HandlerJs {
inner: FE3Handler,
}
#[wasm_bindgen(js_class = Fe3Handler)]
impl Fe3HandlerJs {
#[wasm_bindgen(constructor)]
pub fn new() -> Fe3HandlerJs {
let client = reqwest::Client::builder()
.user_agent("StoreLib")
.build()
.unwrap_or_default();
Fe3HandlerJs {
inner: FE3Handler::new(client),
}
}
#[wasm_bindgen(js_name = onProgress)]
pub fn on_progress(
&mut self,
#[wasm_bindgen(unchecked_param_type = "OnProgress | null")] callback: JsValue,
) {
install_js_progress(callback, &mut self.inner.progress);
}
#[wasm_bindgen(js_name = getCookie)]
pub async fn get_cookie(&self) -> Result<String, JsValue> {
FE3Handler::get_cookie(&self.inner.client)
.await
.map_err(store_err)
}
#[wasm_bindgen(js_name = syncUpdates)]
pub async fn sync_updates(
&self,
wu_category_id: String,
msa_token: Option<String>,
) -> Result<String, JsValue> {
FE3Handler::sync_updates(&wu_category_id, msa_token.as_deref(), &self.inner.client)
.await
.map_err(store_err)
}
#[wasm_bindgen(js_name = processUpdateIds)]
pub fn process_update_ids(xml: &str) -> Result<JsValue, JsValue> {
let (update_ids, revision_ids) = FE3Handler::process_update_ids(xml).map_err(store_err)?;
#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct Ids {
update_ids: Vec<String>,
revision_ids: Vec<String>,
}
Ok(to_js(&Ids {
update_ids,
revision_ids,
})
.map_err(js_err)?)
}
#[wasm_bindgen(js_name = getPackageInstances)]
pub async fn get_package_instances(xml: String) -> Result<JsValue, JsValue> {
let instances = FE3Handler::get_package_instances(&xml)
.await
.map_err(store_err)?;
Ok(to_js(&instances).map_err(js_err)?)
}
#[wasm_bindgen(js_name = getFileUrls)]
pub async fn get_file_urls(
&self,
update_ids: JsValue,
revision_ids: JsValue,
msa_token: Option<String>,
) -> Result<JsValue, JsValue> {
let update_ids: Vec<String> = from_value(update_ids).map_err(js_err)?;
let revision_ids: Vec<String> = from_value(revision_ids).map_err(js_err)?;
let pairs = self
.inner
.get_file_urls(&update_ids, &revision_ids, msa_token.as_deref())
.await
.map_err(store_err)?;
#[derive(serde::Serialize)]
struct UrlEntry {
url: String,
size: Option<i64>,
}
let mapped: Vec<UrlEntry> = pairs
.into_iter()
.map(|(url, size)| UrlEntry { url, size })
.collect();
Ok(to_js(&mapped).map_err(js_err)?)
}
}
impl Default for Fe3HandlerJs {
fn default() -> Self {
Self::new()
}
}