From 97a6d87aae5745e5d83a483e51a8252de356eb16 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sun, 29 Mar 2026 13:01:10 +0200 Subject: [PATCH 1/2] feat(functions-tools): convert camelCase field names to snake_case --- .../src/converters/zod-to-candid.ts | 4 ++-- .../src/converters/zod-to-idl.ts | 6 ++++-- .../src/converters/zod-to-rust.ts | 21 ++++++++++++++----- .../tests/converters/zod-to-candid.spec.ts | 8 +++++++ .../src/tests/converters/zod-to-idl.spec.ts | 8 +++++++ .../src/tests/converters/zod-to-rust.spec.ts | 8 +++++++ 6 files changed, 46 insertions(+), 9 deletions(-) diff --git a/packages/functions-tools/src/converters/zod-to-candid.ts b/packages/functions-tools/src/converters/zod-to-candid.ts index 902cf3e7..4e6f35ad 100644 --- a/packages/functions-tools/src/converters/zod-to-candid.ts +++ b/packages/functions-tools/src/converters/zod-to-candid.ts @@ -1,4 +1,4 @@ -import {capitalize} from '@junobuild/utils'; +import {capitalize, convertCamelToSnake} from '@junobuild/utils'; import type {z} from 'zod'; import {jsonToSputnikSchema, type SputnikSchemaResult} from './_converters'; import type {SputnikSchema} from './_types'; @@ -27,7 +27,7 @@ const sputnikSchemaToDid = (schema: SputnikSchema): string => { if (schema.fields.length === 0) { return 'record {}'; } - return `record { ${schema.fields.map((f) => `${f.name} : ${sputnikSchemaToDid(f.type)}`).join('; ')} }`; + return `record { ${schema.fields.map((f) => `${convertCamelToSnake(f.name)} : ${sputnikSchemaToDid(f.type)}`).join('; ')} }`; case 'tuple': return `record { ${schema.members.map(sputnikSchemaToDid).join('; ')} }`; case 'indexedTuple': diff --git a/packages/functions-tools/src/converters/zod-to-idl.ts b/packages/functions-tools/src/converters/zod-to-idl.ts index 89be6f1e..006eed71 100644 --- a/packages/functions-tools/src/converters/zod-to-idl.ts +++ b/packages/functions-tools/src/converters/zod-to-idl.ts @@ -1,5 +1,5 @@ import {IDL} from '@icp-sdk/core/candid'; -import {capitalize} from '@junobuild/utils'; +import {capitalize, convertCamelToSnake} from '@junobuild/utils'; import type {z} from 'zod'; import {jsonToSputnikSchema, type SputnikSchemaResult} from './_converters'; import type {SputnikSchema} from './_types'; @@ -29,7 +29,9 @@ const schemaToIdlType = (schema: SputnikSchema): IDL.Type => { return IDL.Tuple(...schema.members.map(schemaToIdlType)); case 'record': return IDL.Record( - Object.fromEntries(schema.fields.map((f) => [f.name, schemaToIdlType(f.type)])) + Object.fromEntries( + schema.fields.map((f) => [convertCamelToSnake(f.name), schemaToIdlType(f.type)]) + ) ); case 'variant': return IDL.Variant(Object.fromEntries(schema.tags.map((t) => [t, IDL.Null]))); diff --git a/packages/functions-tools/src/converters/zod-to-rust.ts b/packages/functions-tools/src/converters/zod-to-rust.ts index 26f76cdf..b8774e4a 100644 --- a/packages/functions-tools/src/converters/zod-to-rust.ts +++ b/packages/functions-tools/src/converters/zod-to-rust.ts @@ -1,4 +1,4 @@ -import {capitalize} from '@junobuild/utils'; +import {capitalize, convertCamelToSnake} from '@junobuild/utils'; import type {z} from 'zod'; import {type SputnikSchemaResult, jsonToSputnikSchema} from './_converters'; import type {SputnikSchema} from './_types'; @@ -32,6 +32,17 @@ const RUST_KEYWORDS = new Set([ const sanitizeFieldName = (name: string): {name: string; sanitized: boolean} => RUST_KEYWORDS.has(name) ? {name: `r#${name}`, sanitized: true} : {name, sanitized: false}; +const transformFieldName = (name: string): {name: string; renamed: boolean} => { + const {sanitized, ...rest} = sanitizeFieldName(name); + + if (sanitized) { + return {renamed: true, ...rest}; + } + + const snake = convertCamelToSnake(name); + return {name: snake, renamed: snake !== name}; +}; + type RustTypeResult = | {kind: 'primitive'; fieldType: string} | {kind: 'composite'; fieldType: string; structs: string[]; needsJsonData: boolean}; @@ -138,8 +149,8 @@ const schemaToRustType = ({ const fields = nonDiscriminatorFields .map((f, fi) => { - const {name: fieldName, sanitized} = sanitizeFieldName(f.name); - const renameAttr = sanitized ? ` #[serde(rename = "${f.name}")]\n` : ''; + const {name: fieldName, renamed} = transformFieldName(f.name); + const renameAttr = renamed ? ` #[serde(rename = "${f.name}")]\n` : ''; return `${renameAttr} ${fieldName}: ${fieldResults[fi].fieldType},`; }) .join('\n'); @@ -186,8 +197,8 @@ const schemaToRustType = ({ const fields = schema.fields .map((f, i) => { const result = fieldResults[i]; - const {name: fieldName, sanitized} = sanitizeFieldName(f.name); - const renameAttr = sanitized ? ` #[serde(rename = "${f.name}")]\n` : ''; + const {name: fieldName, renamed} = transformFieldName(f.name); + const renameAttr = renamed ? ` #[serde(rename = "${f.name}")]\n` : ''; const attr = result.kind === 'composite' && result.needsJsonData ? ' #[json_data(nested)]\n' : ''; return `${renameAttr}${attr} pub ${fieldName}: ${result.fieldType},`; diff --git a/packages/functions-tools/src/tests/converters/zod-to-candid.spec.ts b/packages/functions-tools/src/tests/converters/zod-to-candid.spec.ts index 19d13d3d..805c3dd5 100644 --- a/packages/functions-tools/src/tests/converters/zod-to-candid.spec.ts +++ b/packages/functions-tools/src/tests/converters/zod-to-candid.spec.ts @@ -473,3 +473,11 @@ describe('throws', () => { z.array(z.union([z.object({a: z.string()}), z.object({b: z.int()})])) ); }); + +describe('camelCase field names', () => { + candid( + 'WithCamelCaseFields', + z.object({maxResponseBytes: z.bigint().optional(), isReplicated: z.boolean().optional()}), + 'record { max_response_bytes : opt nat; is_replicated : opt bool }' + ); +}); diff --git a/packages/functions-tools/src/tests/converters/zod-to-idl.spec.ts b/packages/functions-tools/src/tests/converters/zod-to-idl.spec.ts index 78d5dfea..0d13b04c 100644 --- a/packages/functions-tools/src/tests/converters/zod-to-idl.spec.ts +++ b/packages/functions-tools/src/tests/converters/zod-to-idl.spec.ts @@ -222,3 +222,11 @@ describe('discriminated union', () => { }) ); }); + +describe('camelCase field names', () => { + idl( + 'WithCamelCaseFields', + z.object({maxResponseBytes: z.bigint().optional(), isReplicated: z.boolean().optional()}), + IDL.Record({max_response_bytes: IDL.Opt(IDL.Nat64), is_replicated: IDL.Opt(IDL.Bool)}) + ); +}); diff --git a/packages/functions-tools/src/tests/converters/zod-to-rust.spec.ts b/packages/functions-tools/src/tests/converters/zod-to-rust.spec.ts index c27a2bc4..af02786a 100644 --- a/packages/functions-tools/src/tests/converters/zod-to-rust.spec.ts +++ b/packages/functions-tools/src/tests/converters/zod-to-rust.spec.ts @@ -279,3 +279,11 @@ describe('baseName', () => { expect(result.baseName).toBe('QueryArgs'); }); }); + +describe('camelCase field names', () => { + rust( + 'myFunction', + z.object({maxResponseBytes: z.bigint().optional(), isReplicated: z.boolean().optional()}), + '#[derive(CandidType, Serialize, Deserialize, Clone, JsonData)]\npub struct MyFunctionArgs {\n #[serde(rename = "maxResponseBytes")]\n pub max_response_bytes: Option,\n #[serde(rename = "isReplicated")]\n pub is_replicated: Option,\n}' + ); +}); From 7472bd996e275c51c1794fb2bf42ca7c685b7604 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 11:02:25 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=A4=96=20Documentation=20auto-update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/functions-tools/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/functions-tools/README.md b/packages/functions-tools/README.md index 4f2d255e..e802c299 100644 --- a/packages/functions-tools/README.md +++ b/packages/functions-tools/README.md @@ -36,7 +36,7 @@ Returns: An object containing the generated IDL type and the base type name. -[:link: Source](https://github.com/junobuild/juno-js/tree/main/packages/functions-tools/src/converters/zod-to-idl.ts#L93) +[:link: Source](https://github.com/junobuild/juno-js/tree/main/packages/functions-tools/src/converters/zod-to-idl.ts#L95) #### :gear: zodToRust @@ -56,7 +56,7 @@ Returns: An object containing the generated Rust code and the base type name. -[:link: Source](https://github.com/junobuild/juno-js/tree/main/packages/functions-tools/src/converters/zod-to-rust.ts#L239) +[:link: Source](https://github.com/junobuild/juno-js/tree/main/packages/functions-tools/src/converters/zod-to-rust.ts#L250) #### :gear: zodToCandid @@ -91,7 +91,7 @@ An object containing the generated Candid type declaration and the base type nam | `baseName` | `string` | | | `idl` | `IDL.Type` | | -[:link: Source](https://github.com/junobuild/juno-js/tree/main/packages/functions-tools/src/converters/zod-to-idl.ts#L65) +[:link: Source](https://github.com/junobuild/juno-js/tree/main/packages/functions-tools/src/converters/zod-to-idl.ts#L67) #### :gear: RustResult @@ -100,7 +100,7 @@ An object containing the generated Candid type declaration and the base type nam | `baseName` | `string` | | | `code` | `string` | | -[:link: Source](https://github.com/junobuild/juno-js/tree/main/packages/functions-tools/src/converters/zod-to-rust.ts#L208) +[:link: Source](https://github.com/junobuild/juno-js/tree/main/packages/functions-tools/src/converters/zod-to-rust.ts#L219) #### :gear: CandidResult