diff --git a/nix-js/runtime-ts/src/builtins/conversion.ts b/nix-js/runtime-ts/src/builtins/conversion.ts index a5efd81..0806b1e 100644 --- a/nix-js/runtime-ts/src/builtins/conversion.ts +++ b/nix-js/runtime-ts/src/builtins/conversion.ts @@ -32,8 +32,12 @@ export const toJSON = (e: NixValue): NixString => { return mkStringWithContext(string, context); }; -export const toXML = (_e: NixValue): never => { - throw new Error("Not implemented: toXML"); +export const toXML = (e: NixValue): NixString => { + const [xml, context] = Deno.core.ops.op_to_xml(force(e)); + if (context.length === 0) { + return xml; + } + return mkStringWithContext(xml, new Set(context)); }; /** diff --git a/nix-js/runtime-ts/src/types/global.d.ts b/nix-js/runtime-ts/src/types/global.d.ts index 2015aad..7ba3c0d 100644 --- a/nix-js/runtime-ts/src/types/global.d.ts +++ b/nix-js/runtime-ts/src/types/global.d.ts @@ -60,6 +60,7 @@ declare global { function op_from_json(json: string): unknown; function op_from_toml(toml: string): unknown; + function op_to_xml(e: NixValue): [string, string[]]; function op_finalize_derivation(input: { name: string; diff --git a/nix-js/src/context.rs b/nix-js/src/context.rs index 3ced860..2597051 100644 --- a/nix-js/src/context.rs +++ b/nix-js/src/context.rs @@ -119,8 +119,14 @@ impl Context { DERIVATION_NIX.to_string(), ); let code = self.ctx.compile(source, None)?; - self.runtime - .eval(format!("Nix.builtins.derivation = {};Nix.builtins.storeDir=\"{}\"", code, self.get_store_dir()), &mut self.ctx)?; + self.runtime.eval( + format!( + "Nix.builtins.derivation = {};Nix.builtins.storeDir=\"{}\"", + code, + self.get_store_dir() + ), + &mut self.ctx, + )?; Ok(()) } diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index 493b36e..e5bd710 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -73,6 +73,7 @@ fn runtime_extension() -> Extension { op_from_json(), op_from_toml(), op_finalize_derivation::(), + op_to_xml(), ]; ops.extend(crate::fetcher::register_ops::()); @@ -193,7 +194,8 @@ impl Runtime { #[cfg(feature = "inspector")] pub(crate) fn wait_for_inspector_disconnect(&mut self) { - let _ = self.rt + let _ = self + .rt .block_on(self.js_runtime.run_event_loop(PollEventLoopOptions { wait_for_inspector: true, ..Default::default() diff --git a/nix-js/src/runtime/ops.rs b/nix-js/src/runtime/ops.rs index 34bc11c..b265e17 100644 --- a/nix-js/src/runtime/ops.rs +++ b/nix-js/src/runtime/ops.rs @@ -3,8 +3,7 @@ use std::str::FromStr; use hashbrown::hash_map::{Entry, HashMap}; -use deno_core::OpState; -use deno_core::v8; +use deno_core::{FromV8, OpState, v8}; use regex::Regex; use rust_embed::Embed; @@ -12,6 +11,8 @@ use super::{NixRuntimeError, OpStateExt, RuntimeContext}; use crate::error::Source; use crate::store::Store as _; +type Result = std::result::Result; + #[derive(Debug, Default)] pub(super) struct RegexCache { cache: HashMap, @@ -24,7 +25,7 @@ impl RegexCache { } } - fn get_regex(&mut self, pattern: &str) -> Result { + fn get_regex(&mut self, pattern: &str) -> std::result::Result { Ok(match self.cache.entry(pattern.to_string()) { Entry::Occupied(occupied) => occupied.get().clone(), Entry::Vacant(vacant) => { @@ -44,7 +45,7 @@ pub(crate) struct CorePkgs; pub(super) fn op_import( state: &mut OpState, #[string] path: String, -) -> std::result::Result { +) -> Result { let _span = tracing::info_span!("op_import", path = %path).entered(); let ctx: &mut Ctx = state.get_ctx_mut(); @@ -93,7 +94,7 @@ pub(super) fn op_scoped_import( state: &mut OpState, #[string] path: String, #[serde] scope: Vec, -) -> std::result::Result { +) -> Result { let _span = tracing::info_span!("op_scoped_import", path = %path).entered(); let ctx: &mut Ctx = state.get_ctx_mut(); @@ -119,7 +120,7 @@ pub(super) fn op_scoped_import( #[deno_core::op2] #[string] -pub(super) fn op_read_file(#[string] path: String) -> std::result::Result { +pub(super) fn op_read_file(#[string] path: String) -> Result { Ok(std::fs::read_to_string(&path).map_err(|e| format!("Failed to read {}: {}", path, e))?) } @@ -140,9 +141,7 @@ pub(super) fn op_path_exists(#[string] path: String) -> bool { #[deno_core::op2] #[string] -pub(super) fn op_read_file_type( - #[string] path: String, -) -> std::result::Result { +pub(super) fn op_read_file_type(#[string] path: String) -> Result { let path = Path::new(&path); let metadata = std::fs::symlink_metadata(path) .map_err(|e| format!("Failed to read file type for {}: {}", path.display(), e))?; @@ -165,7 +164,7 @@ pub(super) fn op_read_file_type( #[serde] pub(super) fn op_read_dir( #[string] path: String, -) -> std::result::Result, NixRuntimeError> { +) -> Result> { let path = Path::new(&path); if !path.is_dir() { @@ -210,7 +209,7 @@ pub(super) fn op_read_dir( pub(super) fn op_resolve_path( #[string] current_dir: String, #[string] path: String, -) -> std::result::Result { +) -> Result { let _span = tracing::debug_span!("op_resolve_path").entered(); tracing::debug!(current_dir, path); @@ -261,7 +260,7 @@ pub(super) fn op_make_placeholder(#[string] output: String) -> String { pub(super) fn op_decode_span( state: &mut OpState, #[string] span_str: String, -) -> std::result::Result { +) -> Result { let parts: Vec<&str> = span_str.split(':').collect(); if parts.len() != 3 { return Ok(serde_json::json!({ @@ -317,7 +316,7 @@ pub(super) struct ParsedHash { pub(super) fn op_parse_hash( #[string] hash_str: String, #[string] algo: Option, -) -> std::result::Result { +) -> Result { use nix_compat::nixhash::{HashAlgo, NixHash}; let hash_algo = algo @@ -347,7 +346,7 @@ pub(super) fn op_add_path( #[string] name: Option, recursive: bool, #[string] sha256: Option, -) -> std::result::Result { +) -> Result { use nix_compat::nixhash::{HashAlgo, NixHash}; use sha2::{Digest, Sha256}; use std::fs; @@ -423,7 +422,7 @@ pub(super) fn op_add_path( pub(super) fn op_store_path( state: &mut OpState, #[string] path: String, -) -> std::result::Result { +) -> Result { use crate::store::validate_store_path; let ctx: &Ctx = state.get_ctx(); @@ -446,7 +445,7 @@ pub(super) fn op_to_file( #[string] name: String, #[string] contents: String, #[serde] references: Vec, -) -> std::result::Result { +) -> Result { let ctx: &Ctx = state.get_ctx(); let store = ctx.get_store(); let store_path = store @@ -461,7 +460,7 @@ pub(super) fn op_to_file( pub(super) fn op_copy_path_to_store( state: &mut OpState, #[string] path: String, -) -> std::result::Result { +) -> Result { use std::path::Path; let path_obj = Path::new(&path); @@ -490,7 +489,7 @@ pub(super) fn op_copy_path_to_store( #[deno_core::op2] #[string] -pub(super) fn op_get_env(#[string] key: String) -> std::result::Result { +pub(super) fn op_get_env(#[string] key: String) -> Result { match std::env::var(key) { Ok(val) => Ok(val), Err(std::env::VarError::NotPresent) => Ok("".into()), @@ -500,14 +499,12 @@ pub(super) fn op_get_env(#[string] key: String) -> std::result::Result std::result::Result, NixRuntimeError> { +pub(super) fn op_walk_dir(#[string] path: String) -> Result> { fn walk_recursive( base: &Path, current: &Path, results: &mut Vec<(String, String)>, - ) -> std::result::Result<(), NixRuntimeError> { + ) -> Result<()> { let entries = std::fs::read_dir(current) .map_err(|e| NixRuntimeError::from(format!("failed to read directory: {}", e)))?; @@ -564,7 +561,7 @@ pub(super) fn op_add_filtered_path( recursive: bool, #[string] sha256: Option, #[serde] include_paths: Vec, -) -> std::result::Result { +) -> Result { use nix_compat::nixhash::{HashAlgo, NixHash}; use sha2::{Digest, Sha256}; use std::fs; @@ -667,7 +664,7 @@ pub(super) fn op_match( state: &mut OpState, #[string] regex: String, #[string] text: String, -) -> std::result::Result>>, NixRuntimeError> { +) -> Result>>> { let cache = state.borrow_mut::(); let re = cache .get_regex(&format!("^{}$", regex)) @@ -692,7 +689,7 @@ pub(super) fn op_split( state: &mut OpState, #[string] regex: String, #[string] text: String, -) -> std::result::Result, NixRuntimeError> { +) -> Result> { let cache = state.borrow_mut::(); let re = cache .get_regex(®ex) @@ -741,41 +738,39 @@ pub(super) enum NixJsonValue { } impl<'a> deno_core::convert::ToV8<'a> for NixJsonValue { - type Error = deno_error::JsErrorBox; + type Error = NixRuntimeError; fn to_v8<'i>( self, scope: &mut v8::PinScope<'a, 'i>, ) -> std::result::Result, Self::Error> { - match self { - Self::Null => Ok(v8::null(scope).into()), - Self::Bool(b) => Ok(v8::Boolean::new(scope, b).into()), - Self::Int(i) => Ok(v8::BigInt::new_from_i64(scope, i).into()), - Self::Float(f) => Ok(v8::Number::new(scope, f).into()), + Ok(match self { + Self::Null => v8::null(scope).into() , + Self::Bool(b) => v8::Boolean::new(scope, b).into() , + Self::Int(i) => v8::BigInt::new_from_i64(scope, i).into() , + Self::Float(f) => v8::Number::new(scope, f).into() , Self::Str(s) => v8::String::new(scope, &s) .map(|s| s.into()) - .ok_or_else(|| deno_error::JsErrorBox::type_error("failed to create v8 string")), + .ok_or("failed to create v8 string")?, Self::Arr(arr) => { let elements = arr .into_iter() .map(|v| v.to_v8(scope)) .collect::, _>>()?; - Ok(v8::Array::new_with_elements(scope, &elements).into()) + v8::Array::new_with_elements(scope, &elements).into() } Self::Obj(entries) => { let obj = v8::Object::new(scope); for (k, v) in entries { let key: v8::Local = v8::String::new(scope, &k) - .ok_or_else(|| { - deno_error::JsErrorBox::type_error("failed to create v8 string") - })? + .ok_or("failed to create v8 string")? .into(); let val = v.to_v8(scope)?; obj.set(scope, key, val); } - Ok(obj.into()) + obj.into() } - } + }) } } @@ -815,7 +810,7 @@ impl DrvHashCache { } } -fn toml_to_nix(value: toml::Value) -> std::result::Result { +fn toml_to_nix(value: toml::Value) -> Result { match value { toml::Value::String(s) => Ok(NixJsonValue::Str(s)), toml::Value::Integer(i) => Ok(NixJsonValue::Int(i)), @@ -839,18 +834,14 @@ fn toml_to_nix(value: toml::Value) -> std::result::Result std::result::Result { +pub(super) fn op_from_json(#[string] json_str: String) -> Result { let parsed: serde_json::Value = serde_json::from_str(&json_str) .map_err(|e| NixRuntimeError::from(format!("builtins.fromJSON: {e}")))?; Ok(json_to_nix(parsed)) } #[deno_core::op2] -pub(super) fn op_from_toml( - #[string] toml_str: String, -) -> std::result::Result { +pub(super) fn op_from_toml(#[string] toml_str: String) -> Result { let parsed: toml::Value = toml::from_str(&toml_str) .map_err(|e| NixRuntimeError::from(format!("while parsing TOML: {e}")))?; toml_to_nix(parsed) @@ -898,7 +889,7 @@ fn output_path_name(drv_name: &str, output: &str) -> String { pub(super) fn op_finalize_derivation( state: &mut OpState, #[serde] input: FinalizeDerivationInput, -) -> std::result::Result { +) -> Result { use crate::derivation::{DerivationData, OutputInfo}; use crate::string_context::extract_input_drvs_and_srcs; @@ -1104,10 +1095,7 @@ fn op_make_fixed_output_path_impl( #[deno_core::op2] #[string] -pub(super) fn op_hash_string( - #[string] algo: String, - #[string] data: String, -) -> std::result::Result { +pub(super) fn op_hash_string(#[string] algo: String, #[string] data: String) -> Result { use sha2::{Digest, Sha256, Sha512}; let hash_bytes: Vec = match algo.as_str() { @@ -1144,10 +1132,7 @@ pub(super) fn op_hash_string( #[deno_core::op2] #[string] -pub(super) fn op_hash_file( - #[string] algo: String, - #[string] path: String, -) -> std::result::Result { +pub(super) fn op_hash_file(#[string] algo: String, #[string] path: String) -> Result { let data = std::fs::read(&path) .map_err(|e| NixRuntimeError::from(format!("cannot read '{}': {}", path, e)))?; @@ -1187,9 +1172,7 @@ pub(super) fn op_hash_file( #[deno_core::op2] #[string] -pub(super) fn op_convert_hash( - #[serde] input: ConvertHashInput, -) -> std::result::Result { +pub(super) fn op_convert_hash(#[serde] input: ConvertHashInput) -> Result { use nix_compat::nixhash::{HashAlgo, NixHash}; let hash_algo = input @@ -1229,3 +1212,540 @@ pub(super) struct ConvertHashInput { #[serde(rename = "toHashFormat")] to_format: String, } + +struct XmlCtx<'s> { + force_fn: v8::Local<'s, v8::Function>, + is_thunk: v8::Local<'s, v8::Symbol>, + has_context: v8::Local<'s, v8::Symbol>, + is_path: v8::Local<'s, v8::Symbol>, + primop_meta: v8::Local<'s, v8::Symbol>, +} + +impl<'s> XmlCtx<'s> { + fn new<'i>( + scope: &mut v8::PinScope<'s, 'i>, + nix_obj: v8::Local<'s, v8::Object>, + ) -> Result { + let get_fn = |scope: &v8::PinScope<'s, 'i>, name: &str| { + let key = v8::String::new(scope, name).ok_or("v8 string" )?; + let val = nix_obj + .get(scope, key.into()) + .ok_or_else(|| format!("no {name}") )?; + v8::Local::::try_from(val) + .map_err(|e| format!("{name} not function: {e}") ) + }; + let get_sym = |scope: &v8::PinScope<'s, 'i>, name: &str| { + let key = v8::String::new(scope, name).ok_or("v8 string" )?; + let val = nix_obj + .get(scope, key.into()) + .ok_or_else(|| format!("no {name}") )?; + v8::Local::::try_from(val).map_err(|e| format!("{name} not symbol: {e}") ) + }; + Ok(Self { + force_fn: get_fn(scope, "force")?, + is_thunk: get_sym(scope, "IS_THUNK")?, + has_context: get_sym(scope, "HAS_CONTEXT")?, + is_path: get_sym(scope, "IS_PATH")?, + primop_meta: get_sym(scope, "PRIMOP_METADATA")?, + }) + } +} + +struct XmlWriter { + buf: String, + context: Vec, + drvs_seen: hashbrown::HashSet, +} + +impl XmlWriter { + fn new() -> Self { + Self { + buf: String::with_capacity(4096), + context: Vec::new(), + drvs_seen: hashbrown::HashSet::new(), + } + } + + fn indent(&mut self, depth: usize) { + for _ in 0..depth { + self.buf.push_str(" "); + } + } + + fn escape_attr(&mut self, s: &str) { + for ch in s.chars() { + match ch { + '"' => self.buf.push_str("""), + '<' => self.buf.push_str("<"), + '>' => self.buf.push_str(">"), + '&' => self.buf.push_str("&"), + '\n' => self.buf.push_str(" "), + '\r' => self.buf.push_str(" "), + '\t' => self.buf.push_str(" "), + c => self.buf.push(c), + } + } + } + + fn force<'s>( + &self, + val: v8::Local<'s, v8::Value>, + scope: &mut v8::PinScope<'s, '_>, + ctx: &XmlCtx<'s>, + ) -> Result> { + let undef = v8::undefined(scope); + ctx.force_fn + .call(scope, undef.into(), &[val]) + .ok_or_else(|| ("force() threw an exception").into()) + } + + fn has_sym<'s>( + obj: v8::Local<'s, v8::Object>, + scope: &mut v8::PinScope<'s, '_>, + sym: v8::Local<'s, v8::Symbol>, + ) -> bool { + matches!(obj.get(scope, sym.into()), Some(v) if v.is_true()) + } + + fn extract_str<'s>( + &self, + val: v8::Local<'s, v8::Value>, + scope: &mut v8::PinScope<'s, '_>, + ctx: &XmlCtx<'s>, + ) -> Option { + if val.is_string() { + return Some(val.to_rust_string_lossy(scope)); + } + if val.is_object() { + let obj = val.to_object(scope)?; + if Self::has_sym(obj, scope, ctx.has_context) { + let key = v8::String::new(scope, "value")?; + let s = obj.get(scope, key.into())?; + return Some(s.to_rust_string_lossy(scope)); + } + } + None + } + + fn collect_context<'s>( + &mut self, + obj: v8::Local<'s, v8::Object>, + scope: &mut v8::PinScope<'s, '_>, + ) { + let context_key = match v8::String::new(scope, "context") { + Some(k) => k, + None => return, + }; + let ctx_val = match obj.get(scope, context_key.into()) { + Some(v) => v, + None => return, + }; + let global = scope.get_current_context().global(scope); + let array_key = match v8::String::new(scope, "Array") { + Some(k) => k, + None => return, + }; + let array_obj = match global + .get(scope, array_key.into()) + .and_then(|v| v.to_object(scope)) + { + Some(o) => o, + None => return, + }; + let from_key = match v8::String::new(scope, "from") { + Some(k) => k, + None => return, + }; + let from_fn = match array_obj + .get(scope, from_key.into()) + .and_then(|v| v8::Local::::try_from(v).ok()) + { + Some(f) => f, + None => return, + }; + let arr_val = match from_fn.call(scope, array_obj.into(), &[ctx_val]) { + Some(v) => v, + None => return, + }; + let arr = match v8::Local::::try_from(arr_val) { + Ok(a) => a, + Err(_) => return, + }; + for i in 0..arr.length() { + if let Some(elem) = arr.get_index(scope, i) { + self.context.push(elem.to_rust_string_lossy(scope)); + } + } + } + + fn is_derivation<'s>(obj: v8::Local<'s, v8::Object>, scope: &mut v8::PinScope<'s, '_>) -> bool { + let key = match v8::String::new(scope, "type") { + Some(k) => k, + None => return false, + }; + match obj.get(scope, key.into()) { + Some(v) if v.is_string() => v.to_rust_string_lossy(scope) == "derivation", + _ => false, + } + } + + fn write_value<'s>( + &mut self, + val: v8::Local<'s, v8::Value>, + scope: &mut v8::PinScope<'s, '_>, + ctx: &XmlCtx<'s>, + depth: usize, + ) -> Result<()> { + let val = self.force(val, scope, ctx)?; + + if val.is_null() { + self.indent(depth); + self.buf.push_str("\n"); + return Ok(()); + } + if val.is_true() { + self.indent(depth); + self.buf.push_str("\n"); + return Ok(()); + } + if val.is_false() { + self.indent(depth); + self.buf.push_str("\n"); + return Ok(()); + } + if val.is_big_int() { + let bi = val.to_big_int(scope).ok_or("bigint" )?; + let (i, _) = bi.i64_value(); + self.indent(depth); + self.buf.push_str("\n"); + return Ok(()); + } + if val.is_number() { + let n = val.number_value(scope).ok_or("number" )?; + self.indent(depth); + self.buf.push_str("\n"); + return Ok(()); + } + if val.is_string() { + let s = val.to_rust_string_lossy(scope); + self.indent(depth); + self.buf.push_str("\n"); + return Ok(()); + } + if val.is_array() { + let arr = v8::Local::::try_from(val).map_err(|e| e.to_string() )?; + self.indent(depth); + self.buf.push_str("\n"); + for i in 0..arr.length() { + let elem = arr.get_index(scope, i).ok_or("array elem" )?; + self.write_value(elem, scope, ctx, depth + 1)?; + } + self.indent(depth); + self.buf.push_str("\n"); + return Ok(()); + } + if val.is_function() { + return self.write_function(val, scope, ctx, depth); + } + if val.is_object() { + let obj = val.to_object(scope).ok_or("to_object" )?; + + if Self::has_sym(obj, scope, ctx.has_context) { + let key = v8::String::new(scope, "value").ok_or("v8 str" )?; + let s = obj + .get(scope, key.into()) + .ok_or("value" )? + .to_rust_string_lossy(scope); + self.collect_context(obj, scope); + self.indent(depth); + self.buf.push_str("\n"); + return Ok(()); + } + if Self::has_sym(obj, scope, ctx.is_path) { + let key = v8::String::new(scope, "value").ok_or("v8 str" )?; + let s = obj + .get(scope, key.into()) + .ok_or("value" )? + .to_rust_string_lossy(scope); + self.indent(depth); + self.buf.push_str("\n"); + return Ok(()); + } + if Self::has_sym(obj, scope, ctx.is_thunk) { + self.indent(depth); + self.buf.push_str("\n"); + return Ok(()); + } + + return self.write_attrs(obj, scope, ctx, depth); + } + + self.indent(depth); + self.buf.push_str("\n"); + Ok(()) + } + + fn write_attrs<'s>( + &mut self, + obj: v8::Local<'s, v8::Object>, + scope: &mut v8::PinScope<'s, '_>, + ctx: &XmlCtx<'s>, + depth: usize, + ) -> Result<()> { + if Self::is_derivation(obj, scope) { + self.indent(depth); + self.buf.push_str("\n"); + + let is_repeated = if let Some(ref dp) = drv_str { + !self.drvs_seen.insert(dp.clone()) + } else { + false + }; + + if is_repeated { + self.indent(depth + 1); + self.buf.push_str("\n"); + } else { + self.write_attrs_sorted(obj, scope, ctx, depth + 1)?; + } + + self.indent(depth); + self.buf.push_str("\n"); + } else { + self.indent(depth); + self.buf.push_str("\n"); + self.write_attrs_sorted(obj, scope, ctx, depth + 1)?; + self.indent(depth); + self.buf.push_str("\n"); + } + Ok(()) + } + + fn write_attrs_sorted<'s>( + &mut self, + obj: v8::Local<'s, v8::Object>, + scope: &mut v8::PinScope<'s, '_>, + ctx: &XmlCtx<'s>, + depth: usize, + ) -> Result<()> { + let keys = obj + .get_own_property_names(scope, v8::GetPropertyNamesArgsBuilder::new().build()) + .ok_or("property names" )?; + + let mut key_strings: Vec = Vec::with_capacity(keys.length() as usize); + for i in 0..keys.length() { + let key = keys.get_index(scope, i).ok_or("key index" )?; + key_strings.push(key.to_rust_string_lossy(scope)); + } + key_strings.sort(); + + for key_str in &key_strings { + let v8_key = v8::String::new(scope, key_str).ok_or("v8 str" )?; + let val = obj + .get(scope, v8_key.into()) + .ok_or("attr value" )?; + + self.indent(depth); + self.buf.push_str("\n"); + + self.write_value(val, scope, ctx, depth + 1)?; + + self.indent(depth); + self.buf.push_str("\n"); + } + Ok(()) + } + + fn write_function<'s>( + &mut self, + val: v8::Local<'s, v8::Value>, + scope: &mut v8::PinScope<'s, '_>, + ctx: &XmlCtx<'s>, + depth: usize, + ) -> Result<()> { + let obj = val.to_object(scope).ok_or("fn to_object" )?; + + if let Some(meta) = obj.get(scope, ctx.primop_meta.into()) + && meta.is_object() + && !meta.is_null_or_undefined() + { + self.indent(depth); + self.buf.push_str("\n"); + return Ok(()); + } + + let args_key = v8::String::new(scope, "args").ok_or("v8 str" )?; + let args_val = obj.get(scope, args_key.into()); + + match args_val { + Some(args) if args.is_object() && !args.is_null_or_undefined() => { + let args_obj = args.to_object(scope).ok_or("args to_object" )?; + + let req_key = v8::String::new(scope, "required").ok_or("v8 str" )?; + let opt_key = v8::String::new(scope, "optional").ok_or("v8 str" )?; + let ellipsis_key = v8::String::new(scope, "ellipsis").ok_or("v8 str" )?; + + let mut all_formals: Vec = Vec::new(); + if let Some(req) = args_obj.get(scope, req_key.into()) + && let Ok(arr) = v8::Local::::try_from(req) + { + for i in 0..arr.length() { + if let Some(elem) = arr.get_index(scope, i) { + all_formals.push(elem.to_rust_string_lossy(scope)); + } + } + } + if let Some(opt) = args_obj.get(scope, opt_key.into()) + && let Ok(arr) = v8::Local::::try_from(opt) + { + for i in 0..arr.length() { + if let Some(elem) = arr.get_index(scope, i) { + all_formals.push(elem.to_rust_string_lossy(scope)); + } + } + } + all_formals.sort(); + + let has_ellipsis = matches!( + args_obj.get(scope, ellipsis_key.into()), + Some(v) if v.is_true() + ); + + self.indent(depth); + self.buf.push_str("\n"); + self.indent(depth + 1); + if has_ellipsis { + self.buf.push_str("\n"); + } else { + self.buf.push_str("\n"); + } + for formal in &all_formals { + self.indent(depth + 2); + self.buf.push_str("\n"); + } + self.indent(depth + 1); + self.buf.push_str("\n"); + self.indent(depth); + self.buf.push_str("\n"); + } + _ => { + let source = val + .to_detail_string(scope) + .map(|s| s.to_rust_string_lossy(scope)); + let param = source.as_deref().and_then(extract_lambda_param); + self.indent(depth); + self.buf.push_str("\n"); + self.indent(depth + 1); + self.buf.push_str("\n"); + self.indent(depth); + self.buf.push_str("\n"); + } + } + Ok(()) + } +} + +fn extract_lambda_param(source: &str) -> Option<&str> { + let s = source.trim(); + let arrow_pos = s.find("=>")?; + let before = s[..arrow_pos].trim(); + if before.starts_with('(') && before.ends_with(')') { + let inner = before[1..before.len() - 1].trim(); + if !inner.is_empty() && !inner.contains(',') { + return Some(inner); + } + } else if !before.is_empty() + && !before.contains(' ') + && !before.contains('(') + && before + .chars() + .all(|c| c.is_alphanumeric() || c == '_' || c == '$') + { + return Some(before); + } + None +} + +pub(super) struct ToXmlResult { + pub xml: String, + pub context: Vec, +} + +impl<'a> FromV8<'a> for ToXmlResult { + type Error = NixRuntimeError; + + fn from_v8( + scope: &mut v8::PinScope<'a, '_>, + value: v8::Local<'a, v8::Value>, + ) -> std::result::Result { + let global = scope.get_current_context().global(scope); + let nix_key = v8::String::new(scope, "Nix").ok_or("v8 string" )?; + let nix_obj = global + .get(scope, nix_key.into()) + .ok_or("no Nix global" )? + .to_object(scope) + .ok_or("Nix not object" )?; + + let ctx = XmlCtx::new(scope, nix_obj)?; + + let mut writer = XmlWriter::new(); + writer + .buf + .push_str("\n\n"); + writer.write_value(value, scope, &ctx, 1)?; + writer.buf.push_str("\n"); + + Ok(ToXmlResult { + xml: writer.buf, + context: writer.context, + }) + } +} + +#[deno_core::op2] +#[serde] +pub(super) fn op_to_xml(#[scoped] value: ToXmlResult) -> (String, Vec) { + (value.xml, value.context) +} diff --git a/nix-js/tests/lang.rs b/nix-js/tests/lang.rs index 666784f..96ce500 100644 --- a/nix-js/tests/lang.rs +++ b/nix-js/tests/lang.rs @@ -241,14 +241,8 @@ eval_okay_test!( tail_call_1 ); eval_okay_test!(tojson); -eval_okay_test!( - #[ignore = "not implemented: toXML"] - toxml -); -eval_okay_test!( - #[ignore = "not implemented: toXML"] - toxml2 -); +eval_okay_test!(toxml); +eval_okay_test!(toxml2); eval_okay_test!(tryeval); eval_okay_test!(types); eval_okay_test!(versions);