mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-20 16:29:31 +00:00
179 lines
5.6 KiB
Nim
179 lines
5.6 KiB
Nim
## CDDL (RFC 8610) schema generator for the nim-ffi framework.
|
|
## Mirrors the CBOR wire format produced by ffi/cbor_serial.nim: every
|
|
## user-declared {.ffi.} type becomes a CDDL rule, every {.ffi.} / {.ffiCtor.}
|
|
## proc gets a request envelope rule plus a response shape rule.
|
|
|
|
import std/[os, strutils, unicode]
|
|
import ./meta
|
|
|
|
proc innerOf(typeName, prefix: string): string =
|
|
if typeName.startsWith(prefix) and typeName.endsWith("]"):
|
|
return typeName[prefix.len .. ^2]
|
|
return ""
|
|
|
|
proc capitalizeFirstLetter(s: string): string =
|
|
if s.len == 0:
|
|
return s
|
|
return s.capitalize()
|
|
|
|
proc toCamelCase(s: string): string =
|
|
## "testlib_create" → "TestlibCreate"
|
|
var parts = s.split('_')
|
|
var res = ""
|
|
for p in parts:
|
|
res.add capitalizeFirstLetter(p)
|
|
return res
|
|
|
|
proc nimTypeToCddl*(typeName: string): string =
|
|
## Maps a Nim type name (as recorded in the compile-time registries) to its
|
|
## CDDL equivalent. Unknown PascalCase names are passed through as references
|
|
## to other CDDL rules in the same document.
|
|
let t = typeName.strip()
|
|
let seqI = innerOf(t, "seq[")
|
|
if seqI.len > 0:
|
|
return "[* " & nimTypeToCddl(seqI) & "]"
|
|
let arrI = innerOf(t, "array[")
|
|
if arrI.len > 0:
|
|
# CDDL has no fixed-length array literal as ergonomic as Nim's array; emit
|
|
# an unbounded array whose element type is the user-declared element type.
|
|
let commaIdx = arrI.find(',')
|
|
let elemT =
|
|
if commaIdx >= 0:
|
|
arrI[commaIdx + 1 .. ^1].strip()
|
|
else:
|
|
arrI
|
|
return "[* " & nimTypeToCddl(elemT) & "]"
|
|
let optI = innerOf(t, "Option[")
|
|
if optI.len > 0:
|
|
return nimTypeToCddl(optI) & " / nil"
|
|
let mayI = innerOf(t, "Maybe[")
|
|
if mayI.len > 0:
|
|
return nimTypeToCddl(mayI) & " / nil"
|
|
case t
|
|
of "bool": "bool"
|
|
of "int", "int64", "int32", "int16", "int8": "int"
|
|
of "uint", "uint64", "uint32", "uint16", "uint8": "uint"
|
|
of "string", "cstring": "tstr"
|
|
of "float", "float64": "float64"
|
|
of "float32": "float32"
|
|
of "pointer": "uint"
|
|
else: t
|
|
# reference to another rule in this CDDL document
|
|
|
|
proc reqStructName(p: FFIProcMeta): string =
|
|
## Mirrors the Nim macro: <CamelCase(procName)>{Ctor}Req.
|
|
let camel = toCamelCase(p.procName)
|
|
if p.kind == FFIKind.CTOR:
|
|
camel & "CtorReq"
|
|
else:
|
|
camel & "Req"
|
|
|
|
proc emitMap(
|
|
fields: openArray[tuple[name: string, typeName: string, isPtr: bool]]
|
|
): string =
|
|
if fields.len == 0:
|
|
return "{ }"
|
|
var parts: seq[string] = @[]
|
|
for f in fields:
|
|
let cddlType =
|
|
if f.isPtr:
|
|
"uint"
|
|
else:
|
|
nimTypeToCddl(f.typeName)
|
|
parts.add(f.name & ": " & cddlType)
|
|
"{ " & parts.join(", ") & " }"
|
|
|
|
proc emitObjectFields(t: FFITypeMeta): string =
|
|
var fields: seq[tuple[name: string, typeName: string, isPtr: bool]] = @[]
|
|
for f in t.fields:
|
|
fields.add((name: f.name, typeName: f.typeName, isPtr: false))
|
|
emitMap(fields)
|
|
|
|
proc emitReqFields(p: FFIProcMeta): string =
|
|
var fields: seq[tuple[name: string, typeName: string, isPtr: bool]] = @[]
|
|
for ep in p.extraParams:
|
|
fields.add((name: ep.name, typeName: ep.typeName, isPtr: ep.isPtr))
|
|
emitMap(fields)
|
|
|
|
proc responseRule(p: FFIProcMeta): string =
|
|
## CDDL shape of the success payload returned by the FFI callback.
|
|
## Error payloads stay as raw UTF-8 and are intentionally absent from the schema.
|
|
case p.kind
|
|
of FFIKind.CTOR:
|
|
# The ctor returns the FFI context address as a CBOR-encoded decimal string.
|
|
"tstr"
|
|
of FFIKind.DTOR:
|
|
# The dtor has no meaningful payload — handleRes sends a CBOR null sentinel.
|
|
"nil"
|
|
of FFIKind.FFI:
|
|
if p.returnIsPtr:
|
|
"uint"
|
|
else:
|
|
nimTypeToCddl(p.returnTypeName)
|
|
|
|
proc generateCddlSchema*(
|
|
procs: seq[FFIProcMeta],
|
|
types: seq[FFITypeMeta],
|
|
libName: string,
|
|
nimSrcRelPath: string,
|
|
): string =
|
|
var L: seq[string] = @[]
|
|
L.add("; CDDL schema for `" & libName & "` — auto-generated from " & nimSrcRelPath)
|
|
L.add("; Wire format: CBOR (RFC 8949). Errors return raw UTF-8 (not CBOR) and")
|
|
L.add("; are intentionally absent from this schema.")
|
|
L.add("")
|
|
|
|
if types.len > 0:
|
|
L.add(
|
|
"; ─── User-declared FFI types ──────────────────────────────────────"
|
|
)
|
|
for t in types:
|
|
L.add(t.name & " = " & emitObjectFields(t))
|
|
L.add("")
|
|
|
|
# Per-proc request envelopes (one CBOR blob per request).
|
|
let nonDtor = block:
|
|
var r: seq[FFIProcMeta] = @[]
|
|
for p in procs:
|
|
if p.kind != FFIKind.DTOR:
|
|
r.add(p)
|
|
r
|
|
if nonDtor.len > 0:
|
|
L.add(
|
|
"; ─── Request envelopes (one CBOR blob per request) ────────────────"
|
|
)
|
|
for p in nonDtor:
|
|
L.add(reqStructName(p) & " = " & emitReqFields(p))
|
|
L.add("")
|
|
|
|
# Per-proc request/response rules.
|
|
L.add(
|
|
"; ─── Procs ─────────────────────────────────────────────────────────"
|
|
)
|
|
for p in procs:
|
|
let kindTag =
|
|
case p.kind
|
|
of FFIKind.CTOR: "ctor"
|
|
of FFIKind.DTOR: "dtor"
|
|
of FFIKind.FFI: "ffi"
|
|
L.add("; " & p.procName & " (" & kindTag & ")")
|
|
if p.kind != FFIKind.DTOR:
|
|
L.add(p.procName & "-request = " & reqStructName(p))
|
|
L.add(p.procName & "-response = " & responseRule(p))
|
|
L.add("")
|
|
|
|
return L.join("\n")
|
|
|
|
proc generateCddlBindings*(
|
|
procs: seq[FFIProcMeta],
|
|
types: seq[FFITypeMeta],
|
|
libName: string,
|
|
outputDir: string,
|
|
nimSrcRelPath: string,
|
|
) =
|
|
createDir(outputDir)
|
|
writeFile(
|
|
outputDir / (libName & ".cddl"),
|
|
generateCddlSchema(procs, types, libName, nimSrcRelPath),
|
|
)
|