difftreelog
refactor(web) idiomatic js api
in: master
10 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1119,6 +1119,7 @@
"jrsonnet-stdlib",
"jrsonnet-types",
"js-sys",
+ "rustc-hash 2.1.2",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
README.adocdiffbeforeafterboth--- a/README.adoc
+++ b/README.adoc
@@ -88,6 +88,21 @@
Jrsonnet is written in rust itself, so just add it as dependency
+=== JavaScript/TypeScript (WASM)
+
+// image:https://img.shields.io/npm/v/jrsonnet[alt=npm, link=https://www.npmjs.com/package/jrsonnet]
+image:https://jsr.io/badges/@jrsonnet/jrsonnet[alt=JSR, link=https://jsr.io/@jrsonnet/jrsonnet]
+image:https://jsr.io/badges/@jrsonnet/jrsonnet/score[alt=JSR score, link=https://jsr.io/@jrsonnet/jrsonnet]
+
+WASM bindings are published to JSR as `@jrsonnet/jrsonnet`.
+
+[source,sh]
+----
+deno add jsr:@jrsonnet/jrsonnet
+----
+
+// npm package (`jrsonnet`) is not yet published.
+
=== Python
image:https://img.shields.io/pypi/v/rjsonnet[alt=crates.io, link=https://pypi.org/project/rjsonnet/]
@@ -100,6 +115,6 @@
=== Other
-WASM bingings are also available, Java bindings (Both JNI and WASM compiled to `.class`) are in progress
+Java bindings (Both JNI and WASM compiled to `.class`) are in progress
See link:./bindings/[bindings] for more information.
bindings/jrsonnet-web/Cargo.tomldiffbeforeafterboth--- a/bindings/jrsonnet-web/Cargo.toml
+++ b/bindings/jrsonnet-web/Cargo.toml
@@ -1,6 +1,8 @@
[package]
name = "jrsonnet-web"
-description = "WASM JS bindings for jrsonnet"
+description = "WebAssembly JavaScript bindings for jrsonnet"
+keywords = ["jsonnet", "wasm", "web"]
+categories = ["wasm"]
authors.workspace = true
edition.workspace = true
license.workspace = true
@@ -17,6 +19,7 @@
jrsonnet-stdlib.workspace = true
jrsonnet-types.workspace = true
js-sys.workspace = true
+rustc-hash.workspace = true
url.workspace = true
wasm-bindgen.workspace = true
wasm-bindgen-futures.workspace = true
bindings/jrsonnet-web/deno.jsondiffbeforeafterboth--- a/bindings/jrsonnet-web/deno.json
+++ b/bindings/jrsonnet-web/deno.json
@@ -1,7 +1,7 @@
{
"name": "@jrsonnet/jrsonnet",
"license": "MIT",
- "version": "0.0.1",
+ "version": "0.0.2",
"tasks": {
"wasmbuild": "deno run -A @deno/wasmbuild -p jrsonnet-web --skip-opt"
},
@@ -11,11 +11,15 @@
"@std/assert": "jsr:@std/assert@^1.0.19"
},
"exports": {
- ".": "./mod.ts"
+ ".": "./mod.ts",
+ "./fmt": "./fmt.ts"
},
"publish": {
"exclude": [
"!lib"
]
+ },
+ "fmt": {
+ "useTabs": true
}
}
bindings/jrsonnet-web/fmt.test.tsdiffbeforeafterboth--- a/bindings/jrsonnet-web/fmt.test.ts
+++ b/bindings/jrsonnet-web/fmt.test.ts
@@ -1,7 +1,7 @@
import { assertEquals } from "@std/assert";
-import { format, FormatOptions } from "./mod.ts";
+import { format, FormatOptions } from "./fmt.ts";
Deno.test("format", () => {
- const opts = new FormatOptions();
- assertEquals(format("{a:1+1}", opts), "{ a: 1 + 1 }\n");
+ const opts = new FormatOptions();
+ assertEquals(format("{a:1+1}", opts), "{ a: 1 + 1 }\n");
});
bindings/jrsonnet-web/fmt.tsdiffbeforeafterboth--- a/bindings/jrsonnet-web/fmt.ts
+++ b/bindings/jrsonnet-web/fmt.ts
@@ -0,0 +1 @@
+export { format, FormatOptions } from "./lib/jsonnet_web.js";
bindings/jrsonnet-web/mod.test.tsdiffbeforeafterboth1import { 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";34Deno.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});1011Deno.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});1718Deno.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});2425Deno.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});3314Deno.test("basic", () => {32Deno.test("Val.asArr exposes ArrValue", () => {5 const state = new WasmState();33 const state = new State();67 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});4041Deno.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});5051Deno.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});5657Deno.test("evaluateSnippet propagates jsonnet errors", () => {58 const state = new State();59 assertThrows(() => state.evaluateSnippet("test.jsonnet", "error 'boom'"));60});6162Deno.test("evaluateFile without resolver rejects", async () => {63 const state = new State();64 await assertRejects(() => state.evaluateFile("anything.jsonnet"));65});6667Deno.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});8889Deno.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});101102Deno.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});110111Deno.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});116117Deno.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});9140bindings/jrsonnet-web/mod.tsdiffbeforeafterboth--- a/bindings/jrsonnet-web/mod.ts
+++ b/bindings/jrsonnet-web/mod.ts
@@ -1,60 +1,71 @@
import { assert } from "@std/assert";
import {
- format as formatRaw,
- type ImportResolver,
- WasmFormatOptions,
- WasmState,
- WasmVal,
+ ArrValue,
+ type ImportResolver,
+ ObjValue,
+ setErrorFactory,
+ State,
+ Val,
+ ValKind,
} from "./lib/jsonnet_web.js";
-export { type ImportResolver, WasmFormatOptions, WasmState, WasmVal };
+export interface JrsonnetFrame {
+ desc: string;
+ path?: string;
+ line?: number;
+ column?: number;
+}
-class FetchImportResolver implements ImportResolver {
- constructor(public base: string) {}
+export class JrsonnetError extends Error {
+ override name = "JrsonnetError" as const;
+ frames: JrsonnetFrame[];
- resolution = new Map<string, URL>();
- response = new Map<string, Response>();
+ constructor(message: string, frames: JrsonnetFrame[], cause?: unknown) {
+ super(message, cause !== undefined ? { cause } : undefined);
+ this.frames = frames;
+ }
+}
- async resolveFrom(from: string | undefined, path: string): Promise<string> {
- let resolved: URL;
- if (from) {
- resolved = new URL(path, from);
- } else {
- resolved = new URL(path, this.base);
- }
- const resolvingStr = resolved.toString();
- resolved = this.resolution.get(resolvingStr) ?? resolved;
+setErrorFactory(
+ (message: string, frames: JrsonnetFrame[], cause: unknown) =>
+ new JrsonnetError(message, frames, cause),
+);
+
+export { ArrValue, type ImportResolver, ObjValue, State, Val, ValKind };
- const resolvedStr = resolved.toString();
- if (!this.response.has(resolvedStr)) {
- console.log(resolved);
- const v = await fetch(resolved);
- this.response.set(resolvedStr, v);
- resolved = new URL(v.url);
- this.resolution.set(resolvingStr, resolved);
- }
- return resolved.toString();
- }
- loadFileContents(resolved: string): Promise<Uint8Array> {
- console.log(resolved);
- const v = this.response.get(resolved);
- assert(v, "should be resolved");
- return v.bytes();
- }
-}
+export class FetchImportResolver implements ImportResolver {
+ constructor(base: URL | string) {
+ this.#base = new URL(base);
+ }
-//
-// try {
-// console.log("eval file");
-// await state.evaluate_file("example.jsonnet");
-// console.log("eval file done");
-// } catch (e) {
-// console.log(e);
-// }
-//
-export function format(
- code: string,
- opts: WasmFormatOptions = new WasmFormatOptions(),
-): string {
- return formatRaw(code, opts);
+ #base: URL;
+ #resolution = new Map<string, string>();
+ #bytes = new Map<string, Uint8Array>();
+
+ async resolveFrom(from: string | undefined, path: string): Promise<string> {
+ const base = from !== undefined ? from : this.#base;
+ const requestStr = new URL(path, base).toString();
+
+ const cached = this.#resolution.get(requestStr);
+ if (cached !== undefined) return cached;
+
+ const resp = await fetch(requestStr);
+ if (!resp.ok) {
+ throw new Error(
+ `fetch ${requestStr}: HTTP ${resp.status} ${resp.statusText}`,
+ );
+ }
+ const canonical = resp.url;
+ if (!this.#bytes.has(canonical)) {
+ this.#bytes.set(canonical, await resp.bytes());
+ }
+ this.#resolution.set(requestStr, canonical);
+ return canonical;
+ }
+
+ loadFileContents(resolved: string): Promise<Uint8Array> {
+ const bytes = this.#bytes.get(resolved);
+ assert(bytes, `not loaded: ${resolved}`);
+ return Promise.resolve(bytes);
+ }
}
bindings/jrsonnet-web/scripts/build_npm.tsdiffbeforeafterboth--- a/bindings/jrsonnet-web/scripts/build_npm.ts
+++ b/bindings/jrsonnet-web/scripts/build_npm.ts
@@ -3,27 +3,27 @@
await emptyDir("./npm");
await build({
- entryPoints: ["./mod.ts"],
- outDir: "./npm",
- shims: {
- // see JS docs for overview and more options
- deno: true,
- },
- package: {
- // package.json properties
- name: "jrsonnet",
- version: Deno.args[0],
- description: "Jrsonnet.",
- license: "MIT",
- repository: {
- type: "git",
- url: "git+https://github.com/CertainLach/jrsonnet.git",
- },
- bugs: {
- url: "https://github.com/CertainLach/jrsonnet/issues",
- },
- },
- postBuild() {
- Deno.copyFileSync("../../LICENSE", "npm/LICENSE");
- },
+ entryPoints: ["./mod.ts"],
+ outDir: "./npm",
+ shims: {
+ // see JS docs for overview and more options
+ deno: true,
+ },
+ package: {
+ // package.json properties
+ name: "jrsonnet",
+ version: Deno.args[0],
+ description: "Jrsonnet.",
+ license: "MIT",
+ repository: {
+ type: "git",
+ url: "git+https://github.com/CertainLach/jrsonnet.git",
+ },
+ bugs: {
+ url: "https://github.com/CertainLach/jrsonnet/issues",
+ },
+ },
+ postBuild() {
+ Deno.copyFileSync("../../LICENSE", "npm/LICENSE");
+ },
});
bindings/jrsonnet-web/src/lib.rsdiffbeforeafterboth--- a/bindings/jrsonnet-web/src/lib.rs
+++ b/bindings/jrsonnet-web/src/lib.rs
@@ -1,19 +1,25 @@
-use std::result::Result;
+#![allow(clippy::future_not_send, reason = "we work with js promises anyway")]
+use std::{cell::RefCell, result::Result};
+
use jrsonnet_evaluator::{
- NumValue, Result as JrResult, SourcePath, SourceUrl, State, StateBuilder, Val,
+ IStr, NumValue, ObjValue, Result as JrResult, SourcePath, SourceUrl, State, StateBuilder, Val,
async_import::{ResolvedImportResolver, async_import},
error,
function::builtin::{NativeCallback, NativeCallbackHandler},
manifest::{JsonFormat, ManifestFormat, StringFormat, ToStringFormat, YamlStreamFormat},
- trace::{JsFormat, PathResolver, TraceFormat},
+ tla::{TlaArg, apply_tla},
+ trace::PathResolver,
+ val::ArrValue,
with_state,
};
use jrsonnet_formatter::FormatOptions;
use jrsonnet_gcmodule::Trace;
use jrsonnet_stdlib::{IniFormat, TomlFormat, XmlJsonmlFormat, YamlFormat};
use jrsonnet_types::ValType;
-use wasm_bindgen::prelude::*;
+use js_sys::Reflect::get;
+use rustc_hash::FxHashMap;
+use wasm_bindgen::{convert::RefFromWasmAbi, prelude::*};
#[wasm_bindgen]
#[derive(Clone, Copy)]
@@ -27,37 +33,60 @@
Func,
}
-#[wasm_bindgen(inline_js = r"
-export class JrsonnetError extends Error {
- constructor(message, frames) {
- super(message);
- this.name = 'JrsonnetError';
- this.frames = frames;
- }
+thread_local! {
+ static ERR_FACTORY: RefCell<Option<js_sys::Function>> = const { RefCell::new(None) };
}
-export function makeJrsonnetError(message, frames) {
- return new JrsonnetError(message, frames);
+#[wasm_bindgen(js_name = setErrorFactory)]
+pub fn set_error_factory(f: js_sys::Function) {
+ ERR_FACTORY.with(|c| *c.borrow_mut() = Some(f));
}
-")]
-extern "C" {
- #[wasm_bindgen(js_name = makeJrsonnetError)]
- fn make_jrsonnet_error(message: &str, frames: js_sys::Array) -> JsValue;
+fn make_jrsonnet_error(message: &str, frames: js_sys::Array, cause: &JsValue) -> JsValue {
+ ERR_FACTORY.with(|c| {
+ c.borrow().as_ref().map_or_else(
+ || js_sys::Error::new(message).into(),
+ |f| {
+ let args = js_sys::Array::new();
+ args.push(&JsValue::from_str(message));
+ args.push(&frames);
+ args.push(cause);
+ f.apply(&JsValue::NULL, &args)
+ .unwrap_or_else(|e| js_sys::Error::new(&format!("{e:?}")).into())
+ },
+ )
+ })
}
-#[wasm_bindgen(typescript_custom_section)]
-const TS_JRSONNET_ERROR: &'static str = r"
-export interface JrsonnetFrame {
- desc: string;
- path?: string;
- line?: number;
- column?: number;
+fn js_error_message(e: &JsValue) -> String {
+ e.dyn_ref::<js_sys::Error>().map_or_else(
+ || e.as_string().unwrap_or_else(|| format!("{e:?}")),
+ |err| String::from(err.message()),
+ )
}
-export class JrsonnetError extends Error {
- name: 'JrsonnetError';
- frames: JrsonnetFrame[];
+
+fn unwrap_val_ref(value: &JsValue) -> Result<<WasmVal as RefFromWasmAbi>::Anchor, JsValue> {
+ let ptr = get(value, &JsValue::from_str("__wbg_ptr"))
+ .ok()
+ .and_then(|v| v.as_f64())
+ .ok_or_else(|| JsValue::from_str("expected a Val instance"))? as u32;
+ if ptr == 0 {
+ return Err(JsValue::from_str("Val has been freed"));
+ }
+ Ok(unsafe { <WasmVal as RefFromWasmAbi>::ref_from_abi(ptr) })
}
-";
+fn js_resolver_error(prefix: &str, e: JsValue) -> JsValue {
+ let msg = format!("{prefix}: {}", js_error_message(&e));
+ let frames = js_sys::Array::new();
+ let frame = js_sys::Object::new();
+ let _ = js_sys::Reflect::set(
+ &frame,
+ &JsValue::from_str("desc"),
+ &JsValue::from_str(prefix),
+ );
+ frames.push(&frame);
+ make_jrsonnet_error(&msg, frames, &e)
+}
+
fn jrsonnet_js_error(e: &jrsonnet_evaluator::Error) -> JsValue {
let msg = e.error().to_string();
// let msg = format.format(e).unwrap_or_else(|_| e.to_string());
@@ -90,7 +119,7 @@
}
frames.push(&frame);
}
- make_jrsonnet_error(&msg, frames)
+ make_jrsonnet_error(&msg, frames, &JsValue::UNDEFINED)
}
impl From<ValType> for ValKind {
@@ -107,7 +136,7 @@
}
}
-#[wasm_bindgen]
+#[wasm_bindgen(js_name = Val)]
pub struct WasmVal {
val: Val,
state: Option<State>,
@@ -121,12 +150,6 @@
Self {
val,
state: Some(state),
- }
- }
- fn child(&self, val: Val) -> Self {
- Self {
- val,
- state: self.state.clone(),
}
}
fn run<R>(&self, f: impl FnOnce(&Val) -> R) -> R {
@@ -143,7 +166,7 @@
}
}
-#[wasm_bindgen]
+#[wasm_bindgen(js_class = Val)]
impl WasmVal {
pub fn null() -> Self {
Self::new(Val::Null)
@@ -167,7 +190,7 @@
pub fn func(
params: Vec<String>,
- #[wasm_bindgen(unchecked_param_type = "(...args: WasmVal[]) => WasmVal")]
+ #[wasm_bindgen(unchecked_param_type = "(...args: Val[]) => Val")]
callback: js_sys::Function,
) -> Self {
#[allow(deprecated)]
@@ -181,52 +204,76 @@
pub fn kind(&self) -> ValKind {
self.val.value_type().into()
}
+ #[wasm_bindgen(js_name = asBool)]
pub fn as_bool(&self) -> Option<bool> {
self.val.as_bool()
}
+ #[wasm_bindgen(js_name = asNum)]
pub fn as_num(&self) -> Option<f64> {
self.val.as_num()
}
+ #[wasm_bindgen(js_name = asString)]
pub fn as_string(&self) -> Option<String> {
self.val.as_str().map(|s| s.to_string())
- }
- pub fn arr_len(&self) -> Option<u32> {
- self.val.as_arr().map(|a| a.len())
}
- pub fn arr_at(&self, index: u32) -> Result<Option<WasmVal>, JsValue> {
- let Some(a) = self.val.as_arr() else {
- return Ok(None);
- };
- self.run(|_| a.get(index))
- .map(|opt| opt.map(|v| self.child(v)))
- .map_err(|e| jrsonnet_js_error(&e))
+ #[wasm_bindgen(js_name = asArr)]
+ pub fn as_arr(&self) -> Option<WasmArrValue> {
+ self.val.as_arr().map(|arr| WasmArrValue {
+ arr,
+ state: self.state.clone(),
+ })
}
- pub fn obj_keys(&self) -> Option<Vec<String>> {
- self.val
- .as_obj()
- .map(|o| o.fields().into_iter().map(|s| s.to_string()).collect())
+ #[wasm_bindgen(js_name = asObj)]
+ pub fn as_obj(&self) -> Option<WasmObjValue> {
+ self.val.as_obj().map(|obj| WasmObjValue {
+ obj,
+ state: self.state.clone(),
+ })
}
- pub fn obj_get(&self, key: String) -> Result<Option<WasmVal>, JsValue> {
- let Some(o) = self.val.as_obj() else {
- return Ok(None);
- };
- self.run(|_| o.get(key.into()))
- .map(|opt| opt.map(|v| self.child(v)))
+
+ #[wasm_bindgen(js_name = applyTla)]
+ pub fn apply_tla(
+ &self,
+ #[wasm_bindgen(unchecked_param_type = "Record<string, Val>")] args: &js_sys::Object,
+ ) -> Result<WasmVal, JsValue> {
+ let mut map: FxHashMap<IStr, TlaArg> = FxHashMap::default();
+ for entry in js_sys::Object::entries(args).iter() {
+ let pair: js_sys::Array = entry
+ .dyn_into()
+ .map_err(|_| JsValue::from_str("expected [key, value] entry"))?;
+ let key = pair
+ .get(0)
+ .as_string()
+ .ok_or_else(|| JsValue::from_str("TLA arg key must be a string"))?;
+ let value = unwrap_val_ref(&pair.get(1))?;
+ map.insert(key.into(), TlaArg::Val(value.val.clone()));
+ }
+ let val = self.val.clone();
+ self.run(|_| apply_tla(&map, val))
+ .map(|v| WasmVal {
+ val: v,
+ state: self.state.clone(),
+ })
.map_err(|e| jrsonnet_js_error(&e))
}
+ #[wasm_bindgen(js_name = manifestJson)]
pub fn manifest_json(&self, indent: u32) -> Result<String, JsValue> {
self.manifest_with(JsonFormat::cli(indent as usize))
}
+ #[wasm_bindgen(js_name = manifestToString)]
pub fn manifest_to_string(&self) -> Result<String, JsValue> {
self.manifest_with(ToStringFormat)
}
+ #[wasm_bindgen(js_name = manifestString)]
pub fn manifest_string(&self) -> Result<String, JsValue> {
self.manifest_with(StringFormat)
}
+ #[wasm_bindgen(js_name = manifestYaml)]
pub fn manifest_yaml(&self, indent: u32, quote_keys: bool) -> Result<String, JsValue> {
self.manifest_with(YamlFormat::std_to_yaml(indent != 0, quote_keys))
}
+ #[wasm_bindgen(js_name = manifestYamlStream)]
pub fn manifest_yaml_stream(
&self,
indent: u32,
@@ -238,17 +285,84 @@
c_document_end,
))
}
+ #[wasm_bindgen(js_name = manifestXmlJsonml)]
pub fn manifest_xml_jsonml(&self) -> Result<String, JsValue> {
self.manifest_with(XmlJsonmlFormat::std_to_xml())
}
+ #[wasm_bindgen(js_name = manifestToml)]
pub fn manifest_toml(&self, indent: u32) -> Result<String, JsValue> {
self.manifest_with(TomlFormat::std_to_toml(" ".repeat(indent as usize)))
}
+ #[wasm_bindgen(js_name = manifestIni)]
pub fn manifest_ini(&self) -> Result<String, JsValue> {
self.manifest_with(IniFormat::std())
}
}
+#[wasm_bindgen(js_name = ArrValue)]
+pub struct WasmArrValue {
+ arr: ArrValue,
+ state: Option<State>,
+}
+
+#[wasm_bindgen(js_class = ArrValue)]
+impl WasmArrValue {
+ #[wasm_bindgen(getter)]
+ pub fn length(&self) -> u32 {
+ self.arr.len()
+ }
+ pub fn at(&self, index: u32) -> Result<Option<WasmVal>, JsValue> {
+ let result = self.state.as_ref().map_or_else(
+ || self.arr.get(index),
+ |state| {
+ let _guard = state.try_enter();
+ self.arr.get(index)
+ },
+ );
+ result
+ .map(|opt: Option<Val>| {
+ opt.map(|v| WasmVal {
+ val: v,
+ state: self.state.clone(),
+ })
+ })
+ .map_err(|e| jrsonnet_js_error(&e))
+ }
+}
+
+#[wasm_bindgen(js_name = ObjValue)]
+pub struct WasmObjValue {
+ obj: ObjValue,
+ state: Option<State>,
+}
+
+#[wasm_bindgen(js_class = ObjValue)]
+impl WasmObjValue {
+ pub fn keys(&self) -> Vec<String> {
+ self.obj
+ .fields()
+ .into_iter()
+ .map(|s| s.to_string())
+ .collect()
+ }
+ pub fn get(&self, key: String) -> Result<Option<WasmVal>, JsValue> {
+ let result = if let Some(state) = &self.state {
+ let _guard = state.try_enter();
+ self.obj.get(key.into())
+ } else {
+ self.obj.get(key.into())
+ };
+ result
+ .map(|opt: Option<Val>| {
+ opt.map(|v| WasmVal {
+ val: v,
+ state: self.state.clone(),
+ })
+ })
+ .map_err(|e| jrsonnet_js_error(&e))
+ }
+}
+
#[derive(Trace)]
struct JsHandler {
#[trace(skip)]
@@ -292,15 +406,15 @@
}
}
-#[wasm_bindgen]
+#[wasm_bindgen(js_name = State)]
pub struct WasmState {
state: State,
- resolver: JsAsyncResolver,
+ resolver: Option<JsAsyncResolver>,
}
-#[wasm_bindgen]
+#[wasm_bindgen(js_class = State)]
impl WasmState {
#[wasm_bindgen(constructor)]
- pub fn new(resolver: ImportResolverJs) -> Self {
+ pub fn new(resolver: Option<ImportResolverJs>) -> Self {
console_error_panic_hook::set_once();
let mut state = StateBuilder::default();
state.import_resolver(ResolvedImportResolver::new());
@@ -309,11 +423,11 @@
let state = state.build();
Self {
state,
- resolver: JsAsyncResolver { js: resolver },
+ resolver: resolver.map(|js| JsAsyncResolver { js }),
}
}
- #[wasm_bindgen]
+ #[wasm_bindgen(js_name = evaluateSnippet)]
pub fn evaluate_snippet(&self, name: &str, snippet: &str) -> Result<WasmVal, JsValue> {
let _guard = self.state.enter();
self.state
@@ -322,8 +436,35 @@
.map_err(|e| jrsonnet_js_error(&e))
}
+ #[wasm_bindgen(js_name = evaluateFile)]
pub async fn evaluate_file(&self, path: String) -> Result<WasmVal, JsValue> {
- let path = async_import(self.state.clone(), self.resolver.clone(), &path.as_str()).await?;
+ self.evaluate_file_from_impl(None, path).await
+ }
+
+ #[wasm_bindgen(js_name = evaluateFileFrom)]
+ pub async fn evaluate_file_from(&self, from: String, path: String) -> Result<WasmVal, JsValue> {
+ self.evaluate_file_from_impl(Some(from), path).await
+ }
+}
+
+impl WasmState {
+ async fn evaluate_file_from_impl(
+ &self,
+ from: Option<String>,
+ path: String,
+ ) -> Result<WasmVal, JsValue> {
+ let resolver = self
+ .resolver
+ .clone()
+ .ok_or_else(|| JsValue::from_str("file evaluation requires an ImportResolver"))?;
+ let from = match from {
+ Some(s) => {
+ let url = url::Url::parse(&s).map_err(|e| JsValue::from_str(&e.to_string()))?;
+ SourcePath::new(SourceUrl::new(url))
+ }
+ None => SourcePath::default(),
+ };
+ let path = async_import(self.state.clone(), resolver, &from, &path.as_str()).await?;
let _guard = self.state.enter();
self.state
.import_resolved(path)
@@ -375,8 +516,13 @@
) -> Result<SourcePath, JsValue> {
let from_js = (!from.is_default()).then(|| from.to_string());
let path_str = path.as_path().as_ref().to_string_lossy().into_owned();
- let promise = self.js.resolve_from(from_js, &path_str)?;
- let resolved_js = wasm_bindgen_futures::JsFuture::from(promise).await?;
+ let promise = self
+ .js
+ .resolve_from(from_js, &path_str)
+ .map_err(|e| js_resolver_error("resolveFrom", e))?;
+ let resolved_js = wasm_bindgen_futures::JsFuture::from(promise)
+ .await
+ .map_err(|e| js_resolver_error("resolveFrom", e))?;
let resolved_str = resolved_js
.as_string()
.ok_or_else(|| JsValue::from_str("resolveFrom must return string"))?;
@@ -386,8 +532,13 @@
async fn load_file_contents(&self, resolved: &SourcePath) -> Result<Vec<u8>, JsValue> {
let resolved_str = resolved.to_string();
- let promise = self.js.load_file_contents(&resolved_str)?;
- let bytes_js = wasm_bindgen_futures::JsFuture::from(promise).await?;
+ let promise = self
+ .js
+ .load_file_contents(&resolved_str)
+ .map_err(|e| js_resolver_error("loadFileContents", e))?;
+ let bytes_js = wasm_bindgen_futures::JsFuture::from(promise)
+ .await
+ .map_err(|e| js_resolver_error("loadFileContents", e))?;
let arr = bytes_js
.dyn_into::<js_sys::Uint8Array>()
.map_err(|_| JsValue::from_str("loadFileContents must return Uint8Array"))?;
@@ -395,17 +546,27 @@
}
}
-#[wasm_bindgen]
-pub struct WasmFormatOptions {}
-#[wasm_bindgen]
+#[wasm_bindgen(js_name = FormatOptions)]
+pub struct WasmFormatOptions {
+ indent: u8,
+}
+#[wasm_bindgen(js_class = FormatOptions)]
impl WasmFormatOptions {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
- Self {}
+ Self { indent: 0 }
}
fn build(&self) -> FormatOptions {
- FormatOptions { indent: 0 }
+ FormatOptions {
+ indent: self.indent,
+ }
+ }
+}
+
+impl Default for WasmFormatOptions {
+ fn default() -> Self {
+ Self::new()
}
}