git.delta.rocks / jrsonnet / refs/commits / 9aabcd49c223

difftreelog

refactor(web) idiomatic js api

zwxrzuuxYaroslav Bolyukin2026-05-05parent: #6ac56ea.patch.diff
in: master

10 files changed

modifiedCargo.lockdiffbeforeafterboth
1119 "jrsonnet-stdlib",1119 "jrsonnet-stdlib",
1120 "jrsonnet-types",1120 "jrsonnet-types",
1121 "js-sys",1121 "js-sys",
1122 "rustc-hash 2.1.2",
1122 "url",1123 "url",
1123 "wasm-bindgen",1124 "wasm-bindgen",
1124 "wasm-bindgen-futures",1125 "wasm-bindgen-futures",
modifiedREADME.adocdiffbeforeafterboth
8888
89Jrsonnet is written in rust itself, so just add it as dependency89Jrsonnet is written in rust itself, so just add it as dependency
9090
91=== JavaScript/TypeScript (WASM)
92
93// image:https://img.shields.io/npm/v/jrsonnet[alt=npm, link=https://www.npmjs.com/package/jrsonnet]
94image:https://jsr.io/badges/@jrsonnet/jrsonnet[alt=JSR, link=https://jsr.io/@jrsonnet/jrsonnet]
95image:https://jsr.io/badges/@jrsonnet/jrsonnet/score[alt=JSR score, link=https://jsr.io/@jrsonnet/jrsonnet]
96
97WASM bindings are published to JSR as `@jrsonnet/jrsonnet`.
98
99[source,sh]
100----
101deno add jsr:@jrsonnet/jrsonnet
102----
103
104// npm package (`jrsonnet`) is not yet published.
105
91=== Python106=== Python
92107
93image:https://img.shields.io/pypi/v/rjsonnet[alt=crates.io, link=https://pypi.org/project/rjsonnet/]108image:https://img.shields.io/pypi/v/rjsonnet[alt=crates.io, link=https://pypi.org/project/rjsonnet/]
100115
101=== Other116=== Other
102117
103WASM bingings are also available, Java bindings (Both JNI and WASM compiled to `.class`) are in progress118Java bindings (Both JNI and WASM compiled to `.class`) are in progress
104119
105See link:./bindings/[bindings] for more information.120See link:./bindings/[bindings] for more information.
106121
modifiedbindings/jrsonnet-web/Cargo.tomldiffbeforeafterboth
1[package]1[package]
2name = "jrsonnet-web"2name = "jrsonnet-web"
3description = "WASM JS bindings for jrsonnet"3description = "WebAssembly JavaScript bindings for jrsonnet"
4keywords = ["jsonnet", "wasm", "web"]
5categories = ["wasm"]
4authors.workspace = true6authors.workspace = true
5edition.workspace = true7edition.workspace = true
6license.workspace = true8license.workspace = true
17jrsonnet-stdlib.workspace = true19jrsonnet-stdlib.workspace = true
18jrsonnet-types.workspace = true20jrsonnet-types.workspace = true
19js-sys.workspace = true21js-sys.workspace = true
22rustc-hash.workspace = true
20url.workspace = true23url.workspace = true
21wasm-bindgen.workspace = true24wasm-bindgen.workspace = true
22wasm-bindgen-futures.workspace = true25wasm-bindgen-futures.workspace = true
modifiedbindings/jrsonnet-web/deno.jsondiffbeforeafterboth
1{1{
2 "name": "@jrsonnet/jrsonnet",2 "name": "@jrsonnet/jrsonnet",
3 "license": "MIT",3 "license": "MIT",
4 "version": "0.0.1",4 "version": "0.0.2",
5 "tasks": {5 "tasks": {
6 "wasmbuild": "deno run -A @deno/wasmbuild -p jrsonnet-web --skip-opt"6 "wasmbuild": "deno run -A @deno/wasmbuild -p jrsonnet-web --skip-opt"
7 },7 },
11 "@std/assert": "jsr:@std/assert@^1.0.19"11 "@std/assert": "jsr:@std/assert@^1.0.19"
12 },12 },
13 "exports": {13 "exports": {
14 ".": "./mod.ts"14 ".": "./mod.ts",
15 "./fmt": "./fmt.ts"
15 },16 },
16 "publish": {17 "publish": {
17 "exclude": [18 "exclude": [
18 "!lib"19 "!lib"
19 ]20 ]
20 }21 },
22 "fmt": {
23 "useTabs": true
24 }
21}25}
2226
modifiedbindings/jrsonnet-web/fmt.test.tsdiffbeforeafterboth
1import { assertEquals } from "@std/assert";1import { assertEquals } from "@std/assert";
2import { format, FormatOptions } from "./mod.ts";2import { format, FormatOptions } from "./fmt.ts";
33
4Deno.test("format", () => {4Deno.test("format", () => {
5 const opts = new FormatOptions();5 const opts = new FormatOptions();
modifiedbindings/jrsonnet-web/fmt.tsdiffbeforeafterboth

no changes

modifiedbindings/jrsonnet-web/mod.test.tsdiffbeforeafterboth
1import { assertEquals } from "@std/assert";1import { assertEquals, assertRejects, assertThrows } from "@std/assert";
2import { WasmState } from "./mod.ts";2import { type ImportResolver, JrsonnetError, State, ValKind } from "./mod.ts";
3
4Deno.test("evaluateSnippet returns numbers", () => {
5 const state = new State();
6 const v = state.evaluateSnippet("test.jsonnet", "1 + 2");
7 assertEquals(v.kind, ValKind.Num);
8 assertEquals(v.asNum(), 3);
9});
10
11Deno.test("evaluateSnippet returns booleans", () => {
12 const state = new State();
13 const v = state.evaluateSnippet("test.jsonnet", "true && !false");
14 assertEquals(v.kind, ValKind.Bool);
15 assertEquals(v.asBool(), true);
16});
17
18Deno.test("evaluateSnippet returns strings", () => {
19 const state = new State();
20 const v = state.evaluateSnippet("test.jsonnet", "'hello ' + 'world'");
21 assertEquals(v.kind, ValKind.Str);
22 assertEquals(v.asString(), "hello world");
23});
24
25Deno.test("evaluateSnippet returns null", () => {
26 const state = new State();
27 const v = state.evaluateSnippet("test.jsonnet", "null");
28 assertEquals(v.kind, ValKind.Null);
29 assertEquals(v.asNum(), undefined);
30});
331
4Deno.test("basic", () => {32Deno.test("Val.asArr exposes ArrValue", () => {
5 const state = new WasmState();33 const state = new State();
6
7 assertEquals(state.evaluate_snippet("test.jsonnet", "1 + 2").as_num(), 3);34 const arr = state.evaluateSnippet("test.jsonnet", "[10, 20, 30]").asArr();
35 if (!arr) throw new Error("expected array");
36 assertEquals(arr.length, 3);
37 assertEquals(arr.at(1)?.asNum(), 20);
38 assertEquals(arr.at(99), undefined);
8});39});
40
41Deno.test("Val.asObj exposes ObjValue", () => {
42 const state = new State();
43 const obj = state.evaluateSnippet("test.jsonnet", "{a: 1, b: 'two'}").asObj();
44 if (!obj) throw new Error("expected object");
45 assertEquals(obj.keys().sort(), ["a", "b"]);
46 assertEquals(obj.get("a")?.asNum(), 1);
47 assertEquals(obj.get("b")?.asString(), "two");
48 assertEquals(obj.get("missing"), undefined);
49});
50
51Deno.test("evaluateSnippet manifests JSON", () => {
52 const state = new State();
53 const v = state.evaluateSnippet("test.jsonnet", "{a: 1, b: [2, 3]}");
54 assertEquals(v.manifestJson(0), '{"a":1,"b":[2,3]}');
55});
56
57Deno.test("evaluateSnippet propagates jsonnet errors", () => {
58 const state = new State();
59 assertThrows(() => state.evaluateSnippet("test.jsonnet", "error 'boom'"));
60});
61
62Deno.test("evaluateFile without resolver rejects", async () => {
63 const state = new State();
64 await assertRejects(() => state.evaluateFile("anything.jsonnet"));
65});
66
67Deno.test("resolver errors become JrsonnetError with cause", async () => {
68 const original = new Error("disk on fire");
69 const resolver: ImportResolver = {
70 resolveFrom(_from, path) {
71 return Promise.resolve(`memory:///${path}`);
72 },
73 loadFileContents(_resolved) {
74 throw original;
75 },
76 };
77 const state = new State(resolver);
78 const err = await assertRejects(
79 () => state.evaluateFile("anything.jsonnet"),
80 JrsonnetError,
81 "loadFileContents",
82 );
83 assertEquals(err.cause, original);
84 assertEquals(err.frames[0]?.desc, "loadFileContents");
85 // The wrapped error's own stack must not mention internal wasm frames.
86 assertEquals((err.stack ?? "").includes(".wasm"), false);
87});
88
89Deno.test("Val.applyTla calls function with named args", () => {
90 const state = new State();
91 const fn = state.evaluateSnippet(
92 "test.jsonnet",
93 "function(x, y) x + y",
94 );
95 const result = fn.applyTla({
96 x: state.evaluateSnippet("x.jsonnet", "10"),
97 y: state.evaluateSnippet("y.jsonnet", "32"),
98 });
99 assertEquals(result.asNum(), 42);
100});
101
102Deno.test("Val.applyTla borrows args without consuming them", () => {
103 const state = new State();
104 const fn = state.evaluateSnippet("test.jsonnet", "function(x) x * 2");
105 const x = state.evaluateSnippet("x.jsonnet", "21");
106 assertEquals(fn.applyTla({ x }).asNum(), 42);
107 assertEquals(x.asNum(), 21);
108 assertEquals(fn.applyTla({ x }).asNum(), 42);
109});
110
111Deno.test("Val.applyTla on non-function returns the value unchanged", () => {
112 const state = new State();
113 const v = state.evaluateSnippet("test.jsonnet", "123");
114 assertEquals(v.applyTla({}).asNum(), 123);
115});
116
117Deno.test("evaluateFileFrom resolves relative paths", async () => {
118 const files: Record<string, string> = {
119 "memory:///root/main.jsonnet": "import 'lib.jsonnet'",
120 "memory:///root/lib.jsonnet": "{ answer: 42 }",
121 };
122 const resolver: ImportResolver = {
123 resolveFrom(from, path) {
124 const base = from ?? "memory:///root/";
125 return Promise.resolve(new URL(path, base).toString());
126 },
127 loadFileContents(resolved) {
128 const code = files[resolved];
129 if (code === undefined) throw new Error(`missing ${resolved}`);
130 return Promise.resolve(new TextEncoder().encode(code));
131 },
132 };
133 const state = new State(resolver);
134 const v = await state.evaluateFileFrom(
135 "memory:///root/main.jsonnet",
136 "./lib.jsonnet",
137 );
138 assertEquals(v.asObj()?.get("answer")?.asNum(), 42);
139});
9140
modifiedbindings/jrsonnet-web/mod.tsdiffbeforeafterboth
1import { assert } from "@std/assert";1import { assert } from "@std/assert";
2import {2import {
3 format as formatRaw,3 ArrValue,
4 type ImportResolver,4 type ImportResolver,
5 WasmFormatOptions,5 ObjValue,
6 WasmState,6 setErrorFactory,
7 WasmVal,7 State,
8 Val,
9 ValKind,
8} from "./lib/jsonnet_web.js";10} from "./lib/jsonnet_web.js";
11
12export interface JrsonnetFrame {
13 desc: string;
14 path?: string;
15 line?: number;
16 column?: number;
17}
18
19export class JrsonnetError extends Error {
20 override name = "JrsonnetError" as const;
21 frames: JrsonnetFrame[];
22
23 constructor(message: string, frames: JrsonnetFrame[], cause?: unknown) {
24 super(message, cause !== undefined ? { cause } : undefined);
25 this.frames = frames;
26 }
27}
28
29setErrorFactory(
30 (message: string, frames: JrsonnetFrame[], cause: unknown) =>
31 new JrsonnetError(message, frames, cause),
32);
933
10export { type ImportResolver, WasmFormatOptions, WasmState, WasmVal };34export { ArrValue, type ImportResolver, ObjValue, State, Val, ValKind };
1135
12class FetchImportResolver implements ImportResolver {36export class FetchImportResolver implements ImportResolver {
13 constructor(public base: string) {}37 constructor(base: URL | string) {
1438 this.#base = new URL(base);
39 }
40
41 #base: URL;
15 resolution = new Map<string, URL>();42 #resolution = new Map<string, string>();
16 response = new Map<string, Response>();43 #bytes = new Map<string, Uint8Array>();
1744
18 async resolveFrom(from: string | undefined, path: string): Promise<string> {45 async resolveFrom(from: string | undefined, path: string): Promise<string> {
19 let resolved: URL;
20 if (from) {
21 resolved = new URL(path, from);
22 } else {
23 resolved = new URL(path, this.base);46 const base = from !== undefined ? from : this.#base;
24 }
25 const resolvingStr = resolved.toString();47 const requestStr = new URL(path, base).toString();
48
26 resolved = this.resolution.get(resolvingStr) ?? resolved;49 const cached = this.#resolution.get(requestStr);
27
28 const resolvedStr = resolved.toString();
29 if (!this.response.has(resolvedStr)) {50 if (cached !== undefined) return cached;
30 console.log(resolved);51
31 const v = await fetch(resolved);52 const resp = await fetch(requestStr);
32 this.response.set(resolvedStr, v);53 if (!resp.ok) {
54 throw new Error(
55 `fetch ${requestStr}: HTTP ${resp.status} ${resp.statusText}`,
56 );
57 }
33 resolved = new URL(v.url);58 const canonical = resp.url;
59 if (!this.#bytes.has(canonical)) {
34 this.resolution.set(resolvingStr, resolved);60 this.#bytes.set(canonical, await resp.bytes());
35 }61 }
36 return resolved.toString();62 this.#resolution.set(requestStr, canonical);
63 return canonical;
37 }64 }
65
38 loadFileContents(resolved: string): Promise<Uint8Array> {66 loadFileContents(resolved: string): Promise<Uint8Array> {
39 console.log(resolved);
40 const v = this.response.get(resolved);67 const bytes = this.#bytes.get(resolved);
41 assert(v, "should be resolved");68 assert(bytes, `not loaded: ${resolved}`);
42 return v.bytes();69 return Promise.resolve(bytes);
43 }70 }
44}71}
45
46//
47// try {
48// console.log("eval file");
49// await state.evaluate_file("example.jsonnet");
50// console.log("eval file done");
51// } catch (e) {
52// console.log(e);
53// }
54//
55export function format(
56 code: string,
57 opts: WasmFormatOptions = new WasmFormatOptions(),
58): string {
59 return formatRaw(code, opts);
60}
6172
modifiedbindings/jrsonnet-web/scripts/build_npm.tsdiffbeforeafterboth

no syntactic changes

modifiedbindings/jrsonnet-web/src/lib.rsdiffbeforeafterboth
1use std::result::Result;1#![allow(clippy::future_not_send, reason = "we work with js promises anyway")]
22
3use std::{cell::RefCell, result::Result};
4
3use jrsonnet_evaluator::{5use jrsonnet_evaluator::{
4 NumValue, Result as JrResult, SourcePath, SourceUrl, State, StateBuilder, Val,6 IStr, NumValue, ObjValue, Result as JrResult, SourcePath, SourceUrl, State, StateBuilder, Val,
5 async_import::{ResolvedImportResolver, async_import},7 async_import::{ResolvedImportResolver, async_import},
6 error,8 error,
7 function::builtin::{NativeCallback, NativeCallbackHandler},9 function::builtin::{NativeCallback, NativeCallbackHandler},
8 manifest::{JsonFormat, ManifestFormat, StringFormat, ToStringFormat, YamlStreamFormat},10 manifest::{JsonFormat, ManifestFormat, StringFormat, ToStringFormat, YamlStreamFormat},
9 trace::{JsFormat, PathResolver, TraceFormat},11 tla::{TlaArg, apply_tla},
12 trace::PathResolver,
13 val::ArrValue,
10 with_state,14 with_state,
11};15};
12use jrsonnet_formatter::FormatOptions;16use jrsonnet_formatter::FormatOptions;
13use jrsonnet_gcmodule::Trace;17use jrsonnet_gcmodule::Trace;
14use jrsonnet_stdlib::{IniFormat, TomlFormat, XmlJsonmlFormat, YamlFormat};18use jrsonnet_stdlib::{IniFormat, TomlFormat, XmlJsonmlFormat, YamlFormat};
15use jrsonnet_types::ValType;19use jrsonnet_types::ValType;
16use wasm_bindgen::prelude::*;20use js_sys::Reflect::get;
21use rustc_hash::FxHashMap;
22use wasm_bindgen::{convert::RefFromWasmAbi, prelude::*};
1723
18#[wasm_bindgen]24#[wasm_bindgen]
19#[derive(Clone, Copy)]25#[derive(Clone, Copy)]
27 Func,33 Func,
28}34}
2935
30#[wasm_bindgen(inline_js = r"36thread_local! {
31export class JrsonnetError extends Error {37 static ERR_FACTORY: RefCell<Option<js_sys::Function>> = const { RefCell::new(None) };
32 constructor(message, frames) {
33 super(message);
34 this.name = 'JrsonnetError';
35 this.frames = frames;
36 }
37}38}
38export function makeJrsonnetError(message, frames) {39#[wasm_bindgen(js_name = setErrorFactory)]
40pub fn set_error_factory(f: js_sys::Function) {
39 return new JrsonnetError(message, frames);41 ERR_FACTORY.with(|c| *c.borrow_mut() = Some(f));
40}42}
41")]
42extern "C" {
43 #[wasm_bindgen(js_name = makeJrsonnetError)]
44 fn make_jrsonnet_error(message: &str, frames: js_sys::Array) -> JsValue;43fn make_jrsonnet_error(message: &str, frames: js_sys::Array, cause: &JsValue) -> JsValue {
44 ERR_FACTORY.with(|c| {
45 c.borrow().as_ref().map_or_else(
46 || js_sys::Error::new(message).into(),
47 |f| {
48 let args = js_sys::Array::new();
49 args.push(&JsValue::from_str(message));
50 args.push(&frames);
51 args.push(cause);
52 f.apply(&JsValue::NULL, &args)
53 .unwrap_or_else(|e| js_sys::Error::new(&format!("{e:?}")).into())
54 },
55 )
56 })
45}57}
4658
47#[wasm_bindgen(typescript_custom_section)]59fn js_error_message(e: &JsValue) -> String {
48const TS_JRSONNET_ERROR: &'static str = r"
49export interface JrsonnetFrame {
50 desc: string;60 e.dyn_ref::<js_sys::Error>().map_or_else(
51 path?: string;61 || e.as_string().unwrap_or_else(|| format!("{e:?}")),
52 line?: number;62 |err| String::from(err.message()),
53 column?: number;63 )
54}64}
65
55export class JrsonnetError extends Error {66fn unwrap_val_ref(value: &JsValue) -> Result<<WasmVal as RefFromWasmAbi>::Anchor, JsValue> {
56 name: 'JrsonnetError';67 let ptr = get(value, &JsValue::from_str("__wbg_ptr"))
68 .ok()
69 .and_then(|v| v.as_f64())
70 .ok_or_else(|| JsValue::from_str("expected a Val instance"))? as u32;
57 frames: JrsonnetFrame[];71 if ptr == 0 {
72 return Err(JsValue::from_str("Val has been freed"));
73 }
74 Ok(unsafe { <WasmVal as RefFromWasmAbi>::ref_from_abi(ptr) })
58}75}
59";
6076
77fn js_resolver_error(prefix: &str, e: JsValue) -> JsValue {
78 let msg = format!("{prefix}: {}", js_error_message(&e));
79 let frames = js_sys::Array::new();
80 let frame = js_sys::Object::new();
81 let _ = js_sys::Reflect::set(
82 &frame,
83 &JsValue::from_str("desc"),
84 &JsValue::from_str(prefix),
85 );
86 frames.push(&frame);
87 make_jrsonnet_error(&msg, frames, &e)
88}
89
61fn jrsonnet_js_error(e: &jrsonnet_evaluator::Error) -> JsValue {90fn jrsonnet_js_error(e: &jrsonnet_evaluator::Error) -> JsValue {
62 let msg = e.error().to_string();91 let msg = e.error().to_string();
63 // let msg = format.format(e).unwrap_or_else(|_| e.to_string());92 // let msg = format.format(e).unwrap_or_else(|_| e.to_string());
90 }119 }
91 frames.push(&frame);120 frames.push(&frame);
92 }121 }
93 make_jrsonnet_error(&msg, frames)122 make_jrsonnet_error(&msg, frames, &JsValue::UNDEFINED)
94}123}
95124
96impl From<ValType> for ValKind {125impl From<ValType> for ValKind {
107 }136 }
108}137}
109138
110#[wasm_bindgen]139#[wasm_bindgen(js_name = Val)]
111pub struct WasmVal {140pub struct WasmVal {
112 val: Val,141 val: Val,
113 state: Option<State>,142 state: Option<State>,
123 state: Some(state),152 state: Some(state),
124 }153 }
125 }154 }
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 {155 fn run<R>(&self, f: impl FnOnce(&Val) -> R) -> R {
133 if let Some(state) = &self.state {156 if let Some(state) = &self.state {
134 let _guard = state.try_enter();157 let _guard = state.try_enter();
143 }166 }
144}167}
145168
146#[wasm_bindgen]169#[wasm_bindgen(js_class = Val)]
147impl WasmVal {170impl WasmVal {
148 pub fn null() -> Self {171 pub fn null() -> Self {
149 Self::new(Val::Null)172 Self::new(Val::Null)
167 pub fn func(190 pub fn func(
168 params: Vec<String>,191 params: Vec<String>,
169192
170 #[wasm_bindgen(unchecked_param_type = "(...args: WasmVal[]) => WasmVal")]193 #[wasm_bindgen(unchecked_param_type = "(...args: Val[]) => Val")]
171 callback: js_sys::Function,194 callback: js_sys::Function,
172 ) -> Self {195 ) -> Self {
173 #[allow(deprecated)]196 #[allow(deprecated)]
181 pub fn kind(&self) -> ValKind {204 pub fn kind(&self) -> ValKind {
182 self.val.value_type().into()205 self.val.value_type().into()
183 }206 }
207 #[wasm_bindgen(js_name = asBool)]
184 pub fn as_bool(&self) -> Option<bool> {208 pub fn as_bool(&self) -> Option<bool> {
185 self.val.as_bool()209 self.val.as_bool()
186 }210 }
211 #[wasm_bindgen(js_name = asNum)]
187 pub fn as_num(&self) -> Option<f64> {212 pub fn as_num(&self) -> Option<f64> {
188 self.val.as_num()213 self.val.as_num()
189 }214 }
215 #[wasm_bindgen(js_name = asString)]
190 pub fn as_string(&self) -> Option<String> {216 pub fn as_string(&self) -> Option<String> {
191 self.val.as_str().map(|s| s.to_string())217 self.val.as_str().map(|s| s.to_string())
192 }218 }
193 pub fn arr_len(&self) -> Option<u32> {219 #[wasm_bindgen(js_name = asArr)]
220 pub fn as_arr(&self) -> Option<WasmArrValue> {
194 self.val.as_arr().map(|a| a.len())221 self.val.as_arr().map(|arr| WasmArrValue {
222 arr,
223 state: self.state.clone(),
224 })
195 }225 }
196 pub fn arr_at(&self, index: u32) -> Result<Option<WasmVal>, JsValue> {226 #[wasm_bindgen(js_name = asObj)]
227 pub fn as_obj(&self) -> Option<WasmObjValue> {
197 let Some(a) = self.val.as_arr() else {228 self.val.as_obj().map(|obj| WasmObjValue {
198 return Ok(None);229 obj,
199 };230 state: self.state.clone(),
200 self.run(|_| a.get(index))
201 .map(|opt| opt.map(|v| self.child(v)))231 })
202 .map_err(|e| jrsonnet_js_error(&e))
203 }232 }
233
204 pub fn obj_keys(&self) -> Option<Vec<String>> {234 #[wasm_bindgen(js_name = applyTla)]
235 pub fn apply_tla(
236 &self,
237 #[wasm_bindgen(unchecked_param_type = "Record<string, Val>")] args: &js_sys::Object,
238 ) -> Result<WasmVal, JsValue> {
205 self.val239 let mut map: FxHashMap<IStr, TlaArg> = FxHashMap::default();
240 for entry in js_sys::Object::entries(args).iter() {
206 .as_obj()241 let pair: js_sys::Array = entry
242 .dyn_into()
207 .map(|o| o.fields().into_iter().map(|s| s.to_string()).collect())243 .map_err(|_| JsValue::from_str("expected [key, value] entry"))?;
244 let key = pair
245 .get(0)
246 .as_string()
208 }247 .ok_or_else(|| JsValue::from_str("TLA arg key must be a string"))?;
209 pub fn obj_get(&self, key: String) -> Result<Option<WasmVal>, JsValue> {
210 let Some(o) = self.val.as_obj() else {248 let value = unwrap_val_ref(&pair.get(1))?;
211 return Ok(None);249 map.insert(key.into(), TlaArg::Val(value.val.clone()));
212 };250 }
251 let val = self.val.clone();
213 self.run(|_| o.get(key.into()))252 self.run(|_| apply_tla(&map, val))
214 .map(|opt| opt.map(|v| self.child(v)))253 .map(|v| WasmVal {
254 val: v,
255 state: self.state.clone(),
256 })
215 .map_err(|e| jrsonnet_js_error(&e))257 .map_err(|e| jrsonnet_js_error(&e))
216 }258 }
217259
260 #[wasm_bindgen(js_name = manifestJson)]
218 pub fn manifest_json(&self, indent: u32) -> Result<String, JsValue> {261 pub fn manifest_json(&self, indent: u32) -> Result<String, JsValue> {
219 self.manifest_with(JsonFormat::cli(indent as usize))262 self.manifest_with(JsonFormat::cli(indent as usize))
220 }263 }
264 #[wasm_bindgen(js_name = manifestToString)]
221 pub fn manifest_to_string(&self) -> Result<String, JsValue> {265 pub fn manifest_to_string(&self) -> Result<String, JsValue> {
222 self.manifest_with(ToStringFormat)266 self.manifest_with(ToStringFormat)
223 }267 }
268 #[wasm_bindgen(js_name = manifestString)]
224 pub fn manifest_string(&self) -> Result<String, JsValue> {269 pub fn manifest_string(&self) -> Result<String, JsValue> {
225 self.manifest_with(StringFormat)270 self.manifest_with(StringFormat)
226 }271 }
272 #[wasm_bindgen(js_name = manifestYaml)]
227 pub fn manifest_yaml(&self, indent: u32, quote_keys: bool) -> Result<String, JsValue> {273 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))274 self.manifest_with(YamlFormat::std_to_yaml(indent != 0, quote_keys))
229 }275 }
276 #[wasm_bindgen(js_name = manifestYamlStream)]
230 pub fn manifest_yaml_stream(277 pub fn manifest_yaml_stream(
231 &self,278 &self,
232 indent: u32,279 indent: u32,
238 c_document_end,285 c_document_end,
239 ))286 ))
240 }287 }
288 #[wasm_bindgen(js_name = manifestXmlJsonml)]
241 pub fn manifest_xml_jsonml(&self) -> Result<String, JsValue> {289 pub fn manifest_xml_jsonml(&self) -> Result<String, JsValue> {
242 self.manifest_with(XmlJsonmlFormat::std_to_xml())290 self.manifest_with(XmlJsonmlFormat::std_to_xml())
243 }291 }
292 #[wasm_bindgen(js_name = manifestToml)]
244 pub fn manifest_toml(&self, indent: u32) -> Result<String, JsValue> {293 pub fn manifest_toml(&self, indent: u32) -> Result<String, JsValue> {
245 self.manifest_with(TomlFormat::std_to_toml(" ".repeat(indent as usize)))294 self.manifest_with(TomlFormat::std_to_toml(" ".repeat(indent as usize)))
246 }295 }
296 #[wasm_bindgen(js_name = manifestIni)]
247 pub fn manifest_ini(&self) -> Result<String, JsValue> {297 pub fn manifest_ini(&self) -> Result<String, JsValue> {
248 self.manifest_with(IniFormat::std())298 self.manifest_with(IniFormat::std())
249 }299 }
250}300}
251301
302#[wasm_bindgen(js_name = ArrValue)]
303pub struct WasmArrValue {
304 arr: ArrValue,
305 state: Option<State>,
306}
307
308#[wasm_bindgen(js_class = ArrValue)]
309impl WasmArrValue {
310 #[wasm_bindgen(getter)]
311 pub fn length(&self) -> u32 {
312 self.arr.len()
313 }
314 pub fn at(&self, index: u32) -> Result<Option<WasmVal>, JsValue> {
315 let result = self.state.as_ref().map_or_else(
316 || self.arr.get(index),
317 |state| {
318 let _guard = state.try_enter();
319 self.arr.get(index)
320 },
321 );
322 result
323 .map(|opt: Option<Val>| {
324 opt.map(|v| WasmVal {
325 val: v,
326 state: self.state.clone(),
327 })
328 })
329 .map_err(|e| jrsonnet_js_error(&e))
330 }
331}
332
333#[wasm_bindgen(js_name = ObjValue)]
334pub struct WasmObjValue {
335 obj: ObjValue,
336 state: Option<State>,
337}
338
339#[wasm_bindgen(js_class = ObjValue)]
340impl WasmObjValue {
341 pub fn keys(&self) -> Vec<String> {
342 self.obj
343 .fields()
344 .into_iter()
345 .map(|s| s.to_string())
346 .collect()
347 }
348 pub fn get(&self, key: String) -> Result<Option<WasmVal>, JsValue> {
349 let result = if let Some(state) = &self.state {
350 let _guard = state.try_enter();
351 self.obj.get(key.into())
352 } else {
353 self.obj.get(key.into())
354 };
355 result
356 .map(|opt: Option<Val>| {
357 opt.map(|v| WasmVal {
358 val: v,
359 state: self.state.clone(),
360 })
361 })
362 .map_err(|e| jrsonnet_js_error(&e))
363 }
364}
365
252#[derive(Trace)]366#[derive(Trace)]
253struct JsHandler {367struct JsHandler {
254 #[trace(skip)]368 #[trace(skip)]
292 }406 }
293}407}
294408
295#[wasm_bindgen]409#[wasm_bindgen(js_name = State)]
296pub struct WasmState {410pub struct WasmState {
297 state: State,411 state: State,
298 resolver: JsAsyncResolver,412 resolver: Option<JsAsyncResolver>,
299}413}
300#[wasm_bindgen]414#[wasm_bindgen(js_class = State)]
301impl WasmState {415impl WasmState {
302 #[wasm_bindgen(constructor)]416 #[wasm_bindgen(constructor)]
303 pub fn new(resolver: ImportResolverJs) -> Self {417 pub fn new(resolver: Option<ImportResolverJs>) -> Self {
304 console_error_panic_hook::set_once();418 console_error_panic_hook::set_once();
305 let mut state = StateBuilder::default();419 let mut state = StateBuilder::default();
306 state.import_resolver(ResolvedImportResolver::new());420 state.import_resolver(ResolvedImportResolver::new());
309 let state = state.build();423 let state = state.build();
310 Self {424 Self {
311 state,425 state,
312 resolver: JsAsyncResolver { js: resolver },426 resolver: resolver.map(|js| JsAsyncResolver { js }),
313 }427 }
314 }428 }
315429
316 #[wasm_bindgen]430 #[wasm_bindgen(js_name = evaluateSnippet)]
317 pub fn evaluate_snippet(&self, name: &str, snippet: &str) -> Result<WasmVal, JsValue> {431 pub fn evaluate_snippet(&self, name: &str, snippet: &str) -> Result<WasmVal, JsValue> {
318 let _guard = self.state.enter();432 let _guard = self.state.enter();
319 self.state433 self.state
322 .map_err(|e| jrsonnet_js_error(&e))436 .map_err(|e| jrsonnet_js_error(&e))
323 }437 }
324438
439 #[wasm_bindgen(js_name = evaluateFile)]
325 pub async fn evaluate_file(&self, path: String) -> Result<WasmVal, JsValue> {440 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?;441 self.evaluate_file_from_impl(None, path).await
442 }
443
444 #[wasm_bindgen(js_name = evaluateFileFrom)]
445 pub async fn evaluate_file_from(&self, from: String, path: String) -> Result<WasmVal, JsValue> {
446 self.evaluate_file_from_impl(Some(from), path).await
447 }
448}
449
450impl WasmState {
451 async fn evaluate_file_from_impl(
452 &self,
453 from: Option<String>,
454 path: String,
455 ) -> Result<WasmVal, JsValue> {
456 let resolver = self
457 .resolver
458 .clone()
459 .ok_or_else(|| JsValue::from_str("file evaluation requires an ImportResolver"))?;
460 let from = match from {
461 Some(s) => {
462 let url = url::Url::parse(&s).map_err(|e| JsValue::from_str(&e.to_string()))?;
463 SourcePath::new(SourceUrl::new(url))
464 }
465 None => SourcePath::default(),
466 };
467 let path = async_import(self.state.clone(), resolver, &from, &path.as_str()).await?;
327 let _guard = self.state.enter();468 let _guard = self.state.enter();
328 self.state469 self.state
329 .import_resolved(path)470 .import_resolved(path)
375 ) -> Result<SourcePath, JsValue> {516 ) -> Result<SourcePath, JsValue> {
376 let from_js = (!from.is_default()).then(|| from.to_string());517 let from_js = (!from.is_default()).then(|| from.to_string());
377 let path_str = path.as_path().as_ref().to_string_lossy().into_owned();518 let path_str = path.as_path().as_ref().to_string_lossy().into_owned();
378 let promise = self.js.resolve_from(from_js, &path_str)?;519 let promise = self
520 .js
521 .resolve_from(from_js, &path_str)
522 .map_err(|e| js_resolver_error("resolveFrom", e))?;
379 let resolved_js = wasm_bindgen_futures::JsFuture::from(promise).await?;523 let resolved_js = wasm_bindgen_futures::JsFuture::from(promise)
524 .await
525 .map_err(|e| js_resolver_error("resolveFrom", e))?;
380 let resolved_str = resolved_js526 let resolved_str = resolved_js
381 .as_string()527 .as_string()
382 .ok_or_else(|| JsValue::from_str("resolveFrom must return string"))?;528 .ok_or_else(|| JsValue::from_str("resolveFrom must return string"))?;
386532
387 async fn load_file_contents(&self, resolved: &SourcePath) -> Result<Vec<u8>, JsValue> {533 async fn load_file_contents(&self, resolved: &SourcePath) -> Result<Vec<u8>, JsValue> {
388 let resolved_str = resolved.to_string();534 let resolved_str = resolved.to_string();
389 let promise = self.js.load_file_contents(&resolved_str)?;535 let promise = self
536 .js
537 .load_file_contents(&resolved_str)
538 .map_err(|e| js_resolver_error("loadFileContents", e))?;
390 let bytes_js = wasm_bindgen_futures::JsFuture::from(promise).await?;539 let bytes_js = wasm_bindgen_futures::JsFuture::from(promise)
540 .await
541 .map_err(|e| js_resolver_error("loadFileContents", e))?;
391 let arr = bytes_js542 let arr = bytes_js
392 .dyn_into::<js_sys::Uint8Array>()543 .dyn_into::<js_sys::Uint8Array>()
393 .map_err(|_| JsValue::from_str("loadFileContents must return Uint8Array"))?;544 .map_err(|_| JsValue::from_str("loadFileContents must return Uint8Array"))?;
394 Ok(arr.to_vec())545 Ok(arr.to_vec())
395 }546 }
396}547}
397548
398#[wasm_bindgen]549#[wasm_bindgen(js_name = FormatOptions)]
399pub struct WasmFormatOptions {}550pub struct WasmFormatOptions {
551 indent: u8,
552}
400#[wasm_bindgen]553#[wasm_bindgen(js_class = FormatOptions)]
401impl WasmFormatOptions {554impl WasmFormatOptions {
402 #[wasm_bindgen(constructor)]555 #[wasm_bindgen(constructor)]
403 pub fn new() -> Self {556 pub fn new() -> Self {
404 Self {}557 Self { indent: 0 }
405 }558 }
406559
407 fn build(&self) -> FormatOptions {560 fn build(&self) -> FormatOptions {
408 FormatOptions { indent: 0 }561 FormatOptions {
562 indent: self.indent,
563 }
564 }
565}
566
567impl Default for WasmFormatOptions {
568 fn default() -> Self {
569 Self::new()
409 }570 }
410}571}
411572