Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions di/api/api.md
Original file line number Diff line number Diff line change
@@ -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
```
75 changes: 75 additions & 0 deletions di/api/api.q
Original file line number Diff line number Diff line change
@@ -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"];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The find function converts the search pattern s to a list with s,() and then applies like with a scalar pattern. In q, like expects a scalar string pattern on the right-hand side, but after s:s,(), s is a list (e.g. enlist "*foo*"). A list pattern passed to like will produce a type error at runtime. The ,() coercion should be removed; the scalar string s (already ensured to be a string by the preceding guards) should be passed directly to like.

'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"));
};
4 changes: 4 additions & 0 deletions di/api/init.q
Original file line number Diff line number Diff line change
@@ -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])
41 changes: 41 additions & 0 deletions di/api/test.csv
Original file line number Diff line number Diff line change
@@ -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