git.delta.rocks / jrsonnet / refs/heads / master

difftreelog

source

bindings/jrsonnet-web/src/lib.rs16.9 KiBsourcehistory
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	// let msg = format.format(e).unwrap_or_else(|_| e.to_string());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}