git.delta.rocks / jrsonnet / refs/commits / 6ac56eac7eb3

difftreelog

source

bindings/jrsonnet-web/src/lib.rs10.7 KiBsourcehistory
1use std::result::Result;23use jrsonnet_evaluator::{4	NumValue, Result as JrResult, SourcePath, SourceUrl, State, StateBuilder, Val,5	async_import::{ResolvedImportResolver, async_import},6	error,7	function::builtin::{NativeCallback, NativeCallbackHandler},8	manifest::{JsonFormat, ManifestFormat, StringFormat, ToStringFormat, YamlStreamFormat},9	trace::{JsFormat, PathResolver, TraceFormat},10	with_state,11};12use jrsonnet_formatter::FormatOptions;13use jrsonnet_gcmodule::Trace;14use jrsonnet_stdlib::{IniFormat, TomlFormat, XmlJsonmlFormat, YamlFormat};15use jrsonnet_types::ValType;16use wasm_bindgen::prelude::*;1718#[wasm_bindgen]19#[derive(Clone, Copy)]20pub enum ValKind {21	Null,22	Bool,23	Num,24	Str,25	Arr,26	Obj,27	Func,28}2930#[wasm_bindgen(inline_js = r"31export class JrsonnetError extends Error {32	constructor(message, frames) {33		super(message);34		this.name = 'JrsonnetError';35		this.frames = frames;36	}37}38export function makeJrsonnetError(message, frames) {39	return new JrsonnetError(message, frames);40}41")]42extern "C" {43	#[wasm_bindgen(js_name = makeJrsonnetError)]44	fn make_jrsonnet_error(message: &str, frames: js_sys::Array) -> JsValue;45}4647#[wasm_bindgen(typescript_custom_section)]48const TS_JRSONNET_ERROR: &'static str = r"49export interface JrsonnetFrame {50	desc: string;51	path?: string;52	line?: number;53	column?: number;54}55export class JrsonnetError extends Error {56	name: 'JrsonnetError';57	frames: JrsonnetFrame[];58}59";6061fn jrsonnet_js_error(e: &jrsonnet_evaluator::Error) -> JsValue {62	let msg = e.error().to_string();63	// let msg = format.format(e).unwrap_or_else(|_| e.to_string());64	let frames = js_sys::Array::new();65	for el in &e.trace().0 {66		let frame = js_sys::Object::new();67		let _ = js_sys::Reflect::set(68			&frame,69			&JsValue::from_str("desc"),70			&JsValue::from_str(&el.desc),71		);72		if let Some(loc) = &el.location {73			let path = loc.0.source_path().to_string();74			let _ = js_sys::Reflect::set(75				&frame,76				&JsValue::from_str("path"),77				&JsValue::from_str(&path),78			);79			let mapped = loc.0.map_source_locations(&[loc.1, loc.2]);80			let _ = js_sys::Reflect::set(81				&frame,82				&JsValue::from_str("line"),83				&JsValue::from(mapped[0].line),84			);85			let _ = js_sys::Reflect::set(86				&frame,87				&JsValue::from_str("column"),88				&JsValue::from(mapped[0].column),89			);90		}91		frames.push(&frame);92	}93	make_jrsonnet_error(&msg, frames)94}9596impl From<ValType> for ValKind {97	fn from(v: ValType) -> Self {98		match v {99			ValType::Null => Self::Null,100			ValType::Bool => Self::Bool,101			ValType::Num => Self::Num,102			ValType::Str => Self::Str,103			ValType::Arr => Self::Arr,104			ValType::Obj => Self::Obj,105			ValType::Func => Self::Func,106		}107	}108}109110#[wasm_bindgen]111pub struct WasmVal {112	val: Val,113	state: Option<State>,114}115116impl WasmVal {117	fn new(val: Val) -> Self {118		Self { val, state: None }119	}120	fn with_state(val: Val, state: State) -> Self {121		Self {122			val,123			state: Some(state),124		}125	}126	fn child(&self, val: Val) -> Self {127		Self {128			val,129			state: self.state.clone(),130		}131	}132	fn run<R>(&self, f: impl FnOnce(&Val) -> R) -> R {133		if let Some(state) = &self.state {134			let _guard = state.try_enter();135			f(&self.val)136		} else {137			f(&self.val)138		}139	}140	fn manifest_with(&self, format: impl ManifestFormat) -> Result<String, JsValue> {141		self.run(|v| v.manifest(format))142			.map_err(|e| jrsonnet_js_error(&e))143	}144}145146#[wasm_bindgen]147impl WasmVal {148	pub fn null() -> Self {149		Self::new(Val::Null)150	}151	pub fn bool(b: bool) -> Self {152		Self::new(Val::Bool(b))153	}154	pub fn num(n: f64) -> Result<Self, JsError> {155		let n = NumValue::new(n)156			.ok_or_else(|| JsError::new("only finite numbers are supported by jsonnet"))?;157		Ok(Self::new(Val::num(n)))158	}159	pub fn string(s: String) -> Self {160		Self::new(Val::string(s))161	}162	pub fn arr(items: Vec<WasmVal>) -> Self {163		Self::new(Val::arr(164			items.into_iter().map(|v| v.val).collect::<Vec<_>>(),165		))166	}167	pub fn func(168		params: Vec<String>,169170		#[wasm_bindgen(unchecked_param_type = "(...args: WasmVal[]) => WasmVal")]171		callback: js_sys::Function,172	) -> Self {173		#[allow(deprecated)]174		Self::new(Val::function(NativeCallback::new(175			params,176			JsHandler { func: callback },177		)))178	}179180	#[wasm_bindgen(getter)]181	pub fn kind(&self) -> ValKind {182		self.val.value_type().into()183	}184	pub fn as_bool(&self) -> Option<bool> {185		self.val.as_bool()186	}187	pub fn as_num(&self) -> Option<f64> {188		self.val.as_num()189	}190	pub fn as_string(&self) -> Option<String> {191		self.val.as_str().map(|s| s.to_string())192	}193	pub fn arr_len(&self) -> Option<u32> {194		self.val.as_arr().map(|a| a.len())195	}196	pub fn arr_at(&self, index: u32) -> Result<Option<WasmVal>, JsValue> {197		let Some(a) = self.val.as_arr() else {198			return Ok(None);199		};200		self.run(|_| a.get(index))201			.map(|opt| opt.map(|v| self.child(v)))202			.map_err(|e| jrsonnet_js_error(&e))203	}204	pub fn obj_keys(&self) -> Option<Vec<String>> {205		self.val206			.as_obj()207			.map(|o| o.fields().into_iter().map(|s| s.to_string()).collect())208	}209	pub fn obj_get(&self, key: String) -> Result<Option<WasmVal>, JsValue> {210		let Some(o) = self.val.as_obj() else {211			return Ok(None);212		};213		self.run(|_| o.get(key.into()))214			.map(|opt| opt.map(|v| self.child(v)))215			.map_err(|e| jrsonnet_js_error(&e))216	}217218	pub fn manifest_json(&self, indent: u32) -> Result<String, JsValue> {219		self.manifest_with(JsonFormat::cli(indent as usize))220	}221	pub fn manifest_to_string(&self) -> Result<String, JsValue> {222		self.manifest_with(ToStringFormat)223	}224	pub fn manifest_string(&self) -> Result<String, JsValue> {225		self.manifest_with(StringFormat)226	}227	pub fn manifest_yaml(&self, indent: u32, quote_keys: bool) -> Result<String, JsValue> {228		self.manifest_with(YamlFormat::std_to_yaml(indent != 0, quote_keys))229	}230	pub fn manifest_yaml_stream(231		&self,232		indent: u32,233		quote_keys: bool,234		c_document_end: bool,235	) -> Result<String, JsValue> {236		self.manifest_with(YamlStreamFormat::std_yaml_stream(237			YamlFormat::std_to_yaml(indent != 0, quote_keys),238			c_document_end,239		))240	}241	pub fn manifest_xml_jsonml(&self) -> Result<String, JsValue> {242		self.manifest_with(XmlJsonmlFormat::std_to_xml())243	}244	pub fn manifest_toml(&self, indent: u32) -> Result<String, JsValue> {245		self.manifest_with(TomlFormat::std_to_toml(" ".repeat(indent as usize)))246	}247	pub fn manifest_ini(&self) -> Result<String, JsValue> {248		self.manifest_with(IniFormat::std())249	}250}251252#[derive(Trace)]253struct JsHandler {254	#[trace(skip)]255	func: js_sys::Function,256}257258#[wasm_bindgen(inline_js = r"259export function js_invoke_val_callback(cb, args) {260	return cb.apply(null, args);261}262")]263extern "C" {264	#[wasm_bindgen(catch)]265	fn js_invoke_val_callback(266		cb: &js_sys::Function,267		args: &js_sys::Array,268	) -> Result<WasmVal, JsValue>;269}270271impl NativeCallbackHandler for JsHandler {272	fn call(&self, args: &[Val]) -> JrResult<Val> {273		let js_args = js_sys::Array::new();274		let state = with_state(|s| s);275		for arg in args {276			js_args.push(&JsValue::from(WasmVal::with_state(277				arg.clone(),278				state.clone(),279			)));280		}281		let result = js_invoke_val_callback(&self.func, &js_args).map_err(|e| {282			let msg = e283				.as_string()284				.or_else(|| {285					e.dyn_ref::<js_sys::Error>()286						.map(|err| String::from(err.message()))287				})288				.unwrap_or_else(|| format!("{e:?}"));289			error!("js callback threw: {msg}")290		})?;291		Ok(result.val)292	}293}294295#[wasm_bindgen]296pub struct WasmState {297	state: State,298	resolver: JsAsyncResolver,299}300#[wasm_bindgen]301impl WasmState {302	#[wasm_bindgen(constructor)]303	pub fn new(resolver: ImportResolverJs) -> Self {304		console_error_panic_hook::set_once();305		let mut state = StateBuilder::default();306		state.import_resolver(ResolvedImportResolver::new());307		let std = jrsonnet_stdlib::ContextInitializer::new(PathResolver::Absolute);308		state.context_initializer(std);309		let state = state.build();310		Self {311			state,312			resolver: JsAsyncResolver { js: resolver },313		}314	}315316	#[wasm_bindgen]317	pub fn evaluate_snippet(&self, name: &str, snippet: &str) -> Result<WasmVal, JsValue> {318		let _guard = self.state.enter();319		self.state320			.evaluate_snippet(name, snippet)321			.map(|v| WasmVal::with_state(v, self.state.clone()))322			.map_err(|e| jrsonnet_js_error(&e))323	}324325	pub async fn evaluate_file(&self, path: String) -> Result<WasmVal, JsValue> {326		let path = async_import(self.state.clone(), self.resolver.clone(), &path.as_str()).await?;327		let _guard = self.state.enter();328		self.state329			.import_resolved(path)330			.map(|v| WasmVal::with_state(v, self.state.clone()))331			.map_err(|e| jrsonnet_js_error(&e))332	}333}334335#[wasm_bindgen]336extern "C" {337	#[wasm_bindgen(typescript_type = "ImportResolver")]338	#[derive(Clone)]339	pub type ImportResolverJs;340341	#[wasm_bindgen(catch, method, structural, js_name = resolveFrom)]342	fn resolve_from(343		this: &ImportResolverJs,344		from: Option<String>,345		path: &str,346	) -> Result<js_sys::Promise, JsValue>;347348	#[wasm_bindgen(catch, method, structural, js_name = loadFileContents)]349	fn load_file_contents(350		this: &ImportResolverJs,351		resolved: &str,352	) -> Result<js_sys::Promise, JsValue>;353}354355#[wasm_bindgen(typescript_custom_section)]356const TS_IMPORT_RESOLVER: &'static str = r"357export interface ImportResolver {358	resolveFrom(from: string | undefined, path: string): Promise<string>;359	loadFileContents(resolved: string): Promise<Uint8Array>;360}361";362363#[derive(Clone)]364struct JsAsyncResolver {365	js: ImportResolverJs,366}367368impl jrsonnet_evaluator::async_import::AsyncImportResolver for JsAsyncResolver {369	type Error = JsValue;370371	async fn resolve_from(372		&self,373		from: &SourcePath,374		path: &dyn jrsonnet_evaluator::AsPathLike,375	) -> Result<SourcePath, JsValue> {376		let from_js = (!from.is_default()).then(|| from.to_string());377		let path_str = path.as_path().as_ref().to_string_lossy().into_owned();378		let promise = self.js.resolve_from(from_js, &path_str)?;379		let resolved_js = wasm_bindgen_futures::JsFuture::from(promise).await?;380		let resolved_str = resolved_js381			.as_string()382			.ok_or_else(|| JsValue::from_str("resolveFrom must return string"))?;383		let url = url::Url::parse(&resolved_str).map_err(|e| JsValue::from_str(&e.to_string()))?;384		Ok(SourcePath::new(SourceUrl::new(url)))385	}386387	async fn load_file_contents(&self, resolved: &SourcePath) -> Result<Vec<u8>, JsValue> {388		let resolved_str = resolved.to_string();389		let promise = self.js.load_file_contents(&resolved_str)?;390		let bytes_js = wasm_bindgen_futures::JsFuture::from(promise).await?;391		let arr = bytes_js392			.dyn_into::<js_sys::Uint8Array>()393			.map_err(|_| JsValue::from_str("loadFileContents must return Uint8Array"))?;394		Ok(arr.to_vec())395	}396}397398#[wasm_bindgen]399pub struct WasmFormatOptions {}400#[wasm_bindgen]401impl WasmFormatOptions {402	#[wasm_bindgen(constructor)]403	pub fn new() -> Self {404		Self {}405	}406407	fn build(&self) -> FormatOptions {408		FormatOptions { indent: 0 }409	}410}411412#[wasm_bindgen]413pub fn format(src: &str, opts: &WasmFormatOptions) -> Result<String, String> {414	match jrsonnet_formatter::format(src, &opts.build()) {415		Ok(v) => Ok(v),416		Err(e) => {417			let e = e.build();418			Err(hi_doc::source_to_ansi(&e))419		}420	}421}