1#![allow(clippy::future_not_send, reason = "we work with js promises anyway")]23use std::{cell::RefCell, result::Result};45use jrsonnet_evaluator::{6 IStr, NumValue, ObjValue, Result as JrResult, SourcePath, SourceUrl, State, StateBuilder, Val,7 async_import::{ResolvedImportResolver, async_import},8 error,9 function::builtin::{NativeCallback, NativeCallbackHandler},10 manifest::{JsonFormat, ManifestFormat, StringFormat, ToStringFormat, YamlStreamFormat},11 tla::{TlaArg, apply_tla},12 trace::PathResolver,13 val::ArrValue,14 with_state,15};16use jrsonnet_formatter::FormatOptions;17use jrsonnet_gcmodule::Trace;18use jrsonnet_stdlib::{IniFormat, TomlFormat, XmlJsonmlFormat, YamlFormat};19use jrsonnet_types::ValType;20use js_sys::Reflect::get;21use rustc_hash::FxHashMap;22use wasm_bindgen::{convert::RefFromWasmAbi, prelude::*};2324#[wasm_bindgen]25#[derive(Clone, Copy)]26pub enum ValKind {27 Null,28 Bool,29 Num,30 Str,31 Arr,32 Obj,33 Func,34 BigInt,35}3637thread_local! {38 static ERR_FACTORY: RefCell<Option<js_sys::Function>> = const { RefCell::new(None) };39}40#[wasm_bindgen(js_name = setErrorFactory)]41pub fn set_error_factory(f: js_sys::Function) {42 ERR_FACTORY.with(|c| *c.borrow_mut() = Some(f));43}44fn make_jrsonnet_error(message: &str, frames: js_sys::Array, cause: &JsValue) -> JsValue {45 ERR_FACTORY.with(|c| {46 c.borrow().as_ref().map_or_else(47 || js_sys::Error::new(message).into(),48 |f| {49 let args = js_sys::Array::new();50 args.push(&JsValue::from_str(message));51 args.push(&frames);52 args.push(cause);53 f.apply(&JsValue::NULL, &args)54 .unwrap_or_else(|e| js_sys::Error::new(&format!("{e:?}")).into())55 },56 )57 })58}5960fn js_error_message(e: &JsValue) -> String {61 e.dyn_ref::<js_sys::Error>().map_or_else(62 || e.as_string().unwrap_or_else(|| format!("{e:?}")),63 |err| String::from(err.message()),64 )65}6667fn unwrap_val_ref(value: &JsValue) -> Result<<WasmVal as RefFromWasmAbi>::Anchor, JsValue> {68 #[allow(69 clippy::cast_sign_loss,70 clippy::cast_possible_truncation,71 reason = "defined to be u32"72 )]73 let ptr = get(value, &JsValue::from_str("__wbg_ptr"))74 .ok()75 .and_then(|v| v.as_f64())76 .ok_or_else(|| JsValue::from_str("expected a Val instance"))? as u32;77 if ptr == 0 {78 return Err(JsValue::from_str("Val has been freed"));79 }80 Ok(unsafe { <WasmVal as RefFromWasmAbi>::ref_from_abi(ptr) })81}8283fn js_resolver_error(prefix: &str, e: JsValue) -> JsValue {84 let msg = format!("{prefix}: {}", js_error_message(&e));85 let frames = js_sys::Array::new();86 let frame = js_sys::Object::new();87 let _ = js_sys::Reflect::set(88 &frame,89 &JsValue::from_str("desc"),90 &JsValue::from_str(prefix),91 );92 frames.push(&frame);93 make_jrsonnet_error(&msg, frames, &e)94}9596fn jrsonnet_js_error(e: &jrsonnet_evaluator::Error) -> JsValue {97 let msg = e.error().to_string();98 99 let frames = js_sys::Array::new();100 for el in &e.trace().0 {101 let frame = js_sys::Object::new();102 let _ = js_sys::Reflect::set(103 &frame,104 &JsValue::from_str("desc"),105 &JsValue::from_str(&el.desc),106 );107 if let Some(loc) = &el.location {108 let path = loc.0.source_path().to_string();109 let _ = js_sys::Reflect::set(110 &frame,111 &JsValue::from_str("path"),112 &JsValue::from_str(&path),113 );114 let mapped = loc.0.map_source_locations(&[loc.1, loc.2]);115 let _ = js_sys::Reflect::set(116 &frame,117 &JsValue::from_str("line"),118 &JsValue::from(mapped[0].line),119 );120 let _ = js_sys::Reflect::set(121 &frame,122 &JsValue::from_str("column"),123 &JsValue::from(mapped[0].column),124 );125 }126 frames.push(&frame);127 }128 make_jrsonnet_error(&msg, frames, &JsValue::UNDEFINED)129}130131impl From<ValType> for ValKind {132 fn from(v: ValType) -> Self {133 match v {134 ValType::Null => Self::Null,135 ValType::Bool => Self::Bool,136 ValType::Num => Self::Num,137 ValType::Str => Self::Str,138 ValType::Arr => Self::Arr,139 ValType::Obj => Self::Obj,140 ValType::Func => Self::Func,141 #[cfg(feature = "exp-bigint")]142 ValType::BigInt => Self::BigInt,143 }144 }145}146147#[wasm_bindgen(js_name = Val)]148pub struct WasmVal {149 val: Val,150 state: Option<State>,151}152153impl WasmVal {154 fn new(val: Val) -> Self {155 Self { val, state: None }156 }157 fn with_state(val: Val, state: State) -> Self {158 Self {159 val,160 state: Some(state),161 }162 }163 fn run<R>(&self, f: impl FnOnce(&Val) -> R) -> R {164 if let Some(state) = &self.state {165 let _guard = state.try_enter();166 f(&self.val)167 } else {168 f(&self.val)169 }170 }171 fn manifest_with(&self, format: impl ManifestFormat) -> Result<String, JsValue> {172 self.run(|v| v.manifest(format))173 .map_err(|e| jrsonnet_js_error(&e))174 }175}176177#[wasm_bindgen(js_class = Val)]178impl WasmVal {179 pub fn null() -> Self {180 Self::new(Val::Null)181 }182 pub fn bool(b: bool) -> Self {183 Self::new(Val::Bool(b))184 }185 pub fn num(n: f64) -> Result<Self, JsError> {186 let n = NumValue::new(n)187 .ok_or_else(|| JsError::new("only finite numbers are supported by jsonnet"))?;188 Ok(Self::new(Val::num(n)))189 }190 pub fn string(s: String) -> Self {191 Self::new(Val::string(s))192 }193 pub fn bigint(value: js_sys::BigInt) -> Result<Self, JsError> {194 #[cfg(feature = "exp-bigint")]195 {196 let s: String = value197 .to_string(10)198 .map_err(|_| JsError::new("invalid bigint"))?199 .into();200 let bi = s201 .parse::<num_bigint::BigInt>()202 .map_err(|e| JsError::new(&format!("failed to parse bigint: {e}")))?;203 Ok(Self::new(Val::BigInt(Box::new(bi))))204 }205 #[cfg(not(feature = "exp-bigint"))]206 {207 let _ = value;208 Err(JsError::new(209 "bigint support is not enabled in this build (exp-bigint feature)",210 ))211 }212 }213 pub fn arr(items: Vec<WasmVal>) -> Self {214 Self::new(Val::arr(215 items.into_iter().map(|v| v.val).collect::<Vec<_>>(),216 ))217 }218 pub fn func(219 params: Vec<String>,220221 #[wasm_bindgen(unchecked_param_type = "(...args: Val[]) => Val")]222 callback: js_sys::Function,223 ) -> Self {224 #[allow(deprecated)]225 Self::new(Val::function(NativeCallback::new(226 params,227 JsHandler { func: callback },228 )))229 }230231 #[wasm_bindgen(getter)]232 pub fn kind(&self) -> ValKind {233 self.val.value_type().into()234 }235 #[wasm_bindgen(js_name = asBool)]236 pub fn as_bool(&self) -> Option<bool> {237 self.val.as_bool()238 }239 #[wasm_bindgen(js_name = asNum)]240 pub fn as_num(&self) -> Option<f64> {241 self.val.as_num()242 }243 #[wasm_bindgen(js_name = asBigint)]244 pub fn as_bigint(&self) -> Result<Option<js_sys::BigInt>, JsError> {245 #[cfg(feature = "exp-bigint")]246 {247 let Some(bi) = self.val.as_bigint() else {248 return Ok(None);249 };250 let big = js_sys::BigInt::new(&JsValue::from_str(&bi.to_string()))251 .map_err(|e| JsError::new(&format!("{e:?}")))?;252 Ok(Some(big))253 }254 #[cfg(not(feature = "exp-bigint"))]255 {256 Err(JsError::new(257 "bigint support is not enabled in this build (exp-bigint feature)",258 ))259 }260 }261 #[wasm_bindgen(js_name = asString)]262 pub fn as_string(&self) -> Option<String> {263 self.val.as_str().map(|s| s.to_string())264 }265 #[wasm_bindgen(js_name = asArr)]266 pub fn as_arr(&self) -> Option<WasmArrValue> {267 self.val.as_arr().map(|arr| WasmArrValue {268 arr,269 state: self.state.clone(),270 })271 }272 #[wasm_bindgen(js_name = asObj)]273 pub fn as_obj(&self) -> Option<WasmObjValue> {274 self.val.as_obj().map(|obj| WasmObjValue {275 obj,276 state: self.state.clone(),277 })278 }279280 #[wasm_bindgen(js_name = applyTla)]281 pub fn apply_tla(282 &self,283 #[wasm_bindgen(unchecked_param_type = "Record<string, Val>")] args: &js_sys::Object,284 ) -> Result<WasmVal, JsValue> {285 let mut map: FxHashMap<IStr, TlaArg> = FxHashMap::default();286 for entry in js_sys::Object::entries(args).iter() {287 let pair: js_sys::Array = entry288 .dyn_into()289 .map_err(|_| JsValue::from_str("expected [key, value] entry"))?;290 let key = pair291 .get(0)292 .as_string()293 .ok_or_else(|| JsValue::from_str("TLA arg key must be a string"))?;294 let value = unwrap_val_ref(&pair.get(1))?;295 map.insert(key.into(), TlaArg::Val(value.val.clone()));296 }297 let val = self.val.clone();298 self.run(|_| apply_tla(&map, val))299 .map(|v| WasmVal {300 val: v,301 state: self.state.clone(),302 })303 .map_err(|e| jrsonnet_js_error(&e))304 }305306 #[wasm_bindgen(js_name = manifestJson)]307 pub fn manifest_json(&self, indent: u32) -> Result<String, JsValue> {308 self.manifest_with(JsonFormat::cli(309 indent as usize,310 #[cfg(feature = "exp-preserve-order")]311 false,312 ))313 }314 #[wasm_bindgen(js_name = manifestToString)]315 pub fn manifest_to_string(&self) -> Result<String, JsValue> {316 self.manifest_with(ToStringFormat)317 }318 #[wasm_bindgen(js_name = manifestString)]319 pub fn manifest_string(&self) -> Result<String, JsValue> {320 self.manifest_with(StringFormat)321 }322 #[wasm_bindgen(js_name = manifestYaml)]323 pub fn manifest_yaml(&self, indent: u32, quote_keys: bool) -> Result<String, JsValue> {324 self.manifest_with(YamlFormat::std_to_yaml(325 indent != 0,326 quote_keys,327 #[cfg(feature = "exp-preserve-order")]328 false,329 ))330 }331 #[wasm_bindgen(js_name = manifestYamlStream)]332 pub fn manifest_yaml_stream(333 &self,334 indent: u32,335 quote_keys: bool,336 c_document_end: bool,337 ) -> Result<String, JsValue> {338 self.manifest_with(YamlStreamFormat::std_yaml_stream(339 YamlFormat::std_to_yaml(340 indent != 0,341 quote_keys,342 #[cfg(feature = "exp-preserve-order")]343 false,344 ),345 c_document_end,346 ))347 }348 #[wasm_bindgen(js_name = manifestXmlJsonml)]349 pub fn manifest_xml_jsonml(&self) -> Result<String, JsValue> {350 self.manifest_with(XmlJsonmlFormat::std_to_xml())351 }352 #[wasm_bindgen(js_name = manifestToml)]353 pub fn manifest_toml(&self, indent: u32) -> Result<String, JsValue> {354 self.manifest_with(TomlFormat::std_to_toml(355 " ".repeat(indent as usize),356 #[cfg(feature = "exp-preserve-order")]357 false,358 ))359 }360 #[wasm_bindgen(js_name = manifestIni)]361 pub fn manifest_ini(&self) -> Result<String, JsValue> {362 self.manifest_with(IniFormat::std(363 #[cfg(feature = "exp-preserve-order")]364 false,365 ))366 }367}368369#[wasm_bindgen(js_name = ArrValue)]370pub struct WasmArrValue {371 arr: ArrValue,372 state: Option<State>,373}374375#[wasm_bindgen(js_class = ArrValue)]376impl WasmArrValue {377 #[wasm_bindgen(getter)]378 pub fn length(&self) -> u32 {379 self.arr.len32()380 }381 pub fn at(&self, index: u32) -> Result<Option<WasmVal>, JsValue> {382 let result = self.state.as_ref().map_or_else(383 || self.arr.get32(index),384 |state| {385 let _guard = state.try_enter();386 self.arr.get32(index)387 },388 );389 result390 .map(|opt: Option<Val>| {391 opt.map(|v| WasmVal {392 val: v,393 state: self.state.clone(),394 })395 })396 .map_err(|e| jrsonnet_js_error(&e))397 }398}399400#[wasm_bindgen(js_name = ObjValue)]401pub struct WasmObjValue {402 obj: ObjValue,403 state: Option<State>,404}405406#[wasm_bindgen(js_class = ObjValue)]407impl WasmObjValue {408 pub fn keys(&self) -> Vec<String> {409 self.obj410 .fields(411 #[cfg(feature = "exp-preserve-order")]412 false,413 )414 .into_iter()415 .map(|s| s.to_string())416 .collect()417 }418 pub fn get(&self, key: String) -> Result<Option<WasmVal>, JsValue> {419 let result = if let Some(state) = &self.state {420 let _guard = state.try_enter();421 self.obj.get(key.into())422 } else {423 self.obj.get(key.into())424 };425 result426 .map(|opt: Option<Val>| {427 opt.map(|v| WasmVal {428 val: v,429 state: self.state.clone(),430 })431 })432 .map_err(|e| jrsonnet_js_error(&e))433 }434}435436#[derive(Trace)]437struct JsHandler {438 #[trace(skip)]439 func: js_sys::Function,440}441442#[wasm_bindgen(inline_js = r"443export function js_invoke_val_callback(cb, args) {444 return cb.apply(null, args);445}446")]447extern "C" {448 #[wasm_bindgen(catch)]449 fn js_invoke_val_callback(450 cb: &js_sys::Function,451 args: &js_sys::Array,452 ) -> Result<WasmVal, JsValue>;453}454455impl NativeCallbackHandler for JsHandler {456 fn call(&self, args: &[Val]) -> JrResult<Val> {457 let js_args = js_sys::Array::new();458 let state = with_state(|s| s);459 for arg in args {460 js_args.push(&JsValue::from(WasmVal::with_state(461 arg.clone(),462 state.clone(),463 )));464 }465 let result = js_invoke_val_callback(&self.func, &js_args).map_err(|e| {466 let msg = e467 .as_string()468 .or_else(|| {469 e.dyn_ref::<js_sys::Error>()470 .map(|err| String::from(err.message()))471 })472 .unwrap_or_else(|| format!("{e:?}"));473 error!("js callback threw: {msg}")474 })?;475 Ok(result.val)476 }477}478479#[wasm_bindgen(js_name = State)]480pub struct WasmState {481 state: State,482 resolver: Option<JsAsyncResolver>,483}484#[wasm_bindgen(js_class = State)]485impl WasmState {486 #[wasm_bindgen(constructor)]487 pub fn new(resolver: Option<ImportResolverJs>) -> Self {488 console_error_panic_hook::set_once();489 let mut state = StateBuilder::default();490 state.import_resolver(ResolvedImportResolver::new());491 let std = jrsonnet_stdlib::ContextInitializer::new(PathResolver::Absolute);492 state.context_initializer(std);493 let state = state.build();494 Self {495 state,496 resolver: resolver.map(|js| JsAsyncResolver { js }),497 }498 }499500 #[wasm_bindgen(js_name = evaluateSnippet)]501 pub fn evaluate_snippet(&self, name: &str, snippet: &str) -> Result<WasmVal, JsValue> {502 let _guard = self.state.enter();503 self.state504 .evaluate_snippet(name, snippet)505 .map(|v| WasmVal::with_state(v, self.state.clone()))506 .map_err(|e| jrsonnet_js_error(&e))507 }508509 #[wasm_bindgen(js_name = evaluateFile)]510 pub async fn evaluate_file(&self, path: String) -> Result<WasmVal, JsValue> {511 self.evaluate_file_from_impl(None, path).await512 }513514 #[wasm_bindgen(js_name = evaluateFileFrom)]515 pub async fn evaluate_file_from(&self, from: String, path: String) -> Result<WasmVal, JsValue> {516 self.evaluate_file_from_impl(Some(from), path).await517 }518}519520impl WasmState {521 async fn evaluate_file_from_impl(522 &self,523 from: Option<String>,524 path: String,525 ) -> Result<WasmVal, JsValue> {526 let resolver = self527 .resolver528 .clone()529 .ok_or_else(|| JsValue::from_str("file evaluation requires an ImportResolver"))?;530 let from = match from {531 Some(s) => {532 let url = url::Url::parse(&s).map_err(|e| JsValue::from_str(&e.to_string()))?;533 SourcePath::new(SourceUrl::new(url))534 }535 None => SourcePath::default(),536 };537 let path = async_import(self.state.clone(), resolver, &from, &path.as_str()).await?;538 let _guard = self.state.enter();539 self.state540 .import_resolved(path)541 .map(|v| WasmVal::with_state(v, self.state.clone()))542 .map_err(|e| jrsonnet_js_error(&e))543 }544}545546#[wasm_bindgen]547extern "C" {548 #[wasm_bindgen(typescript_type = "ImportResolver")]549 #[derive(Clone)]550 pub type ImportResolverJs;551552 #[wasm_bindgen(catch, method, structural, js_name = resolveFrom)]553 fn resolve_from(554 this: &ImportResolverJs,555 from: Option<String>,556 path: &str,557 ) -> Result<js_sys::Promise, JsValue>;558559 #[wasm_bindgen(catch, method, structural, js_name = loadFileContents)]560 fn load_file_contents(561 this: &ImportResolverJs,562 resolved: &str,563 ) -> Result<js_sys::Promise, JsValue>;564}565566#[wasm_bindgen(typescript_custom_section)]567const TS_IMPORT_RESOLVER: &'static str = r"568export interface ImportResolver {569 resolveFrom(from: string | undefined, path: string): Promise<string>;570 loadFileContents(resolved: string): Promise<Uint8Array>;571}572";573574#[derive(Clone)]575struct JsAsyncResolver {576 js: ImportResolverJs,577}578579impl jrsonnet_evaluator::async_import::AsyncImportResolver for JsAsyncResolver {580 type Error = JsValue;581582 async fn resolve_from(583 &self,584 from: &SourcePath,585 path: &dyn jrsonnet_evaluator::AsPathLike,586 ) -> Result<SourcePath, JsValue> {587 let from_js = (!from.is_default()).then(|| from.to_string());588 let path_str = path.as_path().as_ref().to_string_lossy().into_owned();589 let promise = self590 .js591 .resolve_from(from_js, &path_str)592 .map_err(|e| js_resolver_error("resolveFrom", e))?;593 let resolved_js = wasm_bindgen_futures::JsFuture::from(promise)594 .await595 .map_err(|e| js_resolver_error("resolveFrom", e))?;596 let resolved_str = resolved_js597 .as_string()598 .ok_or_else(|| JsValue::from_str("resolveFrom must return string"))?;599 let url = url::Url::parse(&resolved_str).map_err(|e| JsValue::from_str(&e.to_string()))?;600 Ok(SourcePath::new(SourceUrl::new(url)))601 }602603 async fn load_file_contents(&self, resolved: &SourcePath) -> Result<Vec<u8>, JsValue> {604 let resolved_str = resolved.to_string();605 let promise = self606 .js607 .load_file_contents(&resolved_str)608 .map_err(|e| js_resolver_error("loadFileContents", e))?;609 let bytes_js = wasm_bindgen_futures::JsFuture::from(promise)610 .await611 .map_err(|e| js_resolver_error("loadFileContents", e))?;612 let arr = bytes_js613 .dyn_into::<js_sys::Uint8Array>()614 .map_err(|_| JsValue::from_str("loadFileContents must return Uint8Array"))?;615 Ok(arr.to_vec())616 }617}618619#[wasm_bindgen(js_name = FormatOptions)]620pub struct WasmFormatOptions {621 indent: u8,622 use_tabs: bool,623 max_width: u32,624}625#[wasm_bindgen(js_class = FormatOptions)]626impl WasmFormatOptions {627 #[wasm_bindgen(constructor)]628 pub fn new() -> Self {629 Self {630 indent: 4,631 use_tabs: true,632 max_width: 100,633 }634 }635636 fn build(&self) -> FormatOptions {637 FormatOptions {638 indent: self.indent,639 use_tabs: self.use_tabs,640 max_width: self.max_width,641 }642 }643}644645impl Default for WasmFormatOptions {646 fn default() -> Self {647 Self::new()648 }649}650651#[wasm_bindgen]652pub fn format(src: &str, opts: &WasmFormatOptions) -> Result<String, String> {653 match jrsonnet_formatter::format(src, &opts.build()) {654 Ok(v) => Ok(v),655 Err(e) => {656 let e = e.build();657 Err(hi_doc::source_to_ansi(&e))658 }659 }660}