Skip to content

Commit 881cc81

Browse files
refactor string manipulation procs in meta
1 parent 4ddeb13 commit 881cc81

5 files changed

Lines changed: 120 additions & 26 deletions

File tree

ffi/codegen/cpp.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ proc stripLibPrefixCpp(procName, libName: string): string =
5858
return procName
5959

6060
proc reqStructName(p: FFIProcMeta): string =
61-
let camel = toCamelCase(p.procName)
61+
let camel = snakeToPascalCase(p.procName)
6262
if p.kind == FFIKind.CTOR: camel & "CtorReq" else: camel & "Req"
6363

6464
proc cppBracedInit(structName: string, fieldNames: seq[string]): string =

ffi/codegen/meta.nim

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
## Compile-time metadata types for FFI binding generation.
22
## Populated by the {.ffiCtor.} and {.ffi.} macros and consumed by codegen.
33

4-
import std/strutils
4+
import std/[strutils, unicode]
55

66
type
77
FFIParamMeta* = object
@@ -51,27 +51,34 @@ const ffiNimSrcRelPath* {.strdefine.} = ""
5151
# Name conversion helpers shared by codegen and the ffi macro
5252
# ---------------------------------------------------------------------------
5353

54-
proc toSnakeCase*(s: string): string =
54+
proc camelToSnakeCase*(s: string): string =
55+
## Converts camelCase to snake_case. Inserts `_` before each uppercase rune
56+
## that's not the first character and lowercases everything.
57+
## e.g. "delayMs" → "delay_ms", "timerName" → "timer_name"
5558
var snake = ""
56-
for i, c in s:
57-
if c.isUpperAscii() and i > 0:
59+
var first = true
60+
for r in runes(s):
61+
if r.isUpper() and not first:
5862
snake.add('_')
59-
snake.add(c.toLowerAscii())
63+
snake.add($r.toLower())
64+
first = false
6065
return snake
6166

62-
proc toPascalCase*(s: string): string =
63-
## Returns `s` with the first character uppercased.
67+
proc capitalizeFirstLetter*(s: string): string =
68+
## Returns `s` with its first rune uppercased; the rest is left unchanged.
69+
## e.g. "abc" → "Abc", "" → "", "Abc" → "Abc"
6470
if s.len == 0:
6571
return s
66-
var pascal = s
67-
pascal[0] = s[0].toUpperAscii()
68-
return pascal
72+
var runesSeq = toRunes(s)
73+
runesSeq[0] = runesSeq[0].toUpper()
74+
return $runesSeq
6975

70-
proc toCamelCase*(s: string): string =
71-
## Converts snake_case or mixed identifiers to PascalCase for type names.
72-
## e.g. "testlib_create" -> "TestlibCreate"
76+
proc snakeToPascalCase*(s: string): string =
77+
## Converts snake_case identifiers to PascalCase: split on `_`, uppercase
78+
## the first rune of each part, concatenate.
79+
## e.g. "testlib_create" → "TestlibCreate", "hello_world" → "HelloWorld"
7380
let parts = s.split('_')
74-
var camel = ""
81+
var pascal = ""
7582
for p in parts:
76-
camel.add toPascalCase(p)
77-
return camel
83+
pascal.add capitalizeFirstLetter(p)
84+
return pascal

ffi/codegen/rust.nim

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ proc nimTypeToRust*(typeName: string): string =
2020
of "bool": "bool"
2121
of "float", "float64": "f64"
2222
of "pointer": "u64"
23-
else: toPascalCase(t)
23+
else: capitalizeFirstLetter(t)
2424

2525
proc deriveLibName*(procs: seq[FFIProcMeta]): string =
2626
## Extracts the common prefix before the first `_` from proc names.
@@ -45,7 +45,7 @@ proc stripLibPrefix*(procName: string, libName: string): string =
4545

4646
proc reqStructName(p: FFIProcMeta): string =
4747
## Mirrors the Nim macro: <CamelCase(procName)>Req or CtorReq for ctors.
48-
let camel = toCamelCase(p.procName)
48+
let camel = snakeToPascalCase(p.procName)
4949
if p.kind == FFIKind.CTOR: camel & "CtorReq" else: camel & "Req"
5050

5151
# ---------------------------------------------------------------------------
@@ -201,7 +201,7 @@ proc generateTypesRs*(
201201
lines.add("#[derive(Debug, Clone, Serialize, Deserialize)]")
202202
lines.add("pub struct $1 {" % [t.name])
203203
for f in t.fields:
204-
let snakeName = toSnakeCase(f.name)
204+
let snakeName = camelToSnakeCase(f.name)
205205
let rustType = nimTypeToRust(f.typeName)
206206
# Add serde rename if camelCase name differs from snake_case
207207
if snakeName != f.name:
@@ -222,7 +222,7 @@ proc generateTypesRs*(
222222
else:
223223
lines.add("pub struct $1 {" % [reqName])
224224
for ep in p.extraParams:
225-
let snake = toSnakeCase(ep.name)
225+
let snake = camelToSnakeCase(ep.name)
226226
let rustType =
227227
if ep.isPtr: "u64"
228228
else: nimTypeToRust(ep.typeName)
@@ -255,7 +255,7 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string =
255255

256256
var libTypeName = ""
257257
if ctors.len > 0: libTypeName = ctors[0].libTypeName
258-
else: libTypeName = toPascalCase(libName)
258+
else: libTypeName = capitalizeFirstLetter(libName)
259259

260260
let ctxTypeName = libTypeName & "Ctx"
261261

@@ -415,7 +415,7 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string =
415415
var paramsList: seq[string] = @[]
416416
var fieldInits: seq[string] = @[]
417417
for ep in ctor.extraParams:
418-
let snake = toSnakeCase(ep.name)
418+
let snake = camelToSnakeCase(ep.name)
419419
let rustType =
420420
if ep.isPtr: "u64"
421421
else: nimTypeToRust(ep.typeName)
@@ -468,7 +468,7 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string =
468468
var paramsList: seq[string] = @[]
469469
var fieldInits: seq[string] = @[]
470470
for ep in m.extraParams:
471-
let snake = toSnakeCase(ep.name)
471+
let snake = camelToSnakeCase(ep.name)
472472
let rustType =
473473
if ep.isPtr: "u64"
474474
else: nimTypeToRust(ep.typeName)

ffi/internal/ffi_macro.nim

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ macro ffi*(prc: untyped): untyped =
527527
else:
528528
raw
529529
let cExportName = nimNameToCExport(procNameStr)
530-
let camelName = toCamelCase(procNameStr)
530+
let camelName = snakeToPascalCase(procNameStr)
531531

532532
let reqTypeName = ident(camelName & "Req")
533533

@@ -1160,7 +1160,7 @@ macro ffiCtor*(prc: untyped): untyped =
11601160
else:
11611161
procNameStr
11621162
let cExportName = nimNameToCExport(cleanName)
1163-
let reqTypeNameStr = toCamelCase(cleanName) & "CtorReq"
1163+
let reqTypeNameStr = snakeToPascalCase(cleanName) & "CtorReq"
11641164
let reqTypeName = ident(reqTypeNameStr)
11651165

11661166
let typeDef = buildCtorRequestType(reqTypeName, paramNames, paramTypes)

tests/test_meta.nim

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
## Unit tests for the string-manipulation helpers used by the codegen.
2+
## These names map identifier conventions between Nim (camelCase),
3+
## Rust (snake_case) and C++ (PascalCase types), and they're load-bearing
4+
## for binding generation, so it's worth pinning their behaviour with tests.
5+
6+
import unittest
7+
import ../ffi/codegen/meta
8+
9+
suite "camelToSnakeCase":
10+
test "empty string":
11+
check camelToSnakeCase("") == ""
12+
13+
test "single lowercase character":
14+
check camelToSnakeCase("a") == "a"
15+
16+
test "single uppercase character":
17+
check camelToSnakeCase("A") == "a"
18+
19+
test "all lowercase passes through":
20+
check camelToSnakeCase("hello") == "hello"
21+
22+
test "simple camelCase":
23+
check camelToSnakeCase("camelCase") == "camel_case"
24+
25+
test "two-letter suffix":
26+
check camelToSnakeCase("delayMs") == "delay_ms"
27+
28+
test "PascalCase input — leading capital stays at start":
29+
check camelToSnakeCase("PascalCase") == "pascal_case"
30+
31+
test "consecutive uppercase letters each get their own underscore":
32+
check camelToSnakeCase("ABC") == "a_b_c"
33+
34+
test "multiple word boundaries":
35+
check camelToSnakeCase("abcDefGhi") == "abc_def_ghi"
36+
37+
test "already snake_case passes through":
38+
check camelToSnakeCase("already_snake") == "already_snake"
39+
40+
suite "capitalizeFirstLetter":
41+
test "empty string":
42+
check capitalizeFirstLetter("") == ""
43+
44+
test "single lowercase character":
45+
check capitalizeFirstLetter("a") == "A"
46+
47+
test "single uppercase character":
48+
check capitalizeFirstLetter("A") == "A"
49+
50+
test "lowercase word":
51+
check capitalizeFirstLetter("abc") == "Abc"
52+
53+
test "already capitalised":
54+
check capitalizeFirstLetter("Abc") == "Abc"
55+
56+
test "all-caps stays unchanged except first stays cap":
57+
check capitalizeFirstLetter("ABC") == "ABC"
58+
59+
test "leading non-letter is left alone":
60+
check capitalizeFirstLetter("_hello") == "_hello"
61+
62+
suite "snakeToPascalCase":
63+
test "empty string":
64+
check snakeToPascalCase("") == ""
65+
66+
test "single lowercase word":
67+
check snakeToPascalCase("hello") == "Hello"
68+
69+
test "two-part snake_case":
70+
check snakeToPascalCase("hello_world") == "HelloWorld"
71+
72+
test "three-part snake_case":
73+
check snakeToPascalCase("testlib_create") == "TestlibCreate"
74+
75+
test "single-letter parts each capitalised":
76+
check snakeToPascalCase("a_b_c") == "ABC"
77+
78+
test "trailing underscore yields empty trailing part":
79+
check snakeToPascalCase("foo_") == "Foo"
80+
81+
test "leading underscore yields empty leading part":
82+
check snakeToPascalCase("_foo") == "Foo"
83+
84+
test "already-mixed parts preserve their existing case after the first":
85+
# split on '_', capitalize first letter of each part; "HasCaps" first
86+
# letter is already 'H' so it's untouched.
87+
check snakeToPascalCase("already_HasCaps") == "AlreadyHasCaps"

0 commit comments

Comments
 (0)