diff --git a/di/api/api.md b/di/api/api.md new file mode 100644 index 00000000..f58a1c5f --- /dev/null +++ b/di/api/api.md @@ -0,0 +1,99 @@ +# di.api + +Registration and metadata for a process's public API functions. Each entry describes one callable +function — its name, whether it is public, and human-readable description / parameters / return +text. The registry is the single source of truth (there is no live-namespace scan, because module +code does not live in scannable root namespaces). + +**Registration model:** di.torq collects the api metadata from each module at startup and registers +it here centrally via `add`. di.api has no dependency on other modules, and other modules do not +depend on di.api. + +> **Status: v1 complete.** Registry (`add`/`getapi`) and query surface (`find`/`f`/`p`) are in +> place. The live-namespace-introspection tools from TorQ's `.api` (`search`, `whereami`, `mem`, +> `fullapi`'s namespace scan, `exportconfig`) are intentionally **not** ported — see "Not ported". + +## Initialisation & Dependencies + +`init` must be called before any other function. The `log` dependency is **required** — there is no +fallback, and the module does no adaptation. The `log` value must already be a binary +`` `info`warn`error!{[c;m]} `` dict (context symbol, message string), built from `di.log` or +hand-rolled; a raw monadic `kx.log` instance must be wrapped by the caller first. + +```q +api:use`di.api +logger:use`di.log +api.init[enlist[`log]!enlist `info`warn`error!(logger.info;logger.warn;logger.error)] +``` + +| Dependency | Key | Required | Description | +|---|---|---|---| +| logger | `` `log `` | yes | Functions `info`,`warn`,`error` — each binary `{[c;m] ...}` (context symbol, message string) | + +## Exported functions + +| Function | Signature | Description | +|---|---|---| +| `init` | `init[deps]` | Wire the injected logger and start with an empty registry. Errors immediately if the `log` dependency is missing or malformed. | +| `add` | `add[name;public;descrip;params;return]` | Register (or overwrite) one api entry. `name` is a symbol (the registry key), `public` a boolean; `descrip`/`params`/`return` are description text. Re-adding the same `name` updates it in place. | +| `getapi` | `getapi[]` | Return the full registry as a keyed table (keyed on `name`, columns `public`/`descrip`/`params`/`return`). | +| `find` | `find[s;p]` | Return registry entries whose name matches pattern `s` (a symbol — `` ` `` matches all, else a `*s*` substring — or a string glob) and whose `public` flag is in `p` (`1b` public-only, `01b` all). Case-insensitive; returns an unkeyed table. | +| `f` | `f[s]` | `find[s;01b]` — all matching entries, public and non-public. | +| `p` | `p[s]` | `find[s;1b]` — public matching entries only. | +| `getapimeta` | `getapimeta[]` | Return **this module's own** api metadata — one `(name;public;descrip;params;return)` row per exported function — for di.torq to collect and register. Every module exposes this (the module-owned metadata convention). Names are bare (the module's own); di.torq applies process-wide qualification when registering. | + +### Registry schema + +```q +([name:`u#`symbol$()] public:`boolean$(); descrip:(); params:(); return:()) +``` + +## Design decisions + +- **Registry-only, no live scan.** The core departure from TorQ. Module functions live in private + `.z.m`, invisible to `\v`/`\f`, so a live-namespace scan would report a process's top-level glue as + its API, not the module logic. The registry is populated explicitly instead and is the single + source of truth (see "Not ported" for the TorQ functions this drops). +- **Module-owned metadata (`getapimeta`).** Rather than a central hand-maintained list, each module + declares its own API as data — one `(name;public;descrip;params;return)` row per export — and + `di.torq` collects and registers them via `add`. Names in `getapimeta` are **bare** (the module's + own); `di.torq` applies process-wide qualification. `di.api` dog-foods this: `getapimeta` documents + its own seven exports, and a test asserts every `export` name has a matching row. +- **Metadata is a compact row literal, not resident data.** `getapimeta` is built with the + row-oriented `flip cols!flip(rows)` idiom — one self-contained line per function — rather than five + parallel column-lists, so it stays terse and a function can't drift out of column alignment. It + costs the module nothing at runtime: it's a *function*, materialised once when `di.torq` calls it + at startup, then registered centrally; the module keeps no resident copy. This is the form the + modularisation skill mandates for every module. +- **`find` unifies the query surface.** `f` and `p` are just projections `find[;01b]` / `find[;1b]`, + so there is one matching implementation. Symbol patterns become `*substring*`, `` ` `` matches all, + and strings pass through as globs — all case-insensitive. +- **Minimal, registered surface.** The public API is small and every entry is something `di.torq` + will actually register. TorQ's introspection helpers are dropped, not stubbed (below). + +## Not ported from TorQ's `.api` (deliberate) + +TorQ's `.api` marries a registry with **live introspection of the root namespaces** (`\v`/`\f` scans, +grepping function definitions, sizing variables). That half does not fit the module world: module +code lives in each module's private `.z.m`, not in scannable root namespaces, so those tools would +see only a process's top-level glue, not the module logic they exist to inspect. Accordingly: + +| TorQ function | Why not ported | +|---|---| +| `u` | "public, excluding `.q`/`.Q`/`.h`/`.o`" — only filtered primitives out of the live scan; this registry holds only registered module functions, so `u` == `p`. | +| `search` / `s` | Greps live function *definitions* across root namespaces — cannot see module-private code. | +| `whereami` | Reverse-looks-up a function value to its name via the root scan — returns nothing for a function that lives inside a module (i.e. the usual error-trap case). | +| `fullapi` (namespace scan) | The scan-and-left-join-`detail` model does not apply; `getapi`/`find` serve the registry directly. | +| `mem` / `m` | Memory sizing belongs to `di.memstats`. | +| `exportconfig` / `exportallconfig` / `torqnamespaces` | A faithful port needs config **values** (di.config `getmodule`) joined with **descriptions** (di.api `getapi`/`find`). di.config and di.api are both standalone and don't depend on each other, so the join belongs in **di.torq** (it has both). It is a **di.torq-era task**, not a di.config change: di.config's `getmodule` already returns per-namespace values; the missing piece is pairing them with the api descriptions across namespaces. | + +## Hard dependencies + +None. + +## Tests + +```q +k4unit:use`di.k4unit +k4unit.moduletest`di.api +``` diff --git a/di/api/api.q b/di/api/api.q new file mode 100644 index 00000000..8e156518 --- /dev/null +++ b/di/api/api.q @@ -0,0 +1,75 @@ +/ api - a registry of a process's callable functions: one entry per function holding its name, +/ public flag, and description / params / return text. registry-only (no live namespace scan - +/ module code is not in root namespaces). di.torq registers each module's metadata here via add; +/ di.api depends on no other module. public api: add, getapi, find/f/p. + +/ registry template (constant); .z.m.detail is the live copy +detailschema:([name:`u#`symbol$()] public:`boolean$(); descrip:(); params:(); return:()); + +init:{[deps] + / wire the injected logger (required, no fallback) and start with an empty registry. deps: a `log + / key holding a binary `info`warn`error dict of {[c;m]} loggers (di.log or hand-rolled; a monadic + / kx.log instance must be wrapped first). e.g. di.api.init[enlist[`log]!enlist logdep] + if[99h<>type deps; + '"di.api: deps must be a dict with a `log key"]; + if[not `log in key deps; + '"di.api: log dependency is required; pass `info`warn`error functions keyed on `log"]; + if[99h<>type deps`log; + '"di.api: log value must be a dict of `info`warn`error functions"]; + if[not all (`info`warn`error) in key deps`log; + '"di.api: log dict must have `info`warn`error keys; got: ",(", " sv string key deps`log)]; + .z.m.loginfo:deps[`log]`info; + .z.m.logwarn:deps[`log]`warn; + .z.m.logerr:deps[`log]`error; + .z.m.detail:detailschema; + }; + +add:{[name;public;descrip;params;return] + / register (or overwrite) one entry, keyed on name (re-adding a name updates in place). + / called by di.torq at startup for each module's exported functions. + if[not -11h=type name; + .z.m.logerr[`add;err:"di.api: name must be a symbol"]; + 'err; + ]; + if[not -1h=type public; + .z.m.logerr[`add;err:"di.api: public must be a boolean"]; + 'err; + ]; + .z.m.detail:.z.m.detail upsert (name;public;descrip;params;return); + }; + +getapi:{[] + / return the whole registry (keyed table, keyed on name) + :.z.m.detail; + }; + +find:{[s;p] + / registry entries whose name matches s and whose public flag is in p. s: a symbol (` = all, else + / a *s* substring) or a string glob; p: public flags to include (1b public-only, 01b all). + / case-insensitive; returns an unkeyed table. + if[-11h=type s;s:$[null s;"*";"*",string[s],"*"]]; + if[not 10h=abs type s:s,(); + .z.m.logerr[`find;err:"di.api: search pattern must be a symbol or string"]; + 'err; + ]; + :select from .z.m.detail where lower[string name] like lower s, public in p; + }; + +/ all entries (public and non-public) +f:find[;01b]; +/ public entries only +p:find[;1b]; + +getapimeta:{[] + / this module's api metadata, one row per exported function, for di.torq to collect and register + / with di.api. names are bare (the module's own); di.torq applies the process-wide qualification. + / one self-contained (name;public;descrip;params;return) row per line - flip cols!flip rows. + :flip `name`public`descrip`params`return!flip( + (`init; 0b; "wire the injected logger and start an empty registry"; "[dict: deps with a `log key]"; "null"); + (`add; 0b; "register (or overwrite) one api entry, keyed on name"; "[symbol: name; boolean: public; descrip; params; return]"; "null"); + (`getapi; 1b; "return the whole registry (keyed table)"; "[]"; "keyed table: the registry"); + (`find; 1b; "registry entries matching a name pattern and public flag"; "[symbol|string: name pattern; boolean(list): public flags]"; "table: matching entries"); + (`f; 1b; "find[;01b] - all entries matching a name pattern"; "[symbol|string: name pattern]"; "table: matching entries"); + (`p; 1b; "find[;1b] - public entries matching a name pattern"; "[symbol|string: name pattern]"; "table: matching public entries"); + (`getapimeta; 0b; "this module's api metadata rows"; "[]"; "table: metadata rows")); + }; diff --git a/di/api/init.q b/di/api/init.q new file mode 100644 index 00000000..ab0ab10c --- /dev/null +++ b/di/api/init.q @@ -0,0 +1,4 @@ +/ api module - registration and metadata for a process's public API functions. +/ registry-only (no live namespace scan); di.torq collects each module's getapimeta and registers here. +\l ::api.q +export:([init;add;getapi;find;f;p;getapimeta]) diff --git a/di/api/test.csv b/di/api/test.csv new file mode 100644 index 00000000..78808331 --- /dev/null +++ b/di/api/test.csv @@ -0,0 +1,41 @@ +action,ms,bytes,lang,code,repeat,minver,comment +before,0,0,q,api:use`di.api,1,1,load the module +before,0,0,q,mylog:`info`warn`error!({[c;m]};{[c;m]};{[c;m]}),1,1,define a no-op binary logger {[c;m]} +before,0,0,q,api.init[enlist[`log]!enlist mylog],1,1,init with the required log dependency +comment,,,,,,,init - dependency validation +run,0,0,q,api.init[enlist[`log]!enlist mylog],1,1,init succeeds with a valid log dependency +fail,0,0,q,api.init[(::)],1,1,init rejects a non-dictionary deps +fail,0,0,q,api.init[enlist[`other]!enlist mylog],1,1,init rejects deps without a log key +fail,0,0,q,api.init[enlist[`log]!enlist 42],1,1,init rejects a non-dict log value +fail,0,0,q,api.init[enlist[`log]!enlist(`info`warn!({[c;m]};{[c;m]}))],1,1,init rejects a log dict missing the error key +comment,,,,,,,add and getapi - registry +run,0,0,q,api.init[enlist[`log]!enlist mylog],1,1,re-init to reset the registry to empty +true,0,0,q,0=count api.getapi[],1,1,registry starts empty after init +run,0,0,q,"api.add[`.gw.asyncexec;1b;""Execute async"";""[query;servers]"";""result""]",1,1,register a public api entry +true,0,0,q,1=count api.getapi[],1,1,add stores one entry +true,0,0,q,1b~(api.getapi[])[`.gw.asyncexec]`public,1,1,public flag stored +true,0,0,q,"""Execute async""~(api.getapi[])[`.gw.asyncexec]`descrip",1,1,description stored +run,0,0,q,"api.add[`.gw.asyncexec;0b;""updated"";""[]"";""x""]",1,1,re-add the same name +true,0,0,q,1=count api.getapi[],1,1,re-adding the same name overwrites (no duplicate) +true,0,0,q,0b~(api.getapi[])[`.gw.asyncexec]`public,1,1,re-add updates the entry in place +fail,0,0,q,"api.add[""notasym"";1b;""d"";""p"";""r""]",1,1,add rejects a non-symbol name +fail,0,0,q,api.add[`.x.f;1;`d;`p;`r],1,1,add rejects a non-boolean public flag +comment,,,,,,,find / f / p - query the registry +run,0,0,q,api.init[enlist[`log]!enlist mylog],1,1,re-init to reset the registry +run,0,0,q,"api.add[`.gw.asyncexec;1b;""a"";""p"";""r""]",1,1,seed a public entry +run,0,0,q,"api.add[`.gw.syncexec;1b;""a"";""p"";""r""]",1,1,seed another public entry +run,0,0,q,"api.add[`.api.add;1b;""a"";""p"";""r""]",1,1,seed a public entry with a different name +run,0,0,q,"api.add[`.internal.helper;0b;""a"";""p"";""r""]",1,1,seed a non-public entry +true,0,0,q,4=count api.f`,1,1,f with a null symbol returns all entries +true,0,0,q,3=count api.p`,1,1,p returns only the public entries +true,0,0,q,2=count api.f`exec,1,1,f matches names containing exec (substring) +true,0,0,q,2=count api.f`EXEC,1,1,name matching is case-insensitive +true,0,0,q,"2=count api.f""*gw*""",1,1,f accepts a string glob pattern +true,0,0,q,`.internal.helper in exec name from api.f`helper,1,1,f includes non-public entries +true,0,0,q,0=count api.p`helper,1,1,p excludes the non-public helper +fail,0,0,q,api.find[42;01b],1,1,find rejects a pattern that is not a symbol or string +comment,,,,,,,getapimeta - module api metadata +true,0,0,q,(asc key api)~asc exec name from api.getapimeta[],1,1,getapimeta documents exactly the module's exports +true,0,0,q,`name`public`descrip`params`return~cols api.getapimeta[],1,1,getapimeta rows carry the registry columns +true,0,0,q,1b~(1!api.getapimeta[])[`find]`public,1,1,a discovery function (find) is marked public +true,0,0,q,0b~(1!api.getapimeta[])[`init]`public,1,1,framework plumbing (init) is not public