use wasm_bindgen::prelude::*;
#[wasm_bindgen(typescript_custom_section)]
const TYPED_API: &'static str = include_str!("wasm_types.d.ts");
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = JSON, js_name = parse)]
fn json_parse(text: &str) -> JsValue;
}
#[wasm_bindgen(unchecked_return_type = "RunOutput")]
pub fn run_typed(toml: &str) -> Result<JsValue, JsValue> {
let o = crate::api::run_toml(toml).map_err(|e| JsValue::from_str(&e))?;
let quoted = |s: &str| serde_json::Value::from(s).to_string();
Ok(json_parse(&format!(
"{{\"json\":{},\"svg\":{},\"summary\":{},\"csv\":{},\"data\":{}}}",
quoted(&o.json),
quoted(&o.svg),
quoted(&o.summary),
o.csv.as_deref().map_or_else(|| "null".to_string(), quoted),
o.json,
)))
}
#[wasm_bindgen(unchecked_return_type = "KindMeta[]")]
pub fn scenario_kinds() -> JsValue {
json_parse(&crate::api::list_scenario_kinds_json())
}
#[wasm_bindgen]
pub fn run(toml: &str) -> Result<String, JsValue> {
crate::api::run_toml(toml)
.map(|o| o.json)
.map_err(|e| JsValue::from_str(&e))
}
#[wasm_bindgen]
pub fn chart_svg(toml: &str) -> Result<String, JsValue> {
crate::api::run_toml(toml)
.map(|o| o.svg)
.map_err(|e| JsValue::from_str(&e))
}
#[wasm_bindgen]
pub fn summary(toml: &str) -> Result<String, JsValue> {
crate::api::run_toml(toml)
.map(|o| o.summary)
.map_err(|e| JsValue::from_str(&e))
}
#[wasm_bindgen]
pub fn table_csv(toml: &str) -> Result<Option<String>, JsValue> {
crate::api::run_toml(toml)
.map(|o| o.csv)
.map_err(|e| JsValue::from_str(&e))
}
#[wasm_bindgen]
pub fn run_all(toml: &str) -> Result<String, JsValue> {
let o = crate::api::run_toml(toml).map_err(|e| JsValue::from_str(&e))?;
Ok(serde_json::json!({
"json": o.json,
"svg": o.svg,
"summary": o.summary,
"csv": o.csv,
})
.to_string())
}
#[wasm_bindgen]
pub fn list_kinds() -> String {
crate::api::list_scenario_kinds_json()
}
#[wasm_bindgen]
pub fn kind_modules() -> String {
crate::api::kind_modules_json()
}
#[wasm_bindgen]
pub fn field_importance() -> String {
crate::api::field_importance_json()
}
#[wasm_bindgen(unchecked_return_type = "ErrorKind | \"\"")]
pub fn error_kind(toml: &str) -> String {
crate::api::run_scenario(toml)
.err()
.map(|e| e.kind_tag().to_string())
.unwrap_or_default()
}
#[wasm_bindgen]
pub fn version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
#[wasm_bindgen]
pub fn encode_permalink(toml: &str) -> String {
crate::permalink::encode_scenario(toml)
}
#[wasm_bindgen]
pub fn decode_permalink(token: &str) -> String {
crate::permalink::decode_scenario(token).unwrap_or_default()
}
#[wasm_bindgen]
pub fn export_sp3(toml: &str) -> Result<String, JsValue> {
crate::api::export_sp3(toml).map_err(|e| JsValue::from_str(&e))
}
#[wasm_bindgen]
pub fn export_omm(toml: &str) -> Result<String, JsValue> {
crate::api::export_omm(toml).map_err(|e| JsValue::from_str(&e))
}
#[wasm_bindgen]
pub fn export_oem(toml: &str) -> Result<String, JsValue> {
crate::api::export_oem(toml).map_err(|e| JsValue::from_str(&e))
}
#[wasm_bindgen]
pub fn receiver_trust(toml: &str) -> Result<String, JsValue> {
let o = crate::receiver_trust::scenario::run_toml(toml).map_err(|e| JsValue::from_str(&e))?;
Ok(serde_json::json!({
"json": o.json,
"csv": o.csv,
"svg": o.svg,
"summary": o.summary,
})
.to_string())
}
#[wasm_bindgen]
pub fn export_formats(toml: &str) -> Result<String, JsValue> {
crate::surface::export_formats(toml, crate::surface::MAX_INPUT_BYTES)
.map(|v| v.to_string())
.map_err(|e| JsValue::from_str(&e))
}
#[wasm_bindgen]
pub fn export_scenario(toml: &str, format: &str) -> Result<String, JsValue> {
crate::surface::export_scenario(toml, format, crate::surface::MAX_INPUT_BYTES)
.map(|e| e.to_json().to_string())
.map_err(|e| JsValue::from_str(&e))
}
#[wasm_bindgen]
pub fn import_route(toml: &str, geojson: &str) -> Result<String, JsValue> {
crate::surface::import_route(toml, geojson, crate::surface::MAX_INPUT_BYTES)
.map_err(|e| JsValue::from_str(&e))
}
#[wasm_bindgen]
pub fn animate_scenario(
toml: &str,
format: &str,
fps: u32,
duration_s: f64,
width: u32,
) -> Result<String, JsValue> {
let a = crate::surface::animate_scenario(
toml,
(!format.is_empty()).then_some(format),
(fps != 0).then_some(fps),
(!duration_s.is_nan()).then_some(duration_s),
(width != 0).then_some(width),
crate::surface::MAX_INPUT_BYTES,
)
.map_err(|e| JsValue::from_str(&e))?;
let files: serde_json::Map<String, serde_json::Value> =
a.files.into_iter().map(|(k, v)| (k, v.into())).collect();
Ok(serde_json::json!({"summary": a.summary, "files": files}).to_string())
}
#[wasm_bindgen]
pub fn list_examples(kind: &str) -> Result<String, JsValue> {
crate::surface::list_examples((!kind.is_empty()).then_some(kind))
.map(|v| v.to_string())
.map_err(|e| JsValue::from_str(&e))
}
#[wasm_bindgen]
pub fn get_example(name: &str) -> Result<String, JsValue> {
crate::surface::get_example(name)
.map(str::to_string)
.map_err(|e| JsValue::from_str(&e))
}
#[wasm_bindgen]
pub fn evidence_attach_timestamp(
files_json: &str,
token_base64: &str,
replace: bool,
) -> Result<String, JsValue> {
let v: serde_json::Value = serde_json::from_str(files_json)
.map_err(|e| JsValue::from_str(&format!("files_json is not JSON: {e}")))?;
let files = crate::surface::files_from_json(&v).map_err(|e| JsValue::from_str(&e))?;
let token = crate::permalink::base64_decode(token_base64.trim())
.ok_or_else(|| JsValue::from_str("token_base64 is not base64"))?;
let t = crate::surface::evidence_attach_timestamp(
&files,
&token,
replace,
crate::surface::MAX_INPUT_BYTES,
)
.map_err(|e| JsValue::from_str(&e))?;
Ok(
serde_json::json!({"files": crate::surface::files_to_json(&t.files), "notes": t.notes})
.to_string(),
)
}
#[wasm_bindgen]
pub fn bench_export(toml: &str, epoch: &str) -> Result<String, JsValue> {
use crate::surface::{BENCH_NOTICE, MAX_INPUT_BYTES};
let e =
crate::surface::bench_export(toml, (!epoch.is_empty()).then_some(epoch), MAX_INPUT_BYTES)
.map_err(|e| JsValue::from_str(&e))?;
let files: serde_json::Map<String, serde_json::Value> =
e.files.into_iter().map(|(k, v)| (k, v.into())).collect();
Ok(serde_json::json!({"files": files, "notes": e.notes, "notice": BENCH_NOTICE}).to_string())
}
#[wasm_bindgen]
pub fn compliance_report(runs_json: &str) -> Result<String, JsValue> {
use crate::surface::{ComplianceRunText, MAX_INPUT_BYTES};
let v: serde_json::Value = serde_json::from_str(runs_json)
.map_err(|e| JsValue::from_str(&format!("runs_json is not JSON: {e}")))?;
let arr = v.as_array().ok_or_else(|| {
JsValue::from_str("runs_json must be an array of {label, result, scenario?}")
})?;
let mut texts = Vec::with_capacity(arr.len());
for (i, r) in arr.iter().enumerate() {
let field = |k: &str| r.get(k).and_then(|x| x.as_str()).map(str::to_string);
texts.push(ComplianceRunText {
label: field("label")
.ok_or_else(|| JsValue::from_str(&format!("runs[{i}] needs `label`")))?,
result_json: field("result")
.ok_or_else(|| JsValue::from_str(&format!("runs[{i}] needs `result`")))?,
scenario_toml: field("scenario"),
});
}
let out = crate::surface::compliance_report(&texts, MAX_INPUT_BYTES)
.map_err(|e| JsValue::from_str(&e))?;
Ok(serde_json::json!({"report": out.report, "markdown": out.markdown}).to_string())
}
#[wasm_bindgen]
pub fn compliance_mapping(sources: bool) -> String {
crate::surface::compliance_mapping(sources)
}
#[wasm_bindgen]
#[allow(clippy::too_many_arguments)]
pub fn interference_map(
source: &str,
csv: &str,
dataset: &str,
cell_deg: f64,
licence: &str,
licence_url: &str,
attribution: &str,
land_geojson: &str,
) -> Result<String, JsValue> {
use crate::surface::{interference_map as build, CustomDataset, MapSource, MAX_INPUT_BYTES};
let custom =
(!licence.is_empty() || !licence_url.is_empty() || !attribution.is_empty()).then(|| {
CustomDataset {
licence: licence.into(),
licence_url: licence_url.into(),
attribution: attribution.into(),
}
});
let days = build(
MapSource::parse(source).map_err(|e| JsValue::from_str(&e))?,
csv,
dataset,
(!cell_deg.is_nan()).then_some(cell_deg),
custom.as_ref(),
(!land_geojson.is_empty()).then_some(land_geojson),
MAX_INPUT_BYTES,
)
.map_err(|e| JsValue::from_str(&e))?;
Ok(serde_json::Value::Array(
days.into_iter()
.map(|d| {
serde_json::json!({
"file_name": d.file_name,
"date": d.date,
"cells_published": d.cells_published,
"cells_flagged": d.cells_flagged,
"geojson": d.geojson,
})
})
.collect(),
)
.to_string())
}
#[wasm_bindgen]
pub fn route_exposure(
route: &str,
maps_json: &str,
date_from: &str,
date_to: &str,
) -> Result<String, JsValue> {
let maps: Vec<String> = serde_json::from_str(maps_json).map_err(|e| {
JsValue::from_str(&format!("maps_json must be a JSON array of strings: {e}"))
})?;
let refs: Vec<&str> = maps.iter().map(String::as_str).collect();
crate::surface::route_exposure(
route,
&refs,
(!date_from.is_empty()).then_some(date_from),
(!date_to.is_empty()).then_some(date_to),
crate::surface::MAX_INPUT_BYTES,
)
.map_err(|e| JsValue::from_str(&e))
}
#[wasm_bindgen]
pub fn nmea_training(toml: &str, seed: f64) -> Result<String, JsValue> {
let seed = (seed.is_finite() && seed >= 0.0).then_some(seed as u64);
let t = crate::surface::nmea_training(toml, seed, crate::surface::MAX_INPUT_BYTES)
.map_err(|e| JsValue::from_str(&e))?;
Ok(
serde_json::json!({"nmea": t.nmea, "log_json": t.log_json, "log_text": t.log_text})
.to_string(),
)
}
#[wasm_bindgen]
pub fn receiver_trust_replay(session_toml: &str, nmea: &str) -> Result<String, JsValue> {
let r = crate::surface::assess_vessel_excerpt(
session_toml,
nmea.as_bytes(),
crate::surface::MAX_INPUT_BYTES,
)
.map_err(|e| JsValue::from_str(&e))?;
serde_json::to_string(&r).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen]
pub fn assess_vessel_log(session_toml: &str, log: &str) -> Result<String, JsValue> {
crate::surface::assess_vessel_log_json(
session_toml,
log.as_bytes(),
crate::surface::MAX_INPUT_BYTES,
)
.map_err(|e| JsValue::from_str(&e))
}
#[wasm_bindgen]
pub fn evidence_verify(
files_json: &str,
public_key: &str,
full_log: &str,
require_timestamp: bool,
) -> Result<String, JsValue> {
let v: serde_json::Value = serde_json::from_str(files_json)
.map_err(|e| JsValue::from_str(&format!("files_json is not JSON: {e}")))?;
let files = crate::surface::files_from_json(&v).map_err(|e| JsValue::from_str(&e))?;
let report = crate::surface::evidence_verify(
&files,
(!public_key.is_empty()).then_some(public_key),
(!full_log.is_empty()).then_some(full_log.as_bytes()),
require_timestamp,
)
.map_err(|e| JsValue::from_str(&e))?;
Ok(report.to_string())
}
#[wasm_bindgen]
pub fn trust_telemetry(
input: &str,
format: &str,
host: &str,
input_kind: &str,
options_json: &str,
) -> Result<String, JsValue> {
use crate::surface::{self, TelemetryFormat, TelemetryInput, TelemetryOptions};
let err = |e: String| JsValue::from_str(&e);
let mut opts = TelemetryOptions::default();
if !options_json.trim().is_empty() {
let v: serde_json::Value = serde_json::from_str(options_json)
.map_err(|e| err(format!("options_json is not JSON: {e}")))?;
opts.expose_position = v["expose_position"].as_bool().unwrap_or(false);
opts.syslog_all = v["syslog_all"].as_bool().unwrap_or(false);
opts.unix_nano = v["unix_nano"].as_f64().unwrap_or(0.0) as u64;
}
surface::trust_telemetry(
input,
TelemetryFormat::parse(format).map_err(err)?,
host,
TelemetryInput::parse(input_kind).map_err(err)?,
opts,
surface::MAX_INPUT_BYTES,
)
.map_err(err)
}
#[wasm_bindgen]
pub fn osnma_verify(
input_base64: &str,
format: &str,
start_gst: Option<u32>,
merkle_root_hex: &str,
public_keys: Vec<String>,
reference_time: Option<u32>,
max_time_error: Option<u32>,
) -> Result<String, JsValue> {
let b64 = input_base64.trim();
if b64.len() / 4 * 3 > crate::surface::MAX_INPUT_BYTES {
return Err(JsValue::from_str(&format!(
"input is about {} bytes, over the {}-byte limit",
b64.len() / 4 * 3,
crate::surface::MAX_INPUT_BYTES
)));
}
let bytes = crate::permalink::base64_decode(b64)
.ok_or_else(|| JsValue::from_str("input_base64 is not base64"))?;
crate::surface::osnma_verify(
&bytes,
format,
start_gst,
merkle_root_hex,
&public_keys,
reference_time,
max_time_error,
crate::surface::MAX_INPUT_BYTES,
)
.map_err(|e| JsValue::from_str(&e))
}
#[wasm_bindgen]
pub fn sky_geometry(request_json: &str) -> Result<String, JsValue> {
crate::surface::sky_geometry(request_json, crate::surface::MAX_INPUT_BYTES)
.map_err(|e| JsValue::from_str(&e))
}
#[wasm_bindgen]
pub fn sky_monte_carlo(request_json: &str, seed: f64, n: u32) -> Result<String, JsValue> {
if !(seed.is_finite() && seed >= 0.0 && seed.fract() == 0.0 && seed <= 9_007_199_254_740_991.0)
{
return Err(JsValue::from_str("seed must be a non-negative integer"));
}
crate::surface::sky_monte_carlo(
request_json,
seed as u64,
n as usize,
crate::surface::MAX_INPUT_BYTES,
)
.map_err(|e| JsValue::from_str(&e))
}
#[wasm_bindgen]
pub fn skyline_presets() -> String {
crate::surface::skyline_presets()
}
#[wasm_bindgen]
pub fn skyline_profile(skyline_json: &str) -> Result<String, JsValue> {
crate::surface::skyline_profile(skyline_json, crate::surface::MAX_INPUT_BYTES)
.map_err(|e| JsValue::from_str(&e))
}
#[wasm_bindgen]
pub fn skyplot_svg(request_json: &str) -> Result<String, JsValue> {
crate::surface::skyplot_svg(request_json, crate::surface::MAX_INPUT_BYTES)
.map_err(|e| JsValue::from_str(&e))
}
#[wasm_bindgen]
pub fn iq_signals() -> String {
crate::iq::short::signals_json().to_string()
}
#[wasm_bindgen]
pub fn iq_run_short(
fs_hz: f64,
duration_s: f64,
signal: &str,
ids: Vec<f64>,
dopplers_hz: Vec<f64>,
cn0_dbhz: f64,
seed: u32,
) -> Result<String, JsValue> {
let req = crate::iq::short::ShortRequest {
fs_hz,
duration_s,
signal: signal.to_string(),
ids: ids.iter().map(|&v| v as i64).collect(),
dopplers: dopplers_hz,
cn0_dbhz: if cn0_dbhz.is_nan() {
None
} else {
Some(cn0_dbhz)
},
seed: u64::from(seed),
};
crate::iq::short::run_short(&req)
.map(|v| v.to_string())
.map_err(|e| JsValue::from_str(&e))
}