## Demonstrates the Nim-native side of the {.ffi.} / {.ffiCtor.} macros: ## every annotated proc remains callable from Nim with its declared signature ## (`Future[Result[T, string]]`), no callbacks or CBOR buffers involved. The ## C-exported wrapper exists in parallel as an overload distinguishable by ## arity — see `test_ffi_context.nim` for the C-shape callers. import std/options import unittest2 import results import ffi type Counter = object start: int type CounterConfig {.ffi.} = object initial: int type IncRequest {.ffi.} = object by: int type CounterState {.ffi.} = object value: int proc counter_create*(cfg: CounterConfig): Future[Result[Counter, string]] {.ffiCtor.} = ## Async ctor body — exercises the chronos path on the FFI thread. await sleepAsync(1.milliseconds) return ok(Counter(start: cfg.initial)) proc counter_value*(c: Counter): Future[Result[CounterState, string]] {.ffi.} = ## Sync body (no `await`); the Nim-facing wrapper still returns ## Future[Result[...]] so the source-level shape is preserved. return ok(CounterState(value: c.start)) proc counter_add*( c: Counter, req: IncRequest ): Future[Result[CounterState, string]] {.ffi.} = ## Async body with a real chronos yield. await sleepAsync(1.milliseconds) return ok(CounterState(value: c.start + req.by)) proc counter_compose*(c: Counter, a: int, b: int): Future[Result[int, string]] {.ffi.} = ## Multiple primitive params plus a non-object return type. return ok(c.start + a + b) proc counter_greet*( c: Counter, name: Option[string] ): Future[Result[string, string]] {.ffi.} = ## Exercises Option[T] param round-trip. let n = if name.isSome: name.get else: "anon" return ok("hello " & n & " (start=" & $c.start & ")") proc counter_fail*(c: Counter, reason: string): Future[Result[string, string]] {.ffi.} = ## Error path — the failure surfaces as Result.err on the caller side. return err("rejected: " & reason) proc counter_chain*( c: Counter, steps: int ): Future[Result[CounterState, string]] {.ffi.} = ## Real async work: multiple awaits composing other {.ffi.} procs. ## Shows that the Nim-facing wrapper for an {.ffi.} proc is itself ## awaitable, so {.ffi.} procs can be composed naturally without ever ## touching the C-export shape. var current = c for i in 0 ..< steps: await sleepAsync(1.milliseconds) let stepRes = await counter_add(current, IncRequest(by: 1)) if stepRes.isErr: return err(stepRes.error) current = Counter(start: stepRes.value.value) return ok(CounterState(value: current.start)) type RangeFilter {.ffi.} = object lo: int hi: int type Pagination {.ffi.} = object offset: int limit: int type Projection {.ffi.} = object fields: seq[string] includeTotals: bool type QueryReport {.ffi.} = object matched: int returned: int fieldsKept: seq[string] proc counter_query*( c: Counter, filter: RangeFilter, page: Pagination, projection: Projection ): Future[Result[QueryReport, string]] {.ffi.} = ## Three independent object-typed parameters: `filter`, `page`, `projection`. ## Verifies that the macro packs all three into one CBOR Req envelope on the ## wire and unpacks them back into the typed locals before this body runs. if filter.hi < filter.lo: return err("filter range is empty") if page.limit <= 0: return err("page.limit must be positive") let matched = max(0, filter.hi - filter.lo + 1) let returned = min(matched - min(matched, page.offset), page.limit) return ok( QueryReport( matched: matched + c.start, # surfaces lib state in the response returned: returned, fieldsKept: if projection.includeTotals: projection.fields & @["__totals__"] else: projection.fields, ) ) suite "Nim-native API for {.ffi.} / {.ffiCtor.}": test "ffiCtor returns the user-typed lib value": let res = waitFor counter_create(CounterConfig(initial: 7)) check res.isOk check res.value.start == 7 test "sync .ffi. body completes via Future[Result[T, string]]": let res = waitFor counter_value(Counter(start: 5)) check res.isOk check res.value.value == 5 test "async .ffi. body with await": let res = waitFor counter_add(Counter(start: 5), IncRequest(by: 3)) check res.isOk check res.value.value == 8 test "multiple primitive params": let res = waitFor counter_compose(Counter(start: 1), 2, 3) check res.isOk check res.value == 6 test "Option[string] param round-trip — some": let res = waitFor counter_greet(Counter(start: 1), some("jamon")) check res.isOk check res.value == "hello jamon (start=1)" test "Option[string] param round-trip — none": let res = waitFor counter_greet(Counter(start: 2), none(string)) check res.isOk check res.value == "hello anon (start=2)" test "error result propagates as Result.err": let res = waitFor counter_fail(Counter(start: 0), "out of cookies") check res.isErr check res.error == "rejected: out of cookies" test "async .ffi. body chains multiple awaits and composes other .ffi. procs": let res = waitFor counter_chain(Counter(start: 10), 4) check res.isOk check res.value.value == 14 test "chain with 0 steps returns the input unchanged": let res = waitFor counter_chain(Counter(start: 42), 0) check res.isOk check res.value.value == 42 test "three complex object params travel together in one CBOR envelope": let res = waitFor counter_query( Counter(start: 100), RangeFilter(lo: 1, hi: 50), Pagination(offset: 10, limit: 25), Projection(fields: @["id", "name"], includeTotals: true), ) check res.isOk check res.value.matched == 150 # filter range 50 + lib state 100 check res.value.returned == 25 check res.value.fieldsKept == @["id", "name", "__totals__"] test "three-complex-param error path": let res = waitFor counter_query( Counter(start: 0), RangeFilter(lo: 10, hi: 1), # inverted range Pagination(offset: 0, limit: 5), Projection(fields: @[], includeTotals: false), ) check res.isErr check res.error == "filter range is empty"