mirror of
https://github.com/oven-sh/bun
synced 2026-02-04 16:08:53 +00:00
Compare commits
21 Commits
dylan/pyth
...
claude/tom
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d7bc20d3c | ||
|
|
f72c094580 | ||
|
|
0481a6bd96 | ||
|
|
ba5de42631 | ||
|
|
c91caa9e59 | ||
|
|
2b8fe0bd2f | ||
|
|
c4648e5cc5 | ||
|
|
bdefd42cf8 | ||
|
|
ab85b8597f | ||
|
|
8e06c89421 | ||
|
|
4e3ee84c87 | ||
|
|
e587194b3a | ||
|
|
40a795daf8 | ||
|
|
10ccb880a4 | ||
|
|
4e2e517164 | ||
|
|
d355895e63 | ||
|
|
6cc83cde47 | ||
|
|
c04ad1e5db | ||
|
|
cbb177129d | ||
|
|
830b7ae66f | ||
|
|
0cd5ce2ac0 |
@@ -753,6 +753,7 @@ src/install/windows-shim/bun_shim_impl.zig
|
||||
src/install/yarn.zig
|
||||
src/interchange.zig
|
||||
src/interchange/json.zig
|
||||
src/interchange/toml_stringify.zig
|
||||
src/interchange/toml.zig
|
||||
src/interchange/toml/lexer.zig
|
||||
src/interchange/yaml.zig
|
||||
|
||||
39
packages/bun-types/bun.d.ts
vendored
39
packages/bun-types/bun.d.ts
vendored
@@ -617,6 +617,45 @@ declare module "bun" {
|
||||
* @returns A JavaScript object
|
||||
*/
|
||||
export function parse(input: string): object;
|
||||
|
||||
/**
|
||||
* Convert a JavaScript object to a TOML string.
|
||||
*
|
||||
* @category Utilities
|
||||
*
|
||||
* @param value The JavaScript object to stringify
|
||||
* @returns A TOML string
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { TOML } from "bun";
|
||||
*
|
||||
* const obj = {
|
||||
* title: "TOML Example",
|
||||
* database: {
|
||||
* server: "192.168.1.1",
|
||||
* ports: [8001, 8001, 8002],
|
||||
* connection_max: 5000,
|
||||
* enabled: true,
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* console.log(TOML.stringify(obj));
|
||||
* // Output:
|
||||
* // title = "TOML Example"
|
||||
* //
|
||||
* // [database]
|
||||
* // server = "192.168.1.1"
|
||||
* // ports = [
|
||||
* // 8001,
|
||||
* // 8001,
|
||||
* // 8002
|
||||
* // ]
|
||||
* // connection_max = 5000
|
||||
* // enabled = true
|
||||
* ```
|
||||
*/
|
||||
export function stringify(value: any): string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pub fn create(globalThis: *jsc.JSGlobalObject) jsc.JSValue {
|
||||
const object = JSValue.createEmptyObject(globalThis, 1);
|
||||
const object = JSValue.createEmptyObject(globalThis, 2);
|
||||
object.put(
|
||||
globalThis,
|
||||
ZigString.static("parse"),
|
||||
@@ -10,6 +10,16 @@ pub fn create(globalThis: *jsc.JSGlobalObject) jsc.JSValue {
|
||||
parse,
|
||||
),
|
||||
);
|
||||
object.put(
|
||||
globalThis,
|
||||
ZigString.static("stringify"),
|
||||
jsc.createCallback(
|
||||
globalThis,
|
||||
ZigString.static("stringify"),
|
||||
1,
|
||||
stringify,
|
||||
),
|
||||
);
|
||||
|
||||
return object;
|
||||
}
|
||||
@@ -22,12 +32,12 @@ pub fn parse(
|
||||
const allocator = arena.allocator();
|
||||
defer arena.deinit();
|
||||
var log = logger.Log.init(default_allocator);
|
||||
const arguments = callframe.arguments_old(1).slice();
|
||||
if (arguments.len == 0 or arguments[0].isEmptyOrUndefinedOrNull()) {
|
||||
const input = callframe.argumentsAsArray(1)[0];
|
||||
if (input.isEmptyOrUndefinedOrNull()) {
|
||||
return globalThis.throwInvalidArguments("Expected a string to parse", .{});
|
||||
}
|
||||
|
||||
var input_slice = try arguments[0].toSlice(globalThis, bun.default_allocator);
|
||||
var input_slice = try input.toSlice(globalThis, bun.default_allocator);
|
||||
defer input_slice.deinit();
|
||||
const source = &logger.Source.initPathString("input.toml", input_slice.slice());
|
||||
const parse_result = TOML.parse(source, &log, allocator, false) catch {
|
||||
@@ -56,6 +66,50 @@ pub fn parse(
|
||||
return out.toJSByParseJSON(globalThis);
|
||||
}
|
||||
|
||||
pub fn stringify(
|
||||
globalThis: *jsc.JSGlobalObject,
|
||||
callframe: *jsc.CallFrame,
|
||||
) bun.JSError!jsc.JSValue {
|
||||
const arguments = callframe.arguments();
|
||||
if (arguments.len == 0) {
|
||||
return globalThis.throwInvalidArguments("Expected a value to stringify", .{});
|
||||
}
|
||||
|
||||
const value = arguments.ptr[0];
|
||||
|
||||
if (value.isUndefined()) {
|
||||
return globalThis.throwInvalidArguments("Cannot stringify undefined value to TOML", .{});
|
||||
}
|
||||
|
||||
if (value.isNull()) {
|
||||
return globalThis.throwInvalidArguments("Cannot stringify null value to TOML", .{});
|
||||
}
|
||||
|
||||
if (value.isSymbol() or value.isFunction()) {
|
||||
return .js_undefined;
|
||||
}
|
||||
|
||||
// Use a temporary allocator for stringification
|
||||
var arena = bun.ArenaAllocator.init(globalThis.allocator());
|
||||
defer arena.deinit();
|
||||
const allocator = arena.allocator();
|
||||
|
||||
const result = toml_stringify.stringify(globalThis, value, allocator) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
error.InvalidValue => return globalThis.throwInvalidArguments("Invalid value for TOML stringification", .{}),
|
||||
error.CircularReference => return globalThis.throwInvalidArguments("Circular reference detected", .{}),
|
||||
error.InvalidKey => return globalThis.throwInvalidArguments("Invalid key for TOML", .{}),
|
||||
error.UnsupportedType => return globalThis.throwInvalidArguments("Unsupported type for TOML stringification", .{}),
|
||||
error.JSError => return globalThis.throwInvalidArguments("JavaScript error occurred", .{}),
|
||||
};
|
||||
|
||||
var out = bun.String.borrowUTF8(result);
|
||||
defer out.deref();
|
||||
return out.toJS(globalThis);
|
||||
}
|
||||
|
||||
const toml_stringify = @import("../../interchange/toml_stringify.zig");
|
||||
|
||||
const bun = @import("bun");
|
||||
const default_allocator = bun.default_allocator;
|
||||
const js_printer = bun.js_printer;
|
||||
|
||||
313
src/interchange/toml_stringify.zig
Normal file
313
src/interchange/toml_stringify.zig
Normal file
@@ -0,0 +1,313 @@
|
||||
pub const TOMLStringifyError = error{
|
||||
OutOfMemory,
|
||||
InvalidValue,
|
||||
CircularReference,
|
||||
InvalidKey,
|
||||
UnsupportedType,
|
||||
JSError,
|
||||
};
|
||||
|
||||
pub const TOMLStringifier = struct {
|
||||
writer: std.ArrayList(u8),
|
||||
allocator: std.mem.Allocator,
|
||||
seen_objects: std.HashMap(*anyopaque, void, std.hash_map.AutoContext(*anyopaque), std.hash_map.default_max_load_percentage),
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) TOMLStringifier {
|
||||
return TOMLStringifier{
|
||||
.writer = std.ArrayList(u8).init(allocator),
|
||||
.allocator = allocator,
|
||||
.seen_objects = std.HashMap(*anyopaque, void, std.hash_map.AutoContext(*anyopaque), std.hash_map.default_max_load_percentage).init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *TOMLStringifier) void {
|
||||
self.writer.deinit();
|
||||
self.seen_objects.deinit();
|
||||
}
|
||||
|
||||
pub fn stringify(self: *TOMLStringifier, globalThis: *JSGlobalObject, value: JSValue) TOMLStringifyError![]const u8 {
|
||||
self.stringifyValue(globalThis, value, "", true) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
else => return error.InvalidValue,
|
||||
};
|
||||
return self.writer.items;
|
||||
}
|
||||
|
||||
fn stringifyValue(self: *TOMLStringifier, globalThis: *JSGlobalObject, value: JSValue, key: []const u8, is_root: bool) anyerror!void {
|
||||
if (value.isNull() or value.isUndefined()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.isBoolean()) {
|
||||
return self.stringifyBoolean(value, key, is_root);
|
||||
}
|
||||
|
||||
if (value.isNumber()) {
|
||||
return self.stringifyNumber(value, key, is_root);
|
||||
}
|
||||
|
||||
if (value.isString()) {
|
||||
return self.stringifyString(globalThis, value, key, is_root);
|
||||
}
|
||||
|
||||
// Check for arrays first before objects since arrays are also objects in JS
|
||||
if (value.jsType() == .Array) {
|
||||
return self.stringifyArray(globalThis, value, key, is_root);
|
||||
}
|
||||
|
||||
if (value.isObject()) {
|
||||
if (is_root) {
|
||||
return self.stringifyRootObject(globalThis, value);
|
||||
} else {
|
||||
// Non-root objects should be handled as tables in the root pass
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return error.UnsupportedType;
|
||||
}
|
||||
|
||||
fn stringifyBoolean(self: *TOMLStringifier, value: JSValue, key: []const u8, is_root: bool) anyerror!void {
|
||||
if (key.len > 0 and !is_root) {
|
||||
try self.stringifyKey(key);
|
||||
try self.writer.appendSlice(" = ");
|
||||
}
|
||||
if (value.toBoolean()) {
|
||||
try self.writer.appendSlice("true");
|
||||
} else {
|
||||
try self.writer.appendSlice("false");
|
||||
}
|
||||
if (!is_root) try self.writer.append('\n');
|
||||
}
|
||||
|
||||
fn stringifyNumber(self: *TOMLStringifier, value: JSValue, key: []const u8, is_root: bool) anyerror!void {
|
||||
if (key.len > 0 and !is_root) {
|
||||
try self.stringifyKey(key);
|
||||
try self.writer.appendSlice(" = ");
|
||||
}
|
||||
|
||||
const num = value.asNumber();
|
||||
|
||||
// Handle special float values
|
||||
if (std.math.isNan(num)) {
|
||||
try self.writer.appendSlice("nan");
|
||||
} else if (std.math.isPositiveInf(num)) {
|
||||
try self.writer.appendSlice("inf");
|
||||
} else if (std.math.isNegativeInf(num)) {
|
||||
try self.writer.appendSlice("-inf");
|
||||
} else if (std.math.floor(num) == num and num >= -9223372036854775808.0 and num <= 9223372036854775807.0) {
|
||||
// Integer
|
||||
try self.writer.writer().print("{d}", .{@as(i64, @intFromFloat(num))});
|
||||
} else {
|
||||
// Float
|
||||
try self.writer.writer().print("{d}", .{num});
|
||||
}
|
||||
|
||||
if (!is_root) try self.writer.append('\n');
|
||||
}
|
||||
|
||||
fn stringifyString(self: *TOMLStringifier, globalThis: *JSGlobalObject, value: JSValue, key: []const u8, is_root: bool) anyerror!void {
|
||||
if (key.len > 0 and !is_root) {
|
||||
try self.stringifyKey(key);
|
||||
try self.writer.appendSlice(" = ");
|
||||
}
|
||||
|
||||
const str = value.toBunString(globalThis) catch return error.JSError;
|
||||
defer str.deref();
|
||||
const slice = str.toSlice(self.allocator);
|
||||
defer slice.deinit();
|
||||
|
||||
try self.stringifyQuotedString(slice.slice());
|
||||
if (!is_root) try self.writer.append('\n');
|
||||
}
|
||||
|
||||
fn stringifyArray(self: *TOMLStringifier, globalThis: *JSGlobalObject, array: JSValue, key: []const u8, is_root: bool) anyerror!void {
|
||||
if (key.len > 0 and !is_root) {
|
||||
try self.stringifyKey(key);
|
||||
try self.writer.appendSlice(" = ");
|
||||
}
|
||||
|
||||
const length = array.getLength(globalThis) catch return error.JSError;
|
||||
|
||||
try self.writer.append('[');
|
||||
|
||||
const is_multiline = length > 3;
|
||||
if (is_multiline) {
|
||||
try self.writer.append('\n');
|
||||
}
|
||||
|
||||
for (0..length) |i| {
|
||||
if (i > 0) {
|
||||
try self.writer.appendSlice(", ");
|
||||
if (is_multiline) {
|
||||
try self.writer.append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
if (is_multiline) {
|
||||
try self.writer.appendSlice(" ");
|
||||
}
|
||||
|
||||
const item = array.getIndex(globalThis, @intCast(i)) catch return error.JSError;
|
||||
try self.stringifyValue(globalThis, item, "", true);
|
||||
}
|
||||
|
||||
if (is_multiline) {
|
||||
try self.writer.append('\n');
|
||||
}
|
||||
|
||||
try self.writer.append(']');
|
||||
if (!is_root) try self.writer.append('\n');
|
||||
}
|
||||
|
||||
fn stringifyRootObject(self: *TOMLStringifier, globalThis: *JSGlobalObject, obj: JSValue) anyerror!void {
|
||||
return self.stringifyObjectAtPath(globalThis, obj, "");
|
||||
}
|
||||
|
||||
fn stringifyObjectAtPath(self: *TOMLStringifier, globalThis: *JSGlobalObject, obj: JSValue, table_path: []const u8) anyerror!void {
|
||||
const obj_val = obj.getObject() orelse return error.InvalidValue;
|
||||
|
||||
// Basic circular reference detection
|
||||
const obj_ptr = @as(*anyopaque, @ptrCast(obj_val));
|
||||
if (self.seen_objects.contains(obj_ptr)) {
|
||||
return error.CircularReference;
|
||||
}
|
||||
try self.seen_objects.put(obj_ptr, {});
|
||||
defer _ = self.seen_objects.remove(obj_ptr);
|
||||
|
||||
// First pass: write simple key-value pairs
|
||||
var iterator = jsc.JSPropertyIterator(.{
|
||||
.skip_empty_name = false,
|
||||
.include_value = true,
|
||||
}).init(globalThis, obj_val) catch return error.JSError;
|
||||
defer iterator.deinit();
|
||||
|
||||
while (try iterator.next()) |prop| {
|
||||
const value = iterator.value;
|
||||
if (value.isNull() or value.isUndefined()) continue;
|
||||
|
||||
const name = prop.toSlice(self.allocator);
|
||||
defer name.deinit();
|
||||
|
||||
// Skip objects for second pass
|
||||
if (value.isObject() and value.jsType() != .Array) continue;
|
||||
|
||||
try self.stringifyValue(globalThis, value, name.slice(), false);
|
||||
}
|
||||
|
||||
// Second pass: write tables (nested objects)
|
||||
var iterator2 = jsc.JSPropertyIterator(.{
|
||||
.skip_empty_name = false,
|
||||
.include_value = true,
|
||||
}).init(globalThis, obj_val) catch return error.JSError;
|
||||
defer iterator2.deinit();
|
||||
|
||||
var has_written_table = false;
|
||||
while (try iterator2.next()) |prop| {
|
||||
const value = iterator2.value;
|
||||
if (!value.isObject() or value.jsType() == .Array) continue;
|
||||
|
||||
if (has_written_table or self.writer.items.len > 0) {
|
||||
try self.writer.append('\n');
|
||||
}
|
||||
has_written_table = true;
|
||||
|
||||
const name = prop.toSlice(self.allocator);
|
||||
defer name.deinit();
|
||||
|
||||
// Build full table path
|
||||
var path_buf = std.ArrayList(u8).init(self.allocator);
|
||||
defer path_buf.deinit();
|
||||
|
||||
if (table_path.len > 0) {
|
||||
try path_buf.appendSlice(table_path);
|
||||
try path_buf.append('.');
|
||||
}
|
||||
try path_buf.appendSlice(name.slice());
|
||||
|
||||
try self.writer.appendSlice("[");
|
||||
// For table paths, handle dotted keys specially
|
||||
try self.stringifyTablePath(path_buf.items);
|
||||
try self.writer.appendSlice("]\n");
|
||||
|
||||
try self.stringifyObjectAtPath(globalThis, value, path_buf.items);
|
||||
}
|
||||
}
|
||||
|
||||
fn stringifyTablePath(self: *TOMLStringifier, path: []const u8) anyerror!void {
|
||||
// Split path by dots and quote each part if needed
|
||||
var it = std.mem.splitScalar(u8, path, '.');
|
||||
var first = true;
|
||||
while (it.next()) |part| {
|
||||
if (!first) {
|
||||
try self.writer.append('.');
|
||||
}
|
||||
first = false;
|
||||
try self.stringifyKey(part);
|
||||
}
|
||||
}
|
||||
|
||||
fn stringifyKey(self: *TOMLStringifier, key: []const u8) anyerror!void {
|
||||
if (key.len == 0) return error.InvalidKey;
|
||||
|
||||
// Check if key needs quoting
|
||||
var needs_quotes = false;
|
||||
|
||||
// Empty key always needs quotes
|
||||
if (key.len == 0) needs_quotes = true;
|
||||
|
||||
// Check for characters that require quoting
|
||||
for (key) |ch| {
|
||||
if (!std.ascii.isAlphanumeric(ch) and ch != '_' and ch != '-') {
|
||||
needs_quotes = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it starts with a number (bare keys can't start with numbers in some contexts)
|
||||
if (key.len > 0 and std.ascii.isDigit(key[0])) {
|
||||
needs_quotes = true;
|
||||
}
|
||||
|
||||
if (needs_quotes) {
|
||||
try self.stringifyQuotedString(key);
|
||||
} else {
|
||||
try self.writer.appendSlice(key);
|
||||
}
|
||||
}
|
||||
|
||||
fn stringifyQuotedString(self: *TOMLStringifier, str: []const u8) anyerror!void {
|
||||
try self.writer.append('"');
|
||||
for (str) |ch| {
|
||||
switch (ch) {
|
||||
'"' => try self.writer.appendSlice("\\\""),
|
||||
'\\' => try self.writer.appendSlice("\\\\"),
|
||||
'\n' => try self.writer.appendSlice("\\n"),
|
||||
'\r' => try self.writer.appendSlice("\\r"),
|
||||
'\t' => try self.writer.appendSlice("\\t"),
|
||||
'\x00'...'\x08', '\x0B', '\x0C', '\x0E'...'\x1F', '\x7F' => {
|
||||
// Control characters need unicode escaping
|
||||
try self.writer.writer().print("\\u{X:0>4}", .{ch});
|
||||
},
|
||||
else => try self.writer.append(ch),
|
||||
}
|
||||
}
|
||||
try self.writer.append('"');
|
||||
}
|
||||
};
|
||||
|
||||
pub fn stringify(globalThis: *JSGlobalObject, value: JSValue, allocator: std.mem.Allocator) TOMLStringifyError![]const u8 {
|
||||
var stringifier = TOMLStringifier.init(allocator);
|
||||
defer stringifier.deinit();
|
||||
const result = try stringifier.stringify(globalThis, value);
|
||||
// Make a copy since the stringifier will be deinitialized
|
||||
const owned_result = allocator.dupe(u8, result) catch return error.OutOfMemory;
|
||||
return owned_result;
|
||||
}
|
||||
|
||||
const bun = @import("bun");
|
||||
const std = @import("std");
|
||||
|
||||
const jsc = bun.jsc;
|
||||
const JSGlobalObject = jsc.JSGlobalObject;
|
||||
const JSValue = jsc.JSValue;
|
||||
@@ -4,7 +4,7 @@
|
||||
" catch bun.outOfMemory()": 0,
|
||||
"!= alloc.ptr": 0,
|
||||
"!= allocator.ptr": 0,
|
||||
".arguments_old(": 276,
|
||||
".arguments_old(": 275,
|
||||
".jsBoolean(false)": 0,
|
||||
".jsBoolean(true)": 0,
|
||||
".stdDir()": 41,
|
||||
|
||||
383
test/js/bun/toml/toml-stringify.test.ts
Normal file
383
test/js/bun/toml/toml-stringify.test.ts
Normal file
@@ -0,0 +1,383 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
describe("Bun.TOML.stringify", () => {
|
||||
test("empty object", () => {
|
||||
expect(Bun.TOML.stringify({})).toBe("");
|
||||
});
|
||||
|
||||
test("basic values", () => {
|
||||
expect(Bun.TOML.stringify({ key: "value" })).toBe('key = "value"\n');
|
||||
expect(Bun.TOML.stringify({ num: 42 })).toBe("num = 42\n");
|
||||
expect(Bun.TOML.stringify({ bool: true })).toBe("bool = true\n");
|
||||
expect(Bun.TOML.stringify({ bool: false })).toBe("bool = false\n");
|
||||
});
|
||||
|
||||
test("special number values", () => {
|
||||
expect(Bun.TOML.stringify({ nan: NaN })).toBe("nan = nan\n");
|
||||
expect(Bun.TOML.stringify({ inf: Infinity })).toBe("inf = inf\n");
|
||||
expect(Bun.TOML.stringify({ ninf: -Infinity })).toBe("ninf = -inf\n");
|
||||
expect(Bun.TOML.stringify({ float: 3.14159 })).toBe("float = 3.14159\n");
|
||||
expect(Bun.TOML.stringify({ zero: 0 })).toBe("zero = 0\n");
|
||||
});
|
||||
|
||||
test("string escaping", () => {
|
||||
expect(Bun.TOML.stringify({ simple: "hello" })).toBe('simple = "hello"\n');
|
||||
expect(Bun.TOML.stringify({ empty: "" })).toBe('empty = ""\n');
|
||||
expect(Bun.TOML.stringify({ quote: 'he said "hello"' })).toBe('quote = "he said \\"hello\\""\n');
|
||||
expect(Bun.TOML.stringify({ backslash: "path\\to\\file" })).toBe('backslash = "path\\\\to\\\\file"\n');
|
||||
expect(Bun.TOML.stringify({ newline: "line1\nline2" })).toBe('newline = "line1\\nline2"\n');
|
||||
expect(Bun.TOML.stringify({ tab: "a\tb" })).toBe('tab = "a\\tb"\n');
|
||||
expect(Bun.TOML.stringify({ carriage: "a\rb" })).toBe('carriage = "a\\rb"\n');
|
||||
});
|
||||
|
||||
test("key quoting", () => {
|
||||
expect(Bun.TOML.stringify({ "simple-key": "value" })).toBe('simple-key = "value"\n');
|
||||
expect(Bun.TOML.stringify({ "key with spaces": "value" })).toBe('"key with spaces" = "value"\n');
|
||||
expect(Bun.TOML.stringify({ "key.with.dots": "value" })).toBe('"key.with.dots" = "value"\n');
|
||||
expect(Bun.TOML.stringify({ "key@#$%": "value" })).toBe('"key@#$%" = "value"\n');
|
||||
});
|
||||
|
||||
test("arrays", () => {
|
||||
expect(Bun.TOML.stringify({ arr: [] })).toBe("arr = []\n");
|
||||
expect(Bun.TOML.stringify({ nums: [1, 2, 3] })).toBe("nums = [1, 2, 3]\n");
|
||||
expect(Bun.TOML.stringify({ strings: ["a", "b"] })).toBe('strings = ["a", "b"]\n');
|
||||
expect(Bun.TOML.stringify({ mixed: [1, "two", true] })).toBe('mixed = [1, "two", true]\n');
|
||||
expect(Bun.TOML.stringify({ bools: [true, false, true] })).toBe("bools = [true, false, true]\n");
|
||||
});
|
||||
|
||||
test("multiline arrays", () => {
|
||||
const longArray = [1, 2, 3, 4, 5];
|
||||
const result = Bun.TOML.stringify({ long: longArray });
|
||||
expect(result).toBe("long = [\n 1, \n 2, \n 3, \n 4, \n 5\n]\n");
|
||||
});
|
||||
|
||||
test("arrays always use consistent multiline formatting for long arrays", () => {
|
||||
const shortArr = [1, 2, 3];
|
||||
const longArr = [1, 2, 3, 4, 5];
|
||||
expect(Bun.TOML.stringify({ shortArr })).toBe("shortArr = [1, 2, 3]\n");
|
||||
expect(Bun.TOML.stringify({ longArr })).toBe("longArr = [\n 1, \n 2, \n 3, \n 4, \n 5\n]\n");
|
||||
});
|
||||
|
||||
test("nested objects become tables", () => {
|
||||
const obj = { name: { first: "John", last: "Doe" } };
|
||||
const result = Bun.TOML.stringify(obj);
|
||||
expect(result).toBe(
|
||||
`
|
||||
[name]
|
||||
first = "John"
|
||||
last = "Doe"
|
||||
`.trim() + "\n",
|
||||
);
|
||||
});
|
||||
|
||||
test("regular tables", () => {
|
||||
const obj = { database: { server: "192.168.1.1", port: 5432 } };
|
||||
const result = Bun.TOML.stringify(obj);
|
||||
expect(result).toBe(
|
||||
`
|
||||
[database]
|
||||
server = "192.168.1.1"
|
||||
port = 5432
|
||||
`.trim() + "\n",
|
||||
);
|
||||
});
|
||||
|
||||
test("mixed simple and table values", () => {
|
||||
const obj = {
|
||||
title: "TOML Example",
|
||||
database: {
|
||||
server: "192.168.1.1",
|
||||
ports: [8001, 8001, 8002],
|
||||
connection_max: 5000,
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
const result = Bun.TOML.stringify(obj);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"title = "TOML Example"
|
||||
|
||||
[database]
|
||||
server = "192.168.1.1"
|
||||
ports = [8001, 8001, 8002]
|
||||
connection_max = 5000
|
||||
enabled = true
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
test("nested objects become separate tables", () => {
|
||||
const obj = {
|
||||
global: "value",
|
||||
section1: {
|
||||
key1: "value1",
|
||||
key2: 42,
|
||||
},
|
||||
section2: {
|
||||
key3: "value3",
|
||||
key4: true,
|
||||
},
|
||||
};
|
||||
const result = Bun.TOML.stringify(obj);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"global = "value"
|
||||
|
||||
[section1]
|
||||
key1 = "value1"
|
||||
key2 = 42
|
||||
|
||||
[section2]
|
||||
key3 = "value3"
|
||||
key4 = true
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
test("round-trip compatibility", () => {
|
||||
const original = {
|
||||
title: "Test Document",
|
||||
number: 42,
|
||||
boolean: true,
|
||||
array: [1, 2, 3],
|
||||
section: {
|
||||
key: "value",
|
||||
nested_number: 123,
|
||||
},
|
||||
};
|
||||
|
||||
const tomlString = Bun.TOML.stringify(original);
|
||||
const parsed = Bun.TOML.parse(tomlString);
|
||||
|
||||
expect(parsed).toEqual(original);
|
||||
});
|
||||
|
||||
test("handles null and undefined values", () => {
|
||||
expect(Bun.TOML.stringify({ key: null })).toBe("");
|
||||
expect(Bun.TOML.stringify({ key: undefined })).toBe("");
|
||||
expect(Bun.TOML.stringify({ a: "value", b: null, c: "value2" })).toBe('a = "value"\nc = "value2"\n');
|
||||
});
|
||||
|
||||
test("error handling", () => {
|
||||
expect(() => Bun.TOML.stringify()).toThrow();
|
||||
expect(() => Bun.TOML.stringify(null)).toThrow();
|
||||
expect(() => Bun.TOML.stringify(undefined)).toThrow();
|
||||
});
|
||||
|
||||
test("simple single-argument API", () => {
|
||||
const obj = { key: "value" };
|
||||
|
||||
// Should work with single argument
|
||||
expect(Bun.TOML.stringify(obj)).toBe('key = "value"\n');
|
||||
});
|
||||
|
||||
test("very deeply nested objects", () => {
|
||||
const obj = {
|
||||
level1: {
|
||||
level2: {
|
||||
level3: {
|
||||
level4: {
|
||||
value: "deep",
|
||||
number: 42,
|
||||
},
|
||||
other: "value",
|
||||
},
|
||||
simple: "test",
|
||||
},
|
||||
another: "branch",
|
||||
},
|
||||
root: "value",
|
||||
};
|
||||
|
||||
const result = Bun.TOML.stringify(obj);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"root = "value"
|
||||
|
||||
[level1]
|
||||
another = "branch"
|
||||
|
||||
[level1.level2]
|
||||
simple = "test"
|
||||
|
||||
[level1.level2.level3]
|
||||
other = "value"
|
||||
|
||||
[level1.level2.level3.level4]
|
||||
value = "deep"
|
||||
number = 42
|
||||
"
|
||||
`);
|
||||
|
||||
// Verify round-trip
|
||||
const parsed = Bun.TOML.parse(result);
|
||||
expect(parsed).toEqual(obj);
|
||||
});
|
||||
|
||||
test("arrays with simple values only", () => {
|
||||
const obj = {
|
||||
metadata: {
|
||||
version: "1.0",
|
||||
tags: ["production", "web"],
|
||||
numbers: [1, 2, 3, 4, 5],
|
||||
},
|
||||
config: {
|
||||
database: {
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
},
|
||||
cache: {
|
||||
enabled: true,
|
||||
ttl: 300,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = Bun.TOML.stringify(obj);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"[metadata]
|
||||
version = "1.0"
|
||||
tags = ["production", "web"]
|
||||
numbers = [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5
|
||||
]
|
||||
|
||||
[config]
|
||||
|
||||
[config.database]
|
||||
host = "localhost"
|
||||
port = 5432
|
||||
|
||||
[config.cache]
|
||||
enabled = true
|
||||
ttl = 300
|
||||
"
|
||||
`);
|
||||
|
||||
// Verify round-trip
|
||||
const parsed = Bun.TOML.parse(result);
|
||||
expect(parsed).toEqual(obj);
|
||||
});
|
||||
|
||||
test("circular reference detection", () => {
|
||||
const obj: any = { name: "test" };
|
||||
obj.self = obj;
|
||||
expect(() => Bun.TOML.stringify(obj)).toThrow();
|
||||
});
|
||||
|
||||
test("complex nested structure", () => {
|
||||
const obj = {
|
||||
title: "Complex TOML Example",
|
||||
owner: {
|
||||
name: "Tom Preston-Werner",
|
||||
dob: "1979-05-27T00:00:00-08:00",
|
||||
},
|
||||
database: {
|
||||
server: "192.168.1.1",
|
||||
ports: [8001, 8001, 8002],
|
||||
connection_max: 5000,
|
||||
enabled: true,
|
||||
},
|
||||
servers: {
|
||||
alpha: {
|
||||
ip: "10.0.0.1",
|
||||
dc: "eqdc10",
|
||||
},
|
||||
beta: {
|
||||
ip: "10.0.0.2",
|
||||
dc: "eqdc10",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = Bun.TOML.stringify(obj);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"title = "Complex TOML Example"
|
||||
|
||||
[owner]
|
||||
name = "Tom Preston-Werner"
|
||||
dob = "1979-05-27T00:00:00-08:00"
|
||||
|
||||
[database]
|
||||
server = "192.168.1.1"
|
||||
ports = [8001, 8001, 8002]
|
||||
connection_max = 5000
|
||||
enabled = true
|
||||
|
||||
[servers]
|
||||
|
||||
[servers.alpha]
|
||||
ip = "10.0.0.1"
|
||||
dc = "eqdc10"
|
||||
|
||||
[servers.beta]
|
||||
ip = "10.0.0.2"
|
||||
dc = "eqdc10"
|
||||
"
|
||||
`);
|
||||
|
||||
// Verify round-trip
|
||||
const parsed = Bun.TOML.parse(result);
|
||||
expect(parsed).toEqual(obj);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Bun.TOML.parse additional tests", () => {
|
||||
test("parse empty string", () => {
|
||||
expect(Bun.TOML.parse("")).toEqual({});
|
||||
});
|
||||
|
||||
test("parse basic values", () => {
|
||||
expect(Bun.TOML.parse('key = "value"')).toEqual({ key: "value" });
|
||||
expect(Bun.TOML.parse("num = 42")).toEqual({ num: 42 });
|
||||
expect(Bun.TOML.parse("bool = true")).toEqual({ bool: true });
|
||||
expect(Bun.TOML.parse("bool = false")).toEqual({ bool: false });
|
||||
});
|
||||
|
||||
test("parse arrays", () => {
|
||||
expect(Bun.TOML.parse("arr = []")).toEqual({ arr: [] });
|
||||
expect(Bun.TOML.parse("nums = [1, 2, 3]")).toEqual({ nums: [1, 2, 3] });
|
||||
expect(Bun.TOML.parse('strings = ["a", "b"]')).toEqual({ strings: ["a", "b"] });
|
||||
});
|
||||
|
||||
test("parse tables", () => {
|
||||
const toml = `
|
||||
[database]
|
||||
server = "192.168.1.1"
|
||||
port = 5432
|
||||
`;
|
||||
expect(Bun.TOML.parse(toml)).toEqual({
|
||||
database: {
|
||||
server: "192.168.1.1",
|
||||
port: 5432,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("parse mixed content", () => {
|
||||
const toml = `
|
||||
title = "Test"
|
||||
version = 1.0
|
||||
|
||||
[database]
|
||||
server = "localhost"
|
||||
enabled = true
|
||||
`;
|
||||
expect(Bun.TOML.parse(toml)).toEqual({
|
||||
title: "Test",
|
||||
version: 1.0,
|
||||
database: {
|
||||
server: "localhost",
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("parse error handling", () => {
|
||||
expect(() => Bun.TOML.parse()).toThrow("Expected a string to parse");
|
||||
expect(() => Bun.TOML.parse(null)).toThrow("Expected a string to parse");
|
||||
expect(() => Bun.TOML.parse(undefined)).toThrow("Expected a string to parse");
|
||||
expect(() => Bun.TOML.parse("invalid toml [")).toThrow();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user