difftreelog
feat more formatter options
in: master
3 files changed
bindings/jrsonnet-web/src/lib.rsdiffbeforeafterboth1#![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 let ptr = get(value, &JsValue::from_str("__wbg_ptr"))69 .ok()70 .and_then(|v| v.as_f64())71 .ok_or_else(|| JsValue::from_str("expected a Val instance"))? as u32;72 if ptr == 0 {73 return Err(JsValue::from_str("Val has been freed"));74 }75 Ok(unsafe { <WasmVal as RefFromWasmAbi>::ref_from_abi(ptr) })76}7778fn js_resolver_error(prefix: &str, e: JsValue) -> JsValue {79 let msg = format!("{prefix}: {}", js_error_message(&e));80 let frames = js_sys::Array::new();81 let frame = js_sys::Object::new();82 let _ = js_sys::Reflect::set(83 &frame,84 &JsValue::from_str("desc"),85 &JsValue::from_str(prefix),86 );87 frames.push(&frame);88 make_jrsonnet_error(&msg, frames, &e)89}9091fn jrsonnet_js_error(e: &jrsonnet_evaluator::Error) -> JsValue {92 let msg = e.error().to_string();93 // let msg = format.format(e).unwrap_or_else(|_| e.to_string());94 let frames = js_sys::Array::new();95 for el in &e.trace().0 {96 let frame = js_sys::Object::new();97 let _ = js_sys::Reflect::set(98 &frame,99 &JsValue::from_str("desc"),100 &JsValue::from_str(&el.desc),101 );102 if let Some(loc) = &el.location {103 let path = loc.0.source_path().to_string();104 let _ = js_sys::Reflect::set(105 &frame,106 &JsValue::from_str("path"),107 &JsValue::from_str(&path),108 );109 let mapped = loc.0.map_source_locations(&[loc.1, loc.2]);110 let _ = js_sys::Reflect::set(111 &frame,112 &JsValue::from_str("line"),113 &JsValue::from(mapped[0].line),114 );115 let _ = js_sys::Reflect::set(116 &frame,117 &JsValue::from_str("column"),118 &JsValue::from(mapped[0].column),119 );120 }121 frames.push(&frame);122 }123 make_jrsonnet_error(&msg, frames, &JsValue::UNDEFINED)124}125126impl From<ValType> for ValKind {127 fn from(v: ValType) -> Self {128 match v {129 ValType::Null => Self::Null,130 ValType::Bool => Self::Bool,131 ValType::Num => Self::Num,132 ValType::Str => Self::Str,133 ValType::Arr => Self::Arr,134 ValType::Obj => Self::Obj,135 ValType::Func => Self::Func,136 #[cfg(feature = "exp-bigint")]137 ValType::BigInt => Self::BigInt,138 }139 }140}141142#[wasm_bindgen(js_name = Val)]143pub struct WasmVal {144 val: Val,145 state: Option<State>,146}147148impl WasmVal {149 fn new(val: Val) -> Self {150 Self { val, state: None }151 }152 fn with_state(val: Val, state: State) -> Self {153 Self {154 val,155 state: Some(state),156 }157 }158 fn run<R>(&self, f: impl FnOnce(&Val) -> R) -> R {159 if let Some(state) = &self.state {160 let _guard = state.try_enter();161 f(&self.val)162 } else {163 f(&self.val)164 }165 }166 fn manifest_with(&self, format: impl ManifestFormat) -> Result<String, JsValue> {167 self.run(|v| v.manifest(format))168 .map_err(|e| jrsonnet_js_error(&e))169 }170}171172#[wasm_bindgen(js_class = Val)]173impl WasmVal {174 pub fn null() -> Self {175 Self::new(Val::Null)176 }177 pub fn bool(b: bool) -> Self {178 Self::new(Val::Bool(b))179 }180 pub fn num(n: f64) -> Result<Self, JsError> {181 let n = NumValue::new(n)182 .ok_or_else(|| JsError::new("only finite numbers are supported by jsonnet"))?;183 Ok(Self::new(Val::num(n)))184 }185 pub fn string(s: String) -> Self {186 Self::new(Val::string(s))187 }188 pub fn bigint(value: js_sys::BigInt) -> Result<Self, JsError> {189 #[cfg(feature = "exp-bigint")]190 {191 let s: String = value192 .to_string(10)193 .map_err(|_| JsError::new("invalid bigint"))?194 .into();195 let bi = s196 .parse::<num_bigint::BigInt>()197 .map_err(|e| JsError::new(&format!("failed to parse bigint: {e}")))?;198 Ok(Self::new(Val::BigInt(Box::new(bi))))199 }200 #[cfg(not(feature = "exp-bigint"))]201 {202 let _ = value;203 Err(JsError::new(204 "bigint support is not enabled in this build (exp-bigint feature)",205 ))206 }207 }208 pub fn arr(items: Vec<WasmVal>) -> Self {209 Self::new(Val::arr(210 items.into_iter().map(|v| v.val).collect::<Vec<_>>(),211 ))212 }213 pub fn func(214 params: Vec<String>,215216 #[wasm_bindgen(unchecked_param_type = "(...args: Val[]) => Val")]217 callback: js_sys::Function,218 ) -> Self {219 #[allow(deprecated)]220 Self::new(Val::function(NativeCallback::new(221 params,222 JsHandler { func: callback },223 )))224 }225226 #[wasm_bindgen(getter)]227 pub fn kind(&self) -> ValKind {228 self.val.value_type().into()229 }230 #[wasm_bindgen(js_name = asBool)]231 pub fn as_bool(&self) -> Option<bool> {232 self.val.as_bool()233 }234 #[wasm_bindgen(js_name = asNum)]235 pub fn as_num(&self) -> Option<f64> {236 self.val.as_num()237 }238 #[wasm_bindgen(js_name = asBigint)]239 pub fn as_bigint(&self) -> Result<Option<js_sys::BigInt>, JsError> {240 #[cfg(feature = "exp-bigint")]241 {242 let Some(bi) = self.val.as_bigint() else {243 return Ok(None);244 };245 let big = js_sys::BigInt::new(&JsValue::from_str(&bi.to_string()))246 .map_err(|e| JsError::new(&format!("{e:?}")))?;247 Ok(Some(big))248 }249 #[cfg(not(feature = "exp-bigint"))]250 {251 Err(JsError::new(252 "bigint support is not enabled in this build (exp-bigint feature)",253 ))254 }255 }256 #[wasm_bindgen(js_name = asString)]257 pub fn as_string(&self) -> Option<String> {258 self.val.as_str().map(|s| s.to_string())259 }260 #[wasm_bindgen(js_name = asArr)]261 pub fn as_arr(&self) -> Option<WasmArrValue> {262 self.val.as_arr().map(|arr| WasmArrValue {263 arr,264 state: self.state.clone(),265 })266 }267 #[wasm_bindgen(js_name = asObj)]268 pub fn as_obj(&self) -> Option<WasmObjValue> {269 self.val.as_obj().map(|obj| WasmObjValue {270 obj,271 state: self.state.clone(),272 })273 }274275 #[wasm_bindgen(js_name = applyTla)]276 pub fn apply_tla(277 &self,278 #[wasm_bindgen(unchecked_param_type = "Record<string, Val>")] args: &js_sys::Object,279 ) -> Result<WasmVal, JsValue> {280 let mut map: FxHashMap<IStr, TlaArg> = FxHashMap::default();281 for entry in js_sys::Object::entries(args).iter() {282 let pair: js_sys::Array = entry283 .dyn_into()284 .map_err(|_| JsValue::from_str("expected [key, value] entry"))?;285 let key = pair286 .get(0)287 .as_string()288 .ok_or_else(|| JsValue::from_str("TLA arg key must be a string"))?;289 let value = unwrap_val_ref(&pair.get(1))?;290 map.insert(key.into(), TlaArg::Val(value.val.clone()));291 }292 let val = self.val.clone();293 self.run(|_| apply_tla(&map, val))294 .map(|v| WasmVal {295 val: v,296 state: self.state.clone(),297 })298 .map_err(|e| jrsonnet_js_error(&e))299 }300301 #[wasm_bindgen(js_name = manifestJson)]302 pub fn manifest_json(&self, indent: u32) -> Result<String, JsValue> {303 self.manifest_with(JsonFormat::cli(304 indent as usize,305 #[cfg(feature = "exp-preserve-order")]306 false,307 ))308 }309 #[wasm_bindgen(js_name = manifestToString)]310 pub fn manifest_to_string(&self) -> Result<String, JsValue> {311 self.manifest_with(ToStringFormat)312 }313 #[wasm_bindgen(js_name = manifestString)]314 pub fn manifest_string(&self) -> Result<String, JsValue> {315 self.manifest_with(StringFormat)316 }317 #[wasm_bindgen(js_name = manifestYaml)]318 pub fn manifest_yaml(&self, indent: u32, quote_keys: bool) -> Result<String, JsValue> {319 self.manifest_with(YamlFormat::std_to_yaml(320 indent != 0,321 quote_keys,322 #[cfg(feature = "exp-preserve-order")]323 false,324 ))325 }326 #[wasm_bindgen(js_name = manifestYamlStream)]327 pub fn manifest_yaml_stream(328 &self,329 indent: u32,330 quote_keys: bool,331 c_document_end: bool,332 ) -> Result<String, JsValue> {333 self.manifest_with(YamlStreamFormat::std_yaml_stream(334 YamlFormat::std_to_yaml(335 indent != 0,336 quote_keys,337 #[cfg(feature = "exp-preserve-order")]338 false,339 ),340 c_document_end,341 ))342 }343 #[wasm_bindgen(js_name = manifestXmlJsonml)]344 pub fn manifest_xml_jsonml(&self) -> Result<String, JsValue> {345 self.manifest_with(XmlJsonmlFormat::std_to_xml())346 }347 #[wasm_bindgen(js_name = manifestToml)]348 pub fn manifest_toml(&self, indent: u32) -> Result<String, JsValue> {349 self.manifest_with(TomlFormat::std_to_toml(350 " ".repeat(indent as usize),351 #[cfg(feature = "exp-preserve-order")]352 false,353 ))354 }355 #[wasm_bindgen(js_name = manifestIni)]356 pub fn manifest_ini(&self) -> Result<String, JsValue> {357 self.manifest_with(IniFormat::std(358 #[cfg(feature = "exp-preserve-order")]359 false,360 ))361 }362}363364#[wasm_bindgen(js_name = ArrValue)]365pub struct WasmArrValue {366 arr: ArrValue,367 state: Option<State>,368}369370#[wasm_bindgen(js_class = ArrValue)]371impl WasmArrValue {372 #[wasm_bindgen(getter)]373 pub fn length(&self) -> u32 {374 self.arr.len()375 }376 pub fn at(&self, index: u32) -> Result<Option<WasmVal>, JsValue> {377 let result = self.state.as_ref().map_or_else(378 || self.arr.get(index),379 |state| {380 let _guard = state.try_enter();381 self.arr.get(index)382 },383 );384 result385 .map(|opt: Option<Val>| {386 opt.map(|v| WasmVal {387 val: v,388 state: self.state.clone(),389 })390 })391 .map_err(|e| jrsonnet_js_error(&e))392 }393}394395#[wasm_bindgen(js_name = ObjValue)]396pub struct WasmObjValue {397 obj: ObjValue,398 state: Option<State>,399}400401#[wasm_bindgen(js_class = ObjValue)]402impl WasmObjValue {403 pub fn keys(&self) -> Vec<String> {404 self.obj405 .fields(406 #[cfg(feature = "exp-preserve-order")]407 false,408 )409 .into_iter()410 .map(|s| s.to_string())411 .collect()412 }413 pub fn get(&self, key: String) -> Result<Option<WasmVal>, JsValue> {414 let result = if let Some(state) = &self.state {415 let _guard = state.try_enter();416 self.obj.get(key.into())417 } else {418 self.obj.get(key.into())419 };420 result421 .map(|opt: Option<Val>| {422 opt.map(|v| WasmVal {423 val: v,424 state: self.state.clone(),425 })426 })427 .map_err(|e| jrsonnet_js_error(&e))428 }429}430431#[derive(Trace)]432struct JsHandler {433 #[trace(skip)]434 func: js_sys::Function,435}436437#[wasm_bindgen(inline_js = r"438export function js_invoke_val_callback(cb, args) {439 return cb.apply(null, args);440}441")]442extern "C" {443 #[wasm_bindgen(catch)]444 fn js_invoke_val_callback(445 cb: &js_sys::Function,446 args: &js_sys::Array,447 ) -> Result<WasmVal, JsValue>;448}449450impl NativeCallbackHandler for JsHandler {451 fn call(&self, args: &[Val]) -> JrResult<Val> {452 let js_args = js_sys::Array::new();453 let state = with_state(|s| s);454 for arg in args {455 js_args.push(&JsValue::from(WasmVal::with_state(456 arg.clone(),457 state.clone(),458 )));459 }460 let result = js_invoke_val_callback(&self.func, &js_args).map_err(|e| {461 let msg = e462 .as_string()463 .or_else(|| {464 e.dyn_ref::<js_sys::Error>()465 .map(|err| String::from(err.message()))466 })467 .unwrap_or_else(|| format!("{e:?}"));468 error!("js callback threw: {msg}")469 })?;470 Ok(result.val)471 }472}473474#[wasm_bindgen(js_name = State)]475pub struct WasmState {476 state: State,477 resolver: Option<JsAsyncResolver>,478}479#[wasm_bindgen(js_class = State)]480impl WasmState {481 #[wasm_bindgen(constructor)]482 pub fn new(resolver: Option<ImportResolverJs>) -> Self {483 console_error_panic_hook::set_once();484 let mut state = StateBuilder::default();485 state.import_resolver(ResolvedImportResolver::new());486 let std = jrsonnet_stdlib::ContextInitializer::new(PathResolver::Absolute);487 state.context_initializer(std);488 let state = state.build();489 Self {490 state,491 resolver: resolver.map(|js| JsAsyncResolver { js }),492 }493 }494495 #[wasm_bindgen(js_name = evaluateSnippet)]496 pub fn evaluate_snippet(&self, name: &str, snippet: &str) -> Result<WasmVal, JsValue> {497 let _guard = self.state.enter();498 self.state499 .evaluate_snippet(name, snippet)500 .map(|v| WasmVal::with_state(v, self.state.clone()))501 .map_err(|e| jrsonnet_js_error(&e))502 }503504 #[wasm_bindgen(js_name = evaluateFile)]505 pub async fn evaluate_file(&self, path: String) -> Result<WasmVal, JsValue> {506 self.evaluate_file_from_impl(None, path).await507 }508509 #[wasm_bindgen(js_name = evaluateFileFrom)]510 pub async fn evaluate_file_from(&self, from: String, path: String) -> Result<WasmVal, JsValue> {511 self.evaluate_file_from_impl(Some(from), path).await512 }513}514515impl WasmState {516 async fn evaluate_file_from_impl(517 &self,518 from: Option<String>,519 path: String,520 ) -> Result<WasmVal, JsValue> {521 let resolver = self522 .resolver523 .clone()524 .ok_or_else(|| JsValue::from_str("file evaluation requires an ImportResolver"))?;525 let from = match from {526 Some(s) => {527 let url = url::Url::parse(&s).map_err(|e| JsValue::from_str(&e.to_string()))?;528 SourcePath::new(SourceUrl::new(url))529 }530 None => SourcePath::default(),531 };532 let path = async_import(self.state.clone(), resolver, &from, &path.as_str()).await?;533 let _guard = self.state.enter();534 self.state535 .import_resolved(path)536 .map(|v| WasmVal::with_state(v, self.state.clone()))537 .map_err(|e| jrsonnet_js_error(&e))538 }539}540541#[wasm_bindgen]542extern "C" {543 #[wasm_bindgen(typescript_type = "ImportResolver")]544 #[derive(Clone)]545 pub type ImportResolverJs;546547 #[wasm_bindgen(catch, method, structural, js_name = resolveFrom)]548 fn resolve_from(549 this: &ImportResolverJs,550 from: Option<String>,551 path: &str,552 ) -> Result<js_sys::Promise, JsValue>;553554 #[wasm_bindgen(catch, method, structural, js_name = loadFileContents)]555 fn load_file_contents(556 this: &ImportResolverJs,557 resolved: &str,558 ) -> Result<js_sys::Promise, JsValue>;559}560561#[wasm_bindgen(typescript_custom_section)]562const TS_IMPORT_RESOLVER: &'static str = r"563export interface ImportResolver {564 resolveFrom(from: string | undefined, path: string): Promise<string>;565 loadFileContents(resolved: string): Promise<Uint8Array>;566}567";568569#[derive(Clone)]570struct JsAsyncResolver {571 js: ImportResolverJs,572}573574impl jrsonnet_evaluator::async_import::AsyncImportResolver for JsAsyncResolver {575 type Error = JsValue;576577 async fn resolve_from(578 &self,579 from: &SourcePath,580 path: &dyn jrsonnet_evaluator::AsPathLike,581 ) -> Result<SourcePath, JsValue> {582 let from_js = (!from.is_default()).then(|| from.to_string());583 let path_str = path.as_path().as_ref().to_string_lossy().into_owned();584 let promise = self585 .js586 .resolve_from(from_js, &path_str)587 .map_err(|e| js_resolver_error("resolveFrom", e))?;588 let resolved_js = wasm_bindgen_futures::JsFuture::from(promise)589 .await590 .map_err(|e| js_resolver_error("resolveFrom", e))?;591 let resolved_str = resolved_js592 .as_string()593 .ok_or_else(|| JsValue::from_str("resolveFrom must return string"))?;594 let url = url::Url::parse(&resolved_str).map_err(|e| JsValue::from_str(&e.to_string()))?;595 Ok(SourcePath::new(SourceUrl::new(url)))596 }597598 async fn load_file_contents(&self, resolved: &SourcePath) -> Result<Vec<u8>, JsValue> {599 let resolved_str = resolved.to_string();600 let promise = self601 .js602 .load_file_contents(&resolved_str)603 .map_err(|e| js_resolver_error("loadFileContents", e))?;604 let bytes_js = wasm_bindgen_futures::JsFuture::from(promise)605 .await606 .map_err(|e| js_resolver_error("loadFileContents", e))?;607 let arr = bytes_js608 .dyn_into::<js_sys::Uint8Array>()609 .map_err(|_| JsValue::from_str("loadFileContents must return Uint8Array"))?;610 Ok(arr.to_vec())611 }612}613614#[wasm_bindgen(js_name = FormatOptions)]615pub struct WasmFormatOptions {616 indent: u8,617}618#[wasm_bindgen(js_class = FormatOptions)]619impl WasmFormatOptions {620 #[wasm_bindgen(constructor)]621 pub fn new() -> Self {622 Self { indent: 0 }623 }624625 fn build(&self) -> FormatOptions {626 FormatOptions {627 indent: self.indent,628 }629 }630}631632impl Default for WasmFormatOptions {633 fn default() -> Self {634 Self::new()635 }636}637638#[wasm_bindgen]639pub fn format(src: &str, opts: &WasmFormatOptions) -> Result<String, String> {640 match jrsonnet_formatter::format(src, &opts.build()) {641 Ok(v) => Ok(v),642 Err(e) => {643 let e = e.build();644 Err(hi_doc::source_to_ansi(&e))645 }646 }647}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 let ptr = get(value, &JsValue::from_str("__wbg_ptr"))69 .ok()70 .and_then(|v| v.as_f64())71 .ok_or_else(|| JsValue::from_str("expected a Val instance"))? as u32;72 if ptr == 0 {73 return Err(JsValue::from_str("Val has been freed"));74 }75 Ok(unsafe { <WasmVal as RefFromWasmAbi>::ref_from_abi(ptr) })76}7778fn js_resolver_error(prefix: &str, e: JsValue) -> JsValue {79 let msg = format!("{prefix}: {}", js_error_message(&e));80 let frames = js_sys::Array::new();81 let frame = js_sys::Object::new();82 let _ = js_sys::Reflect::set(83 &frame,84 &JsValue::from_str("desc"),85 &JsValue::from_str(prefix),86 );87 frames.push(&frame);88 make_jrsonnet_error(&msg, frames, &e)89}9091fn jrsonnet_js_error(e: &jrsonnet_evaluator::Error) -> JsValue {92 let msg = e.error().to_string();93 // let msg = format.format(e).unwrap_or_else(|_| e.to_string());94 let frames = js_sys::Array::new();95 for el in &e.trace().0 {96 let frame = js_sys::Object::new();97 let _ = js_sys::Reflect::set(98 &frame,99 &JsValue::from_str("desc"),100 &JsValue::from_str(&el.desc),101 );102 if let Some(loc) = &el.location {103 let path = loc.0.source_path().to_string();104 let _ = js_sys::Reflect::set(105 &frame,106 &JsValue::from_str("path"),107 &JsValue::from_str(&path),108 );109 let mapped = loc.0.map_source_locations(&[loc.1, loc.2]);110 let _ = js_sys::Reflect::set(111 &frame,112 &JsValue::from_str("line"),113 &JsValue::from(mapped[0].line),114 );115 let _ = js_sys::Reflect::set(116 &frame,117 &JsValue::from_str("column"),118 &JsValue::from(mapped[0].column),119 );120 }121 frames.push(&frame);122 }123 make_jrsonnet_error(&msg, frames, &JsValue::UNDEFINED)124}125126impl From<ValType> for ValKind {127 fn from(v: ValType) -> Self {128 match v {129 ValType::Null => Self::Null,130 ValType::Bool => Self::Bool,131 ValType::Num => Self::Num,132 ValType::Str => Self::Str,133 ValType::Arr => Self::Arr,134 ValType::Obj => Self::Obj,135 ValType::Func => Self::Func,136 #[cfg(feature = "exp-bigint")]137 ValType::BigInt => Self::BigInt,138 }139 }140}141142#[wasm_bindgen(js_name = Val)]143pub struct WasmVal {144 val: Val,145 state: Option<State>,146}147148impl WasmVal {149 fn new(val: Val) -> Self {150 Self { val, state: None }151 }152 fn with_state(val: Val, state: State) -> Self {153 Self {154 val,155 state: Some(state),156 }157 }158 fn run<R>(&self, f: impl FnOnce(&Val) -> R) -> R {159 if let Some(state) = &self.state {160 let _guard = state.try_enter();161 f(&self.val)162 } else {163 f(&self.val)164 }165 }166 fn manifest_with(&self, format: impl ManifestFormat) -> Result<String, JsValue> {167 self.run(|v| v.manifest(format))168 .map_err(|e| jrsonnet_js_error(&e))169 }170}171172#[wasm_bindgen(js_class = Val)]173impl WasmVal {174 pub fn null() -> Self {175 Self::new(Val::Null)176 }177 pub fn bool(b: bool) -> Self {178 Self::new(Val::Bool(b))179 }180 pub fn num(n: f64) -> Result<Self, JsError> {181 let n = NumValue::new(n)182 .ok_or_else(|| JsError::new("only finite numbers are supported by jsonnet"))?;183 Ok(Self::new(Val::num(n)))184 }185 pub fn string(s: String) -> Self {186 Self::new(Val::string(s))187 }188 pub fn bigint(value: js_sys::BigInt) -> Result<Self, JsError> {189 #[cfg(feature = "exp-bigint")]190 {191 let s: String = value192 .to_string(10)193 .map_err(|_| JsError::new("invalid bigint"))?194 .into();195 let bi = s196 .parse::<num_bigint::BigInt>()197 .map_err(|e| JsError::new(&format!("failed to parse bigint: {e}")))?;198 Ok(Self::new(Val::BigInt(Box::new(bi))))199 }200 #[cfg(not(feature = "exp-bigint"))]201 {202 let _ = value;203 Err(JsError::new(204 "bigint support is not enabled in this build (exp-bigint feature)",205 ))206 }207 }208 pub fn arr(items: Vec<WasmVal>) -> Self {209 Self::new(Val::arr(210 items.into_iter().map(|v| v.val).collect::<Vec<_>>(),211 ))212 }213 pub fn func(214 params: Vec<String>,215216 #[wasm_bindgen(unchecked_param_type = "(...args: Val[]) => Val")]217 callback: js_sys::Function,218 ) -> Self {219 #[allow(deprecated)]220 Self::new(Val::function(NativeCallback::new(221 params,222 JsHandler { func: callback },223 )))224 }225226 #[wasm_bindgen(getter)]227 pub fn kind(&self) -> ValKind {228 self.val.value_type().into()229 }230 #[wasm_bindgen(js_name = asBool)]231 pub fn as_bool(&self) -> Option<bool> {232 self.val.as_bool()233 }234 #[wasm_bindgen(js_name = asNum)]235 pub fn as_num(&self) -> Option<f64> {236 self.val.as_num()237 }238 #[wasm_bindgen(js_name = asBigint)]239 pub fn as_bigint(&self) -> Result<Option<js_sys::BigInt>, JsError> {240 #[cfg(feature = "exp-bigint")]241 {242 let Some(bi) = self.val.as_bigint() else {243 return Ok(None);244 };245 let big = js_sys::BigInt::new(&JsValue::from_str(&bi.to_string()))246 .map_err(|e| JsError::new(&format!("{e:?}")))?;247 Ok(Some(big))248 }249 #[cfg(not(feature = "exp-bigint"))]250 {251 Err(JsError::new(252 "bigint support is not enabled in this build (exp-bigint feature)",253 ))254 }255 }256 #[wasm_bindgen(js_name = asString)]257 pub fn as_string(&self) -> Option<String> {258 self.val.as_str().map(|s| s.to_string())259 }260 #[wasm_bindgen(js_name = asArr)]261 pub fn as_arr(&self) -> Option<WasmArrValue> {262 self.val.as_arr().map(|arr| WasmArrValue {263 arr,264 state: self.state.clone(),265 })266 }267 #[wasm_bindgen(js_name = asObj)]268 pub fn as_obj(&self) -> Option<WasmObjValue> {269 self.val.as_obj().map(|obj| WasmObjValue {270 obj,271 state: self.state.clone(),272 })273 }274275 #[wasm_bindgen(js_name = applyTla)]276 pub fn apply_tla(277 &self,278 #[wasm_bindgen(unchecked_param_type = "Record<string, Val>")] args: &js_sys::Object,279 ) -> Result<WasmVal, JsValue> {280 let mut map: FxHashMap<IStr, TlaArg> = FxHashMap::default();281 for entry in js_sys::Object::entries(args).iter() {282 let pair: js_sys::Array = entry283 .dyn_into()284 .map_err(|_| JsValue::from_str("expected [key, value] entry"))?;285 let key = pair286 .get(0)287 .as_string()288 .ok_or_else(|| JsValue::from_str("TLA arg key must be a string"))?;289 let value = unwrap_val_ref(&pair.get(1))?;290 map.insert(key.into(), TlaArg::Val(value.val.clone()));291 }292 let val = self.val.clone();293 self.run(|_| apply_tla(&map, val))294 .map(|v| WasmVal {295 val: v,296 state: self.state.clone(),297 })298 .map_err(|e| jrsonnet_js_error(&e))299 }300301 #[wasm_bindgen(js_name = manifestJson)]302 pub fn manifest_json(&self, indent: u32) -> Result<String, JsValue> {303 self.manifest_with(JsonFormat::cli(304 indent as usize,305 #[cfg(feature = "exp-preserve-order")]306 false,307 ))308 }309 #[wasm_bindgen(js_name = manifestToString)]310 pub fn manifest_to_string(&self) -> Result<String, JsValue> {311 self.manifest_with(ToStringFormat)312 }313 #[wasm_bindgen(js_name = manifestString)]314 pub fn manifest_string(&self) -> Result<String, JsValue> {315 self.manifest_with(StringFormat)316 }317 #[wasm_bindgen(js_name = manifestYaml)]318 pub fn manifest_yaml(&self, indent: u32, quote_keys: bool) -> Result<String, JsValue> {319 self.manifest_with(YamlFormat::std_to_yaml(320 indent != 0,321 quote_keys,322 #[cfg(feature = "exp-preserve-order")]323 false,324 ))325 }326 #[wasm_bindgen(js_name = manifestYamlStream)]327 pub fn manifest_yaml_stream(328 &self,329 indent: u32,330 quote_keys: bool,331 c_document_end: bool,332 ) -> Result<String, JsValue> {333 self.manifest_with(YamlStreamFormat::std_yaml_stream(334 YamlFormat::std_to_yaml(335 indent != 0,336 quote_keys,337 #[cfg(feature = "exp-preserve-order")]338 false,339 ),340 c_document_end,341 ))342 }343 #[wasm_bindgen(js_name = manifestXmlJsonml)]344 pub fn manifest_xml_jsonml(&self) -> Result<String, JsValue> {345 self.manifest_with(XmlJsonmlFormat::std_to_xml())346 }347 #[wasm_bindgen(js_name = manifestToml)]348 pub fn manifest_toml(&self, indent: u32) -> Result<String, JsValue> {349 self.manifest_with(TomlFormat::std_to_toml(350 " ".repeat(indent as usize),351 #[cfg(feature = "exp-preserve-order")]352 false,353 ))354 }355 #[wasm_bindgen(js_name = manifestIni)]356 pub fn manifest_ini(&self) -> Result<String, JsValue> {357 self.manifest_with(IniFormat::std(358 #[cfg(feature = "exp-preserve-order")]359 false,360 ))361 }362}363364#[wasm_bindgen(js_name = ArrValue)]365pub struct WasmArrValue {366 arr: ArrValue,367 state: Option<State>,368}369370#[wasm_bindgen(js_class = ArrValue)]371impl WasmArrValue {372 #[wasm_bindgen(getter)]373 pub fn length(&self) -> u32 {374 self.arr.len()375 }376 pub fn at(&self, index: u32) -> Result<Option<WasmVal>, JsValue> {377 let result = self.state.as_ref().map_or_else(378 || self.arr.get(index),379 |state| {380 let _guard = state.try_enter();381 self.arr.get(index)382 },383 );384 result385 .map(|opt: Option<Val>| {386 opt.map(|v| WasmVal {387 val: v,388 state: self.state.clone(),389 })390 })391 .map_err(|e| jrsonnet_js_error(&e))392 }393}394395#[wasm_bindgen(js_name = ObjValue)]396pub struct WasmObjValue {397 obj: ObjValue,398 state: Option<State>,399}400401#[wasm_bindgen(js_class = ObjValue)]402impl WasmObjValue {403 pub fn keys(&self) -> Vec<String> {404 self.obj405 .fields(406 #[cfg(feature = "exp-preserve-order")]407 false,408 )409 .into_iter()410 .map(|s| s.to_string())411 .collect()412 }413 pub fn get(&self, key: String) -> Result<Option<WasmVal>, JsValue> {414 let result = if let Some(state) = &self.state {415 let _guard = state.try_enter();416 self.obj.get(key.into())417 } else {418 self.obj.get(key.into())419 };420 result421 .map(|opt: Option<Val>| {422 opt.map(|v| WasmVal {423 val: v,424 state: self.state.clone(),425 })426 })427 .map_err(|e| jrsonnet_js_error(&e))428 }429}430431#[derive(Trace)]432struct JsHandler {433 #[trace(skip)]434 func: js_sys::Function,435}436437#[wasm_bindgen(inline_js = r"438export function js_invoke_val_callback(cb, args) {439 return cb.apply(null, args);440}441")]442extern "C" {443 #[wasm_bindgen(catch)]444 fn js_invoke_val_callback(445 cb: &js_sys::Function,446 args: &js_sys::Array,447 ) -> Result<WasmVal, JsValue>;448}449450impl NativeCallbackHandler for JsHandler {451 fn call(&self, args: &[Val]) -> JrResult<Val> {452 let js_args = js_sys::Array::new();453 let state = with_state(|s| s);454 for arg in args {455 js_args.push(&JsValue::from(WasmVal::with_state(456 arg.clone(),457 state.clone(),458 )));459 }460 let result = js_invoke_val_callback(&self.func, &js_args).map_err(|e| {461 let msg = e462 .as_string()463 .or_else(|| {464 e.dyn_ref::<js_sys::Error>()465 .map(|err| String::from(err.message()))466 })467 .unwrap_or_else(|| format!("{e:?}"));468 error!("js callback threw: {msg}")469 })?;470 Ok(result.val)471 }472}473474#[wasm_bindgen(js_name = State)]475pub struct WasmState {476 state: State,477 resolver: Option<JsAsyncResolver>,478}479#[wasm_bindgen(js_class = State)]480impl WasmState {481 #[wasm_bindgen(constructor)]482 pub fn new(resolver: Option<ImportResolverJs>) -> Self {483 console_error_panic_hook::set_once();484 let mut state = StateBuilder::default();485 state.import_resolver(ResolvedImportResolver::new());486 let std = jrsonnet_stdlib::ContextInitializer::new(PathResolver::Absolute);487 state.context_initializer(std);488 let state = state.build();489 Self {490 state,491 resolver: resolver.map(|js| JsAsyncResolver { js }),492 }493 }494495 #[wasm_bindgen(js_name = evaluateSnippet)]496 pub fn evaluate_snippet(&self, name: &str, snippet: &str) -> Result<WasmVal, JsValue> {497 let _guard = self.state.enter();498 self.state499 .evaluate_snippet(name, snippet)500 .map(|v| WasmVal::with_state(v, self.state.clone()))501 .map_err(|e| jrsonnet_js_error(&e))502 }503504 #[wasm_bindgen(js_name = evaluateFile)]505 pub async fn evaluate_file(&self, path: String) -> Result<WasmVal, JsValue> {506 self.evaluate_file_from_impl(None, path).await507 }508509 #[wasm_bindgen(js_name = evaluateFileFrom)]510 pub async fn evaluate_file_from(&self, from: String, path: String) -> Result<WasmVal, JsValue> {511 self.evaluate_file_from_impl(Some(from), path).await512 }513}514515impl WasmState {516 async fn evaluate_file_from_impl(517 &self,518 from: Option<String>,519 path: String,520 ) -> Result<WasmVal, JsValue> {521 let resolver = self522 .resolver523 .clone()524 .ok_or_else(|| JsValue::from_str("file evaluation requires an ImportResolver"))?;525 let from = match from {526 Some(s) => {527 let url = url::Url::parse(&s).map_err(|e| JsValue::from_str(&e.to_string()))?;528 SourcePath::new(SourceUrl::new(url))529 }530 None => SourcePath::default(),531 };532 let path = async_import(self.state.clone(), resolver, &from, &path.as_str()).await?;533 let _guard = self.state.enter();534 self.state535 .import_resolved(path)536 .map(|v| WasmVal::with_state(v, self.state.clone()))537 .map_err(|e| jrsonnet_js_error(&e))538 }539}540541#[wasm_bindgen]542extern "C" {543 #[wasm_bindgen(typescript_type = "ImportResolver")]544 #[derive(Clone)]545 pub type ImportResolverJs;546547 #[wasm_bindgen(catch, method, structural, js_name = resolveFrom)]548 fn resolve_from(549 this: &ImportResolverJs,550 from: Option<String>,551 path: &str,552 ) -> Result<js_sys::Promise, JsValue>;553554 #[wasm_bindgen(catch, method, structural, js_name = loadFileContents)]555 fn load_file_contents(556 this: &ImportResolverJs,557 resolved: &str,558 ) -> Result<js_sys::Promise, JsValue>;559}560561#[wasm_bindgen(typescript_custom_section)]562const TS_IMPORT_RESOLVER: &'static str = r"563export interface ImportResolver {564 resolveFrom(from: string | undefined, path: string): Promise<string>;565 loadFileContents(resolved: string): Promise<Uint8Array>;566}567";568569#[derive(Clone)]570struct JsAsyncResolver {571 js: ImportResolverJs,572}573574impl jrsonnet_evaluator::async_import::AsyncImportResolver for JsAsyncResolver {575 type Error = JsValue;576577 async fn resolve_from(578 &self,579 from: &SourcePath,580 path: &dyn jrsonnet_evaluator::AsPathLike,581 ) -> Result<SourcePath, JsValue> {582 let from_js = (!from.is_default()).then(|| from.to_string());583 let path_str = path.as_path().as_ref().to_string_lossy().into_owned();584 let promise = self585 .js586 .resolve_from(from_js, &path_str)587 .map_err(|e| js_resolver_error("resolveFrom", e))?;588 let resolved_js = wasm_bindgen_futures::JsFuture::from(promise)589 .await590 .map_err(|e| js_resolver_error("resolveFrom", e))?;591 let resolved_str = resolved_js592 .as_string()593 .ok_or_else(|| JsValue::from_str("resolveFrom must return string"))?;594 let url = url::Url::parse(&resolved_str).map_err(|e| JsValue::from_str(&e.to_string()))?;595 Ok(SourcePath::new(SourceUrl::new(url)))596 }597598 async fn load_file_contents(&self, resolved: &SourcePath) -> Result<Vec<u8>, JsValue> {599 let resolved_str = resolved.to_string();600 let promise = self601 .js602 .load_file_contents(&resolved_str)603 .map_err(|e| js_resolver_error("loadFileContents", e))?;604 let bytes_js = wasm_bindgen_futures::JsFuture::from(promise)605 .await606 .map_err(|e| js_resolver_error("loadFileContents", e))?;607 let arr = bytes_js608 .dyn_into::<js_sys::Uint8Array>()609 .map_err(|_| JsValue::from_str("loadFileContents must return Uint8Array"))?;610 Ok(arr.to_vec())611 }612}613614#[wasm_bindgen(js_name = FormatOptions)]615pub struct WasmFormatOptions {616 indent: u8,617 use_tabs: bool,618 max_width: u32,619}620#[wasm_bindgen(js_class = FormatOptions)]621impl WasmFormatOptions {622 #[wasm_bindgen(constructor)]623 pub fn new() -> Self {624 Self {625 indent: 4,626 use_tabs: true,627 max_width: 100,628 }629 }630631 fn build(&self) -> FormatOptions {632 FormatOptions {633 indent: self.indent,634 use_tabs: self.use_tabs,635 max_width: self.max_width,636 }637 }638}639640impl Default for WasmFormatOptions {641 fn default() -> Self {642 Self::new()643 }644}645646#[wasm_bindgen]647pub fn format(src: &str, opts: &WasmFormatOptions) -> Result<String, String> {648 match jrsonnet_formatter::format(src, &opts.build()) {649 Ok(v) => Ok(v),650 Err(e) => {651 let e = e.build();652 Err(hi_doc::source_to_ansi(&e))653 }654 }655}cmds/jrsonnet-fmt/src/main.rsdiffbeforeafterboth--- a/cmds/jrsonnet-fmt/src/main.rs
+++ b/cmds/jrsonnet-fmt/src/main.rs
@@ -25,11 +25,14 @@
#[arg(long)]
test: bool,
/// Number of spaces to indent with
- #[arg(long, default_value = "2")]
+ #[arg(long, default_value = "4")]
indent: u8,
/// Force hard tab for indentation
- #[arg(long)]
- hard_tabs: bool,
+ #[arg(long, default_value = "true")]
+ use_tabs: bool,
+ /// Max formatted source width
+ #[arg(long, default_value = "100")]
+ max_width: u32,
/// Debug option: how many times to call reformatting in case of unstable dprint output resolution.
///
@@ -51,13 +54,7 @@
}
fn main_result() -> Result<(), Error> {
- eprintln!(
- "jrsonnet-fmt is a prototype of a jsonnet code formatter, do not expect it to produce meaningful results right now."
- );
- eprintln!(
- "It is not expected for its output to match other implementations, it will be completly separate implementation with maybe different name."
- );
- let mut opts = Opts::parse();
+ let opts = Opts::parse();
let input = if opts.exec {
if opts.in_place {
return Err(Error::InPlaceExec);
@@ -66,12 +63,6 @@
} else {
fs::read_to_string(&opts.input)?
};
-
- if opts.indent == 0 {
- // Sane default.
- // TODO: Implement actual guessing.
- opts.hard_tabs = true;
- }
let mut iteration = 0;
let mut formatted = input.clone();
@@ -81,11 +72,9 @@
let reformatted = match format(
&formatted,
&FormatOptions {
- indent: if opts.indent == 0 || opts.hard_tabs {
- 0
- } else {
- opts.indent
- },
+ indent: opts.indent,
+ use_tabs: opts.use_tabs,
+ max_width: opts.max_width,
},
) {
Ok(v) => v,
crates/jrsonnet-formatter/src/lib.rsdiffbeforeafterboth--- a/crates/jrsonnet-formatter/src/lib.rs
+++ b/crates/jrsonnet-formatter/src/lib.rs
@@ -901,14 +901,25 @@
}
}
-#[derive(Default)]
pub struct FormatOptions {
- // 0 for hard tabs, otherwise number of spaces
pub indent: u8,
+ pub use_tabs: bool,
+ pub max_width: u32,
}
+
impl FormatOptions {
pub fn new() -> Self {
- Self::default()
+ Self {
+ indent: 4,
+ use_tabs: true,
+ max_width: 100,
+ }
+ }
+}
+
+impl Default for FormatOptions {
+ fn default() -> Self {
+ Self::new()
}
}
@@ -947,14 +958,9 @@
out
},
PrintOptions {
- indent_width: if opts.indent == 0 {
- // Reasonable max length for both 2 and 4 space sized tabs.
- 3
- } else {
- opts.indent
- },
- max_width: 100,
- use_tabs: opts.indent == 0,
+ indent_width: opts.indent,
+ max_width: opts.max_width,
+ use_tabs: opts.use_tabs,
new_line_text: "\n",
},
))