diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index 2256d48a3d..b21ead3046 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -44,6 +44,16 @@ pub const BunObject = struct { pub const zstdDecompressSync = toJSCallback(JSZstd.decompressSync); pub const zstdCompress = toJSCallback(JSZstd.compress); pub const zstdDecompress = toJSCallback(JSZstd.decompress); + + // Case conversion functions + pub const camelCase = toJSCallback(CaseConvert.jsCamelCase); + pub const pascalCase = toJSCallback(CaseConvert.jsPascalCase); + pub const snakeCase = toJSCallback(CaseConvert.jsSnakeCase); + pub const kebabCase = toJSCallback(CaseConvert.jsKebabCase); + pub const constantCase = toJSCallback(CaseConvert.jsConstantCase); + pub const dotCase = toJSCallback(CaseConvert.jsDotCase); + pub const capitalCase = toJSCallback(CaseConvert.jsCapitalCase); + pub const trainCase = toJSCallback(CaseConvert.jsTrainCase); // --- Callbacks --- @@ -180,6 +190,16 @@ pub const BunObject = struct { @export(&BunObject.zstdDecompressSync, .{ .name = callbackName("zstdDecompressSync") }); @export(&BunObject.zstdCompress, .{ .name = callbackName("zstdCompress") }); @export(&BunObject.zstdDecompress, .{ .name = callbackName("zstdDecompress") }); + + // Case conversion exports + @export(&BunObject.camelCase, .{ .name = callbackName("camelCase") }); + @export(&BunObject.pascalCase, .{ .name = callbackName("pascalCase") }); + @export(&BunObject.snakeCase, .{ .name = callbackName("snakeCase") }); + @export(&BunObject.kebabCase, .{ .name = callbackName("kebabCase") }); + @export(&BunObject.constantCase, .{ .name = callbackName("constantCase") }); + @export(&BunObject.dotCase, .{ .name = callbackName("dotCase") }); + @export(&BunObject.capitalCase, .{ .name = callbackName("capitalCase") }); + @export(&BunObject.trainCase, .{ .name = callbackName("trainCase") }); // --- Callbacks --- // --- LazyProperty initializers --- @@ -2070,6 +2090,7 @@ pub fn createBunStdout(globalThis: *jsc.JSGlobalObject) callconv(.C) jsc.JSValue } const Braces = @import("../../shell/braces.zig"); +const CaseConvert = @import("./CaseConvert.zig"); const Which = @import("../../which.zig"); const options = @import("../../options.zig"); const std = @import("std"); diff --git a/src/bun.js/api/CaseConvert.zig b/src/bun.js/api/CaseConvert.zig new file mode 100644 index 0000000000..601d3fd1ab --- /dev/null +++ b/src/bun.js/api/CaseConvert.zig @@ -0,0 +1,378 @@ +const std = @import("std"); +const bun = @import("bun"); +const jsc = bun.jsc; +const JSValue = jsc.JSValue; + +/// Check if a character is a word boundary (not alphanumeric) +fn isWordBoundary(c: u8) bool { + return !std.ascii.isAlphanumeric(c); +} + +/// Check if character is uppercase +fn isUpper(c: u8) bool { + return c >= 'A' and c <= 'Z'; +} + +/// Check if character is lowercase +fn isLower(c: u8) bool { + return c >= 'a' and c <= 'z'; +} + +/// Convert character to uppercase +fn toUpper(c: u8) u8 { + if (isLower(c)) { + return c - 32; + } + return c; +} + +/// Convert character to lowercase +fn toLower(c: u8) u8 { + if (isUpper(c)) { + return c + 32; + } + return c; +} + +/// Split a string into words based on various delimiters and case changes +fn splitIntoWords(allocator: std.mem.Allocator, input: []const u8) !std.ArrayList([]const u8) { + var words = std.ArrayList([]const u8).init(allocator); + errdefer words.deinit(); + + if (input.len == 0) return words; + + var start: usize = 0; + var i: usize = 0; + + while (i < input.len) : (i += 1) { + const c = input[i]; + + // Skip non-alphanumeric characters + if (!std.ascii.isAlphanumeric(c)) { + if (i > start) { + try words.append(input[start..i]); + } + start = i + 1; + continue; + } + + // Handle transitions if we're not at the first character + if (i > 0) { + const prev = input[i - 1]; + + // Skip if previous was not alphanumeric (already handled above) + if (std.ascii.isAlphanumeric(prev)) { + const prevIsDigit = std.ascii.isDigit(prev); + const currIsDigit = std.ascii.isDigit(c); + + // Check for transitions that should cause splits + if (!currIsDigit) { + // Split on digit to uppercase letter transition (test123Case -> test123, Case) + if (prevIsDigit and isUpper(c)) { + if (i > start) { + try words.append(input[start..i]); + } + start = i; + } + // Detect lowercase to uppercase transition (camelCase) + else if (!prevIsDigit and isLower(prev) and isUpper(c)) { + if (i > start) { + try words.append(input[start..i]); + } + start = i; + } + // Detect uppercase sequence ending (XMLParser -> XML, Parser) + else if (!prevIsDigit and i < input.len - 1) { + const next = input[i + 1]; + if (isUpper(prev) and isUpper(c) and std.ascii.isAlphanumeric(next) and !std.ascii.isDigit(next) and isLower(next)) { + if (i > start) { + try words.append(input[start..i]); + } + start = i; + } + } + } + } + } + } + + // Add the last word if any + if (start < input.len) { + try words.append(input[start..]); + } + + return words; +} + +/// Convert string to camelCase: "two words" -> "twoWords" +pub fn camelCase(allocator: std.mem.Allocator, input: []const u8) ![]u8 { + const words = try splitIntoWords(allocator, input); + defer words.deinit(); + + if (words.items.len == 0) return try allocator.alloc(u8, 0); + + var result = std.ArrayList(u8).init(allocator); + errdefer result.deinit(); + + for (words.items, 0..) |word, idx| { + if (word.len == 0) continue; + + if (idx == 0) { + // First word is all lowercase + for (word) |c| { + try result.append(toLower(c)); + } + } else { + // Subsequent words: capitalize first letter, lowercase rest + try result.append(toUpper(word[0])); + for (word[1..]) |c| { + try result.append(toLower(c)); + } + } + } + + return result.toOwnedSlice(); +} + +/// Convert string to PascalCase: "two words" -> "TwoWords" +pub fn pascalCase(allocator: std.mem.Allocator, input: []const u8) ![]u8 { + const words = try splitIntoWords(allocator, input); + defer words.deinit(); + + if (words.items.len == 0) return try allocator.alloc(u8, 0); + + var result = std.ArrayList(u8).init(allocator); + errdefer result.deinit(); + + for (words.items) |word| { + if (word.len == 0) continue; + + // Capitalize first letter, lowercase rest + try result.append(toUpper(word[0])); + for (word[1..]) |c| { + try result.append(toLower(c)); + } + } + + return result.toOwnedSlice(); +} + +/// Convert string to snake_case: "two words" -> "two_words" +pub fn snakeCase(allocator: std.mem.Allocator, input: []const u8) ![]u8 { + const words = try splitIntoWords(allocator, input); + defer words.deinit(); + + if (words.items.len == 0) return try allocator.alloc(u8, 0); + + var result = std.ArrayList(u8).init(allocator); + errdefer result.deinit(); + + for (words.items, 0..) |word, idx| { + if (word.len == 0) continue; + + if (idx > 0) { + try result.append('_'); + } + + for (word) |c| { + try result.append(toLower(c)); + } + } + + return result.toOwnedSlice(); +} + +/// Convert string to kebab-case: "two words" -> "two-words" +pub fn kebabCase(allocator: std.mem.Allocator, input: []const u8) ![]u8 { + const words = try splitIntoWords(allocator, input); + defer words.deinit(); + + if (words.items.len == 0) return try allocator.alloc(u8, 0); + + var result = std.ArrayList(u8).init(allocator); + errdefer result.deinit(); + + for (words.items, 0..) |word, idx| { + if (word.len == 0) continue; + + if (idx > 0) { + try result.append('-'); + } + + for (word) |c| { + try result.append(toLower(c)); + } + } + + return result.toOwnedSlice(); +} + +/// Convert string to CONSTANT_CASE: "two words" -> "TWO_WORDS" +pub fn constantCase(allocator: std.mem.Allocator, input: []const u8) ![]u8 { + const words = try splitIntoWords(allocator, input); + defer words.deinit(); + + if (words.items.len == 0) return try allocator.alloc(u8, 0); + + var result = std.ArrayList(u8).init(allocator); + errdefer result.deinit(); + + for (words.items, 0..) |word, idx| { + if (word.len == 0) continue; + + if (idx > 0) { + try result.append('_'); + } + + for (word) |c| { + try result.append(toUpper(c)); + } + } + + return result.toOwnedSlice(); +} + +/// Convert string to dot.case: "two words" -> "two.words" +pub fn dotCase(allocator: std.mem.Allocator, input: []const u8) ![]u8 { + const words = try splitIntoWords(allocator, input); + defer words.deinit(); + + if (words.items.len == 0) return try allocator.alloc(u8, 0); + + var result = std.ArrayList(u8).init(allocator); + errdefer result.deinit(); + + for (words.items, 0..) |word, idx| { + if (word.len == 0) continue; + + if (idx > 0) { + try result.append('.'); + } + + for (word) |c| { + try result.append(toLower(c)); + } + } + + return result.toOwnedSlice(); +} + +/// Convert string to Capital Case: "two words" -> "Two Words" +pub fn capitalCase(allocator: std.mem.Allocator, input: []const u8) ![]u8 { + const words = try splitIntoWords(allocator, input); + defer words.deinit(); + + if (words.items.len == 0) return try allocator.alloc(u8, 0); + + var result = std.ArrayList(u8).init(allocator); + errdefer result.deinit(); + + for (words.items, 0..) |word, idx| { + if (word.len == 0) continue; + + if (idx > 0) { + try result.append(' '); + } + + // Capitalize first letter, lowercase rest + try result.append(toUpper(word[0])); + for (word[1..]) |c| { + try result.append(toLower(c)); + } + } + + return result.toOwnedSlice(); +} + +/// Convert string to Train-Case: "two words" -> "Two-Words" +pub fn trainCase(allocator: std.mem.Allocator, input: []const u8) ![]u8 { + const words = try splitIntoWords(allocator, input); + defer words.deinit(); + + if (words.items.len == 0) return try allocator.alloc(u8, 0); + + var result = std.ArrayList(u8).init(allocator); + errdefer result.deinit(); + + for (words.items, 0..) |word, idx| { + if (word.len == 0) continue; + + if (idx > 0) { + try result.append('-'); + } + + // Capitalize first letter, lowercase rest + try result.append(toUpper(word[0])); + for (word[1..]) |c| { + try result.append(toLower(c)); + } + } + + return result.toOwnedSlice(); +} + +/// Generic case conversion function that handles string extraction and conversion +fn convertCase(globalThis: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame, comptime converter: fn (std.mem.Allocator, []const u8) anyerror![]u8) bun.JSError!JSValue { + const arguments = callFrame.arguments_old(1); + if (arguments.len < 1) { + return globalThis.throw("expected 1 argument, got 0", .{}); + } + + const input_value = arguments.ptr[0]; + + // Convert to string + const bunstr = try input_value.toBunString(globalThis); + if (globalThis.hasException()) return .zero; + defer bunstr.deref(); + + // Get UTF8 bytes + const allocator = bun.default_allocator; + const utf8_slice = bunstr.toUTF8(allocator); + defer utf8_slice.deinit(); + + // Apply the conversion + const result_bytes = converter(allocator, utf8_slice.slice()) catch |err| { + if (err == error.OutOfMemory) { + return globalThis.throwOutOfMemory(); + } + return globalThis.throw("case conversion failed", .{}); + }; + defer allocator.free(result_bytes); + + // Create a new string from the result + var result_str = bun.String.cloneUTF8(result_bytes); + return result_str.transferToJS(globalThis); +} + +// JavaScript-exposed functions +pub fn jsCamelCase(globalThis: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) bun.JSError!JSValue { + return convertCase(globalThis, callFrame, camelCase); +} + +pub fn jsPascalCase(globalThis: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) bun.JSError!JSValue { + return convertCase(globalThis, callFrame, pascalCase); +} + +pub fn jsSnakeCase(globalThis: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) bun.JSError!JSValue { + return convertCase(globalThis, callFrame, snakeCase); +} + +pub fn jsKebabCase(globalThis: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) bun.JSError!JSValue { + return convertCase(globalThis, callFrame, kebabCase); +} + +pub fn jsConstantCase(globalThis: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) bun.JSError!JSValue { + return convertCase(globalThis, callFrame, constantCase); +} + +pub fn jsDotCase(globalThis: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) bun.JSError!JSValue { + return convertCase(globalThis, callFrame, dotCase); +} + +pub fn jsCapitalCase(globalThis: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) bun.JSError!JSValue { + return convertCase(globalThis, callFrame, capitalCase); +} + +pub fn jsTrainCase(globalThis: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) bun.JSError!JSValue { + return convertCase(globalThis, callFrame, trainCase); +} \ No newline at end of file diff --git a/src/bun.js/bindings/BunObject+exports.h b/src/bun.js/bindings/BunObject+exports.h index 6f1dbf252c..c311d277e3 100644 --- a/src/bun.js/bindings/BunObject+exports.h +++ b/src/bun.js/bindings/BunObject+exports.h @@ -75,6 +75,14 @@ macro(zstdDecompressSync) \ macro(zstdCompress) \ macro(zstdDecompress) \ + macro(camelCase) \ + macro(pascalCase) \ + macro(snakeCase) \ + macro(kebabCase) \ + macro(constantCase) \ + macro(dotCase) \ + macro(capitalCase) \ + macro(trainCase) \ #define DECLARE_ZIG_BUN_OBJECT_CALLBACK(name) BUN_DECLARE_HOST_FUNCTION(BunObject_callback_##name); FOR_EACH_CALLBACK(DECLARE_ZIG_BUN_OBJECT_CALLBACK); diff --git a/src/bun.js/bindings/BunObject.cpp b/src/bun.js/bindings/BunObject.cpp index 9d0fd7eea1..a17f902f66 100644 --- a/src/bun.js/bindings/BunObject.cpp +++ b/src/bun.js/bindings/BunObject.cpp @@ -807,6 +807,14 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj zstdDecompressSync BunObject_callback_zstdDecompressSync DontDelete|Function 1 zstdCompress BunObject_callback_zstdCompress DontDelete|Function 1 zstdDecompress BunObject_callback_zstdDecompress DontDelete|Function 1 + camelCase BunObject_callback_camelCase DontDelete|Function 1 + pascalCase BunObject_callback_pascalCase DontDelete|Function 1 + snakeCase BunObject_callback_snakeCase DontDelete|Function 1 + kebabCase BunObject_callback_kebabCase DontDelete|Function 1 + constantCase BunObject_callback_constantCase DontDelete|Function 1 + dotCase BunObject_callback_dotCase DontDelete|Function 1 + capitalCase BunObject_callback_capitalCase DontDelete|Function 1 + trainCase BunObject_callback_trainCase DontDelete|Function 1 @end */ diff --git a/test/js/bun/case-convert.test.ts b/test/js/bun/case-convert.test.ts new file mode 100644 index 0000000000..938ed96504 --- /dev/null +++ b/test/js/bun/case-convert.test.ts @@ -0,0 +1,166 @@ +import { test, expect } from "bun:test"; + +test("Bun.camelCase", () => { + expect(Bun.camelCase("two words")).toBe("twoWords"); + expect(Bun.camelCase("hello world")).toBe("helloWorld"); + expect(Bun.camelCase("HELLO_WORLD")).toBe("helloWorld"); + expect(Bun.camelCase("kebab-case")).toBe("kebabCase"); + expect(Bun.camelCase("snake_case")).toBe("snakeCase"); + expect(Bun.camelCase("PascalCase")).toBe("pascalCase"); + expect(Bun.camelCase("multiple spaces")).toBe("multipleSpaces"); + expect(Bun.camelCase("123-numbers-456")).toBe("123Numbers456"); + expect(Bun.camelCase("")).toBe(""); + expect(Bun.camelCase("alreadyCamelCase")).toBe("alreadyCamelCase"); + expect(Bun.camelCase("XML-Parser")).toBe("xmlParser"); + expect(Bun.camelCase("XMLParser")).toBe("xmlParser"); +}); + +test("Bun.pascalCase", () => { + expect(Bun.pascalCase("two words")).toBe("TwoWords"); + expect(Bun.pascalCase("hello world")).toBe("HelloWorld"); + expect(Bun.pascalCase("HELLO_WORLD")).toBe("HelloWorld"); + expect(Bun.pascalCase("kebab-case")).toBe("KebabCase"); + expect(Bun.pascalCase("snake_case")).toBe("SnakeCase"); + expect(Bun.pascalCase("camelCase")).toBe("CamelCase"); + expect(Bun.pascalCase("multiple spaces")).toBe("MultipleSpaces"); + expect(Bun.pascalCase("123-numbers-456")).toBe("123Numbers456"); + expect(Bun.pascalCase("")).toBe(""); + expect(Bun.pascalCase("AlreadyPascalCase")).toBe("AlreadyPascalCase"); + expect(Bun.pascalCase("xml-parser")).toBe("XmlParser"); + expect(Bun.pascalCase("XMLParser")).toBe("XmlParser"); +}); + +test("Bun.snakeCase", () => { + expect(Bun.snakeCase("two words")).toBe("two_words"); + expect(Bun.snakeCase("hello world")).toBe("hello_world"); + expect(Bun.snakeCase("HELLO_WORLD")).toBe("hello_world"); + expect(Bun.snakeCase("kebab-case")).toBe("kebab_case"); + expect(Bun.snakeCase("camelCase")).toBe("camel_case"); + expect(Bun.snakeCase("PascalCase")).toBe("pascal_case"); + expect(Bun.snakeCase("multiple spaces")).toBe("multiple_spaces"); + expect(Bun.snakeCase("123-numbers-456")).toBe("123_numbers_456"); + expect(Bun.snakeCase("")).toBe(""); + expect(Bun.snakeCase("already_snake_case")).toBe("already_snake_case"); + expect(Bun.snakeCase("XMLParser")).toBe("xml_parser"); +}); + +test("Bun.kebabCase", () => { + expect(Bun.kebabCase("two words")).toBe("two-words"); + expect(Bun.kebabCase("hello world")).toBe("hello-world"); + expect(Bun.kebabCase("HELLO_WORLD")).toBe("hello-world"); + expect(Bun.kebabCase("snake_case")).toBe("snake-case"); + expect(Bun.kebabCase("camelCase")).toBe("camel-case"); + expect(Bun.kebabCase("PascalCase")).toBe("pascal-case"); + expect(Bun.kebabCase("multiple spaces")).toBe("multiple-spaces"); + expect(Bun.kebabCase("123-numbers-456")).toBe("123-numbers-456"); + expect(Bun.kebabCase("")).toBe(""); + expect(Bun.kebabCase("already-kebab-case")).toBe("already-kebab-case"); + expect(Bun.kebabCase("XMLParser")).toBe("xml-parser"); +}); + +test("Bun.constantCase", () => { + expect(Bun.constantCase("two words")).toBe("TWO_WORDS"); + expect(Bun.constantCase("hello world")).toBe("HELLO_WORLD"); + expect(Bun.constantCase("hello_world")).toBe("HELLO_WORLD"); + expect(Bun.constantCase("kebab-case")).toBe("KEBAB_CASE"); + expect(Bun.constantCase("camelCase")).toBe("CAMEL_CASE"); + expect(Bun.constantCase("PascalCase")).toBe("PASCAL_CASE"); + expect(Bun.constantCase("multiple spaces")).toBe("MULTIPLE_SPACES"); + expect(Bun.constantCase("123-numbers-456")).toBe("123_NUMBERS_456"); + expect(Bun.constantCase("")).toBe(""); + expect(Bun.constantCase("ALREADY_CONSTANT_CASE")).toBe("ALREADY_CONSTANT_CASE"); + expect(Bun.constantCase("XMLParser")).toBe("XML_PARSER"); +}); + +test("Bun.dotCase", () => { + expect(Bun.dotCase("two words")).toBe("two.words"); + expect(Bun.dotCase("hello world")).toBe("hello.world"); + expect(Bun.dotCase("HELLO_WORLD")).toBe("hello.world"); + expect(Bun.dotCase("kebab-case")).toBe("kebab.case"); + expect(Bun.dotCase("camelCase")).toBe("camel.case"); + expect(Bun.dotCase("PascalCase")).toBe("pascal.case"); + expect(Bun.dotCase("multiple spaces")).toBe("multiple.spaces"); + expect(Bun.dotCase("123-numbers-456")).toBe("123.numbers.456"); + expect(Bun.dotCase("")).toBe(""); + expect(Bun.dotCase("already.dot.case")).toBe("already.dot.case"); + expect(Bun.dotCase("XMLParser")).toBe("xml.parser"); +}); + +test("Bun.capitalCase", () => { + expect(Bun.capitalCase("two words")).toBe("Two Words"); + expect(Bun.capitalCase("hello world")).toBe("Hello World"); + expect(Bun.capitalCase("HELLO_WORLD")).toBe("Hello World"); + expect(Bun.capitalCase("kebab-case")).toBe("Kebab Case"); + expect(Bun.capitalCase("camelCase")).toBe("Camel Case"); + expect(Bun.capitalCase("PascalCase")).toBe("Pascal Case"); + expect(Bun.capitalCase("multiple spaces")).toBe("Multiple Spaces"); + expect(Bun.capitalCase("123-numbers-456")).toBe("123 Numbers 456"); + expect(Bun.capitalCase("")).toBe(""); + expect(Bun.capitalCase("already Capital Case")).toBe("Already Capital Case"); + expect(Bun.capitalCase("XMLParser")).toBe("Xml Parser"); +}); + +test("Bun.trainCase", () => { + expect(Bun.trainCase("two words")).toBe("Two-Words"); + expect(Bun.trainCase("hello world")).toBe("Hello-World"); + expect(Bun.trainCase("HELLO_WORLD")).toBe("Hello-World"); + expect(Bun.trainCase("kebab-case")).toBe("Kebab-Case"); + expect(Bun.trainCase("camelCase")).toBe("Camel-Case"); + expect(Bun.trainCase("PascalCase")).toBe("Pascal-Case"); + expect(Bun.trainCase("multiple spaces")).toBe("Multiple-Spaces"); + expect(Bun.trainCase("123-numbers-456")).toBe("123-Numbers-456"); + expect(Bun.trainCase("")).toBe(""); + expect(Bun.trainCase("Already-Train-Case")).toBe("Already-Train-Case"); + expect(Bun.trainCase("XMLParser")).toBe("Xml-Parser"); +}); + +test("case conversion with special characters", () => { + const input = "hello@world#test!"; + expect(Bun.camelCase(input)).toBe("helloWorldTest"); + expect(Bun.pascalCase(input)).toBe("HelloWorldTest"); + expect(Bun.snakeCase(input)).toBe("hello_world_test"); + expect(Bun.kebabCase(input)).toBe("hello-world-test"); + expect(Bun.constantCase(input)).toBe("HELLO_WORLD_TEST"); + expect(Bun.dotCase(input)).toBe("hello.world.test"); + expect(Bun.capitalCase(input)).toBe("Hello World Test"); + expect(Bun.trainCase(input)).toBe("Hello-World-Test"); +}); + +test("case conversion with numbers", () => { + // Numbers stay with adjacent letters unless there's a case change + const input = "test123case456"; + expect(Bun.camelCase(input)).toBe("test123case456"); + expect(Bun.pascalCase(input)).toBe("Test123case456"); + expect(Bun.snakeCase(input)).toBe("test123case456"); + expect(Bun.kebabCase(input)).toBe("test123case456"); + expect(Bun.constantCase(input)).toBe("TEST123CASE456"); + expect(Bun.dotCase(input)).toBe("test123case456"); + expect(Bun.capitalCase(input)).toBe("Test123case456"); + expect(Bun.trainCase(input)).toBe("Test123case456"); + + // When there's a case change after numbers, it splits + const input2 = "test123Case456"; + expect(Bun.camelCase(input2)).toBe("test123Case456"); + expect(Bun.snakeCase(input2)).toBe("test123_case456"); + expect(Bun.kebabCase(input2)).toBe("test123-case456"); +}); + +test("case conversion with non-strings", () => { + // Should convert to string first + expect(Bun.camelCase(123)).toBe("123"); + expect(Bun.camelCase(true)).toBe("true"); + expect(Bun.camelCase(null)).toBe("null"); + expect(Bun.camelCase(undefined)).toBe("undefined"); +}); + +test("case conversion error handling", () => { + // Should throw when no arguments provided + expect(() => (Bun as any).camelCase()).toThrow(); + expect(() => (Bun as any).pascalCase()).toThrow(); + expect(() => (Bun as any).snakeCase()).toThrow(); + expect(() => (Bun as any).kebabCase()).toThrow(); + expect(() => (Bun as any).constantCase()).toThrow(); + expect(() => (Bun as any).dotCase()).toThrow(); + expect(() => (Bun as any).capitalCase()).toThrow(); + expect(() => (Bun as any).trainCase()).toThrow(); +}); \ No newline at end of file