feat: implement fromTOML; fix fromJSON implementation
This commit is contained in:
55
Cargo.lock
generated
55
Cargo.lock
generated
@@ -1966,6 +1966,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"xz2",
|
||||
@@ -2280,7 +2281,7 @@ version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
|
||||
dependencies = [
|
||||
"toml_edit",
|
||||
"toml_edit 0.23.10+spec-1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2824,6 +2825,15 @@ dependencies = [
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
@@ -3358,6 +3368,27 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_edit 0.22.27",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.7.5+spec-1.1.0"
|
||||
@@ -3367,6 +3398,20 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_write",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.23.10+spec-1.0.0"
|
||||
@@ -3374,7 +3419,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
"toml_datetime 0.7.5+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"winnow",
|
||||
]
|
||||
@@ -3388,6 +3433,12 @@ dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.2"
|
||||
|
||||
@@ -9,6 +9,7 @@ let
|
||||
flake = (
|
||||
import flake-compat {
|
||||
src = ./.;
|
||||
copySourceTreeToStore = false;
|
||||
}
|
||||
);
|
||||
in
|
||||
|
||||
@@ -49,6 +49,7 @@ bzip2 = "0.5"
|
||||
zip = "2.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
toml = "0.8"
|
||||
dirs = "5.0"
|
||||
tempfile = "3.24"
|
||||
rusqlite = { version = "0.33", features = ["bundled"] }
|
||||
|
||||
@@ -6,55 +6,22 @@ import type { NixString, NixValue } from "../types";
|
||||
import { isStringWithContext, isNixPath } from "../types";
|
||||
import { force } from "../thunk";
|
||||
import { type NixStringContext, mkStringWithContext, addBuiltContext } from "../string-context";
|
||||
import { forceFunction } from "../type-assert";
|
||||
import { forceFunction, forceStringNoCtx } from "../type-assert";
|
||||
import { nixValueToJson } from "../conversion";
|
||||
import { isAttrs, isPath, typeOf } from "./type-check";
|
||||
|
||||
const convertJsonToNix = (json: unknown): NixValue => {
|
||||
if (json === null) {
|
||||
return null;
|
||||
}
|
||||
if (typeof json === "boolean") {
|
||||
return json;
|
||||
}
|
||||
if (typeof json === "number") {
|
||||
if (Number.isInteger(json)) {
|
||||
return BigInt(json);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
if (typeof json === "string") {
|
||||
return json;
|
||||
}
|
||||
if (Array.isArray(json)) {
|
||||
return json.map(convertJsonToNix);
|
||||
}
|
||||
if (typeof json === "object") {
|
||||
const result: Record<string, NixValue> = {};
|
||||
for (const [key, value] of Object.entries(json)) {
|
||||
result[key] = convertJsonToNix(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
throw new TypeError(`unsupported JSON value type: ${typeof json}`);
|
||||
};
|
||||
|
||||
export const fromJSON = (e: NixValue): NixValue => {
|
||||
const str = force(e);
|
||||
if (typeof str !== "string" && !isStringWithContext(str)) {
|
||||
throw new TypeError(`builtins.fromJSON: expected a string, got ${typeOf(str)}`);
|
||||
}
|
||||
const jsonStr = isStringWithContext(str) ? str.value : str;
|
||||
try {
|
||||
const parsed = JSON.parse(jsonStr);
|
||||
return convertJsonToNix(parsed);
|
||||
} catch (err) {
|
||||
throw new SyntaxError(`builtins.fromJSON: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
return Deno.core.ops.op_from_json(jsonStr);
|
||||
};
|
||||
|
||||
export const fromTOML = (e: NixValue): never => {
|
||||
throw new Error("Not implemented: fromTOML");
|
||||
export const fromTOML = (e: NixValue): NixValue => {
|
||||
const toml = forceStringNoCtx(e);
|
||||
return Deno.core.ops.op_from_toml(toml);
|
||||
};
|
||||
|
||||
export const toJSON = (e: NixValue): NixString => {
|
||||
|
||||
@@ -41,11 +41,12 @@ export const nixValueToJson = (
|
||||
}
|
||||
}
|
||||
|
||||
if (seen.has(v)) {
|
||||
throw new Error("cycle detected in toJSON");
|
||||
} else {
|
||||
seen.add(v)
|
||||
}
|
||||
// FIXME: is this check necessary?
|
||||
// if (seen.has(v)) {
|
||||
// throw new Error("cycle detected in toJSON");
|
||||
// } else {
|
||||
// seen.add(v)
|
||||
// }
|
||||
|
||||
if (Array.isArray(v)) {
|
||||
return v.map((item) => nixValueToJson(item, strict, outContext, copyToStore, seen));
|
||||
|
||||
2
nix-js/runtime-ts/src/types/global.d.ts
vendored
2
nix-js/runtime-ts/src/types/global.d.ts
vendored
@@ -74,6 +74,8 @@ declare global {
|
||||
): string;
|
||||
function op_match(regex: string, text: string): (string | null)[] | null;
|
||||
function op_split(regex: string, text: string): (string | (string | null)[])[];
|
||||
function op_from_json(json: string): any;
|
||||
function op_from_toml(toml: string): any;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,9 +183,7 @@ fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<(), std::io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn extract_tarball_to_temp(
|
||||
data: &[u8],
|
||||
) -> Result<(PathBuf, tempfile::TempDir), ArchiveError> {
|
||||
pub fn extract_tarball_to_temp(data: &[u8]) -> Result<(PathBuf, tempfile::TempDir), ArchiveError> {
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let extracted_path = extract_archive(data, temp_dir.path())?;
|
||||
Ok((extracted_path, temp_dir))
|
||||
|
||||
@@ -69,6 +69,8 @@ fn runtime_extension<Ctx: RuntimeContext>() -> Extension {
|
||||
op_add_filtered_path::<Ctx>(),
|
||||
op_match(),
|
||||
op_split(),
|
||||
op_from_json(),
|
||||
op_from_toml(),
|
||||
];
|
||||
ops.extend(crate::fetcher::register_ops::<Ctx>());
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::str::FromStr;
|
||||
use hashbrown::hash_map::{Entry, HashMap};
|
||||
|
||||
use deno_core::OpState;
|
||||
use deno_core::v8;
|
||||
use regex::Regex;
|
||||
use rust_embed::Embed;
|
||||
|
||||
@@ -1039,3 +1040,118 @@ pub(super) enum SplitResult {
|
||||
Text(String),
|
||||
Captures(Vec<Option<String>>),
|
||||
}
|
||||
|
||||
pub(super) enum NixJsonValue {
|
||||
Null,
|
||||
Bool(bool),
|
||||
Int(i64),
|
||||
Float(f64),
|
||||
Str(String),
|
||||
Arr(Vec<NixJsonValue>),
|
||||
Obj(Vec<(String, NixJsonValue)>),
|
||||
}
|
||||
|
||||
impl<'a> deno_core::convert::ToV8<'a> for NixJsonValue {
|
||||
type Error = deno_error::JsErrorBox;
|
||||
|
||||
fn to_v8<'i>(
|
||||
self,
|
||||
scope: &mut v8::PinScope<'a, 'i>,
|
||||
) -> std::result::Result<v8::Local<'a, v8::Value>, 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()),
|
||||
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")),
|
||||
Self::Arr(arr) => {
|
||||
let elements = arr
|
||||
.into_iter()
|
||||
.map(|v| v.to_v8(scope))
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
Ok(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::Value> = v8::String::new(scope, &k)
|
||||
.ok_or_else(|| {
|
||||
deno_error::JsErrorBox::type_error("failed to create v8 string")
|
||||
})?
|
||||
.into();
|
||||
let val = v.to_v8(scope)?;
|
||||
obj.set(scope, key, val);
|
||||
}
|
||||
Ok(obj.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn json_to_nix(value: serde_json::Value) -> NixJsonValue {
|
||||
match value {
|
||||
serde_json::Value::Null => NixJsonValue::Null,
|
||||
serde_json::Value::Bool(b) => NixJsonValue::Bool(b),
|
||||
serde_json::Value::Number(n) => {
|
||||
if let Some(i) = n.as_i64() {
|
||||
NixJsonValue::Int(i)
|
||||
} else if let Some(f) = n.as_f64() {
|
||||
NixJsonValue::Float(f)
|
||||
} else {
|
||||
NixJsonValue::Float(n.as_u64().unwrap_or(0) as f64)
|
||||
}
|
||||
}
|
||||
serde_json::Value::String(s) => NixJsonValue::Str(s),
|
||||
serde_json::Value::Array(arr) => {
|
||||
NixJsonValue::Arr(arr.into_iter().map(json_to_nix).collect())
|
||||
}
|
||||
serde_json::Value::Object(map) => {
|
||||
NixJsonValue::Obj(map.into_iter().map(|(k, v)| (k, json_to_nix(v))).collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn toml_to_nix(value: toml::Value) -> std::result::Result<NixJsonValue, NixRuntimeError> {
|
||||
match value {
|
||||
toml::Value::String(s) => Ok(NixJsonValue::Str(s)),
|
||||
toml::Value::Integer(i) => Ok(NixJsonValue::Int(i)),
|
||||
toml::Value::Float(f) => Ok(NixJsonValue::Float(f)),
|
||||
toml::Value::Boolean(b) => Ok(NixJsonValue::Bool(b)),
|
||||
toml::Value::Datetime(_) => Err(NixRuntimeError::from(
|
||||
"while parsing TOML: Dates and times are not supported",
|
||||
)),
|
||||
toml::Value::Array(arr) => {
|
||||
let items: std::result::Result<Vec<_>, _> = arr.into_iter().map(toml_to_nix).collect();
|
||||
Ok(NixJsonValue::Arr(items?))
|
||||
}
|
||||
toml::Value::Table(table) => {
|
||||
let entries: std::result::Result<Vec<_>, _> = table
|
||||
.into_iter()
|
||||
.map(|(k, v)| toml_to_nix(v).map(|v| (k, v)))
|
||||
.collect();
|
||||
Ok(NixJsonValue::Obj(entries?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[deno_core::op2]
|
||||
#[to_v8]
|
||||
pub(super) fn op_from_json(
|
||||
#[string] json_str: String,
|
||||
) -> std::result::Result<NixJsonValue, NixRuntimeError> {
|
||||
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]
|
||||
#[to_v8]
|
||||
pub(super) fn op_from_toml(
|
||||
#[string] toml_str: String,
|
||||
) -> std::result::Result<NixJsonValue, NixRuntimeError> {
|
||||
let parsed: toml::Value = toml::from_str(&toml_str)
|
||||
.map_err(|e| NixRuntimeError::from(format!("while parsing TOML: {e}")))?;
|
||||
toml_to_nix(parsed)
|
||||
}
|
||||
|
||||
@@ -268,12 +268,66 @@ fn escape_quote_string(s: &str) -> String {
|
||||
ret
|
||||
}
|
||||
|
||||
/// Format a float matching C's `printf("%g", x)` with default precision 6.
|
||||
fn fmt_nix_float(f: &mut Formatter<'_>, x: f64) -> FmtResult {
|
||||
if !x.is_finite() {
|
||||
return write!(f, "{x}");
|
||||
}
|
||||
if x == 0.0 {
|
||||
return if x.is_sign_negative() {
|
||||
write!(f, "-0")
|
||||
} else {
|
||||
write!(f, "0")
|
||||
};
|
||||
}
|
||||
|
||||
let precision: i32 = 6;
|
||||
let exp = x.abs().log10().floor() as i32;
|
||||
|
||||
let formatted = if exp >= -4 && exp < precision {
|
||||
let decimal_places = (precision - 1 - exp) as usize;
|
||||
format!("{x:.decimal_places$}")
|
||||
} else {
|
||||
let sig_digits = (precision - 1) as usize;
|
||||
let s = format!("{x:.sig_digits$e}");
|
||||
let (mantissa, exp_part) = s
|
||||
.split_once('e')
|
||||
.expect("scientific notation must contain 'e'");
|
||||
let (sign, digits) = if let Some(d) = exp_part.strip_prefix('-') {
|
||||
("-", d)
|
||||
} else if let Some(d) = exp_part.strip_prefix('+') {
|
||||
("+", d)
|
||||
} else {
|
||||
("+", exp_part)
|
||||
};
|
||||
if digits.len() < 2 {
|
||||
format!("{mantissa}e{sign}0{digits}")
|
||||
} else {
|
||||
format!("{mantissa}e{sign}{digits}")
|
||||
}
|
||||
};
|
||||
|
||||
if formatted.contains('.') {
|
||||
if let Some(e_pos) = formatted.find('e') {
|
||||
let trimmed = formatted[..e_pos]
|
||||
.trim_end_matches('0')
|
||||
.trim_end_matches('.');
|
||||
write!(f, "{}{}", trimmed, &formatted[e_pos..])
|
||||
} else {
|
||||
let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
|
||||
write!(f, "{trimmed}")
|
||||
}
|
||||
} else {
|
||||
write!(f, "{formatted}")
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Value {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
use Value::*;
|
||||
match self {
|
||||
&Int(x) => write!(f, "{x}"),
|
||||
&Float(x) => write!(f, "{x}"),
|
||||
&Float(x) => fmt_nix_float(f, x),
|
||||
&Bool(x) => write!(f, "{x}"),
|
||||
Null => write!(f, "null"),
|
||||
String(x) => write!(f, "{}", escape_quote_string(x)),
|
||||
@@ -302,7 +356,7 @@ impl Display for ValueCompatDisplay<'_> {
|
||||
use Value::*;
|
||||
match self.0 {
|
||||
&Int(x) => write!(f, "{x}"),
|
||||
&Float(x) => write!(f, "{x}"),
|
||||
&Float(x) => fmt_nix_float(f, x),
|
||||
&Bool(x) => write!(f, "{x}"),
|
||||
Null => write!(f, "null"),
|
||||
String(x) => write!(f, "{}", escape_quote_string(x)),
|
||||
|
||||
@@ -152,12 +152,9 @@ eval_okay_test!(foldlStrict_lazy_elements);
|
||||
eval_okay_test!(foldlStrict_lazy_initial_accumulator);
|
||||
eval_okay_test!(fromjson);
|
||||
eval_okay_test!(fromjson_escapes);
|
||||
eval_okay_test!(fromTOML);
|
||||
eval_okay_test!(
|
||||
#[ignore = "not implemented: fromTOML"]
|
||||
fromTOML
|
||||
);
|
||||
eval_okay_test!(
|
||||
#[ignore = "not implemented: fromTOML"]
|
||||
#[ignore = "timestamps are not supported"]
|
||||
fromTOML_timestamps
|
||||
);
|
||||
eval_okay_test!(functionargs);
|
||||
|
||||
Reference in New Issue
Block a user