Skip to content

Feature api#115

Open
ascottDI wants to merge 4 commits into
mainfrom
feature-API
Open

Feature api#115
ascottDI wants to merge 4 commits into
mainfrom
feature-API

Conversation

@ascottDI

Copy link
Copy Markdown

di.api — TorQ Modularisation PR

⚠️ Action required: API descriptions need backfilling

This PR delivers the registry machinery (di.api) and the module-owned-metadata convention
(getapimeta). It does not populate real descriptions for the other modules. Every existing
di.* module still needs its own getapimeta written — one (name;public;descrip;params;return)
row per exported function — so di.torq has real content to collect and register. Until that
backfill happens across the modules, the registry is wired but empty of actual API docs.

Summary

Extracts a process's callable-function registry from TorQ's .api namespace into a standalone
kdb-x module: di.api. The module holds one entry per public/registered function — its name, a
public flag, and human-readable description / params / return text — and exposes a small query
surface over it (getapi, find/f/p). Registration is push-based: di.torq collects each
module's metadata at startup and registers it here via add; every module advertises its own
metadata through a getapimeta export. It satisfies the di.* contract: a required injected logger,
a minimal exported API, no hard module dependencies, and a k4unit suite.

Status: v1 complete and tested in isolation. The registry (add/getapi) and query surface
(find/f/p) are in place, plus the module-owned-metadata convention (getapimeta). TorQ's
live-namespace-introspection half is intentionally not ported — see Not ported. Like the
other di.* modules it is not deployable in a live stack until di.log and di.torq land — see
Caveats.


Background

TorQ's .api marries two things: a registry of documented functions and live introspection
of the root namespaces
(\v/\f scans, grepping function definitions, sizing variables). In the
module world only the first half fits — module code lives in each module's private .z.m, not in
scannable root namespaces, so a live scan would see only a process's top-level glue, not the module
logic it exists to inspect.

So di.api is registry-only: it is the single source of truth for the process's API, populated
explicitly rather than discovered. di.torq reads each module's getapimeta at startup and calls
add for every row (applying process-wide name qualification); consumers query with getapi/find.
di.api depends on no other module, and no module depends on di.api.


Changes

New files

File Description
di/api/api.q Implementation — init, add, getapi, find, f, p, getapimeta, and the detailschema registry template
di/api/init.q Module entry point — loads api.q, declares the export list
di/api/test.csv k4unit suite — 24 assertions (33 rows incl. before/run setup)
di/api/api.md Module README — API, registration model, and the "Not ported" rationale

Differences from the TorQ original

Aspect TorQ .api di.api
Data source Registry + live scan of root namespaces (\v/\f, definition grep) Registry only — module code isn't in scannable root namespaces, so a scan would see nothing useful
Population Self-populating via introspection + a detail table Push-based: di.torq calls add per module, sourced from each module's getapimeta
Logging Hard-coded .lg.o/.lg.e Injected binary log dep, required at init, no fallback and no adaptation
Error handling Mixed add/find log via .z.m.logerr then signal; init signals directly (no logger wired yet) — all with a di.api: prefix
Module contract None kdb-x use singleton, init[deps], export: list

Logging contract (injected dependency)

Consistent with the settled no-normlog direction across di.*: logging is an injected
dependency
, wired at init, with no default logger and no adaptation inside the module.
init must be called before any other function.

  • init[deps] takes a dict carrying the required log key. It errors immediately (plain signal,
    di.api: prefix) if deps is not a dict, is missing `log, log is not a dict, or log
    lacks any of `info`warn`error.
  • The log value must already be a binary `info`warn`error!{[c;m]} dict (context symbol
    c, message string m). Build it from di.log or hand-roll one; a raw monadic kx.log
    instance must be wrapped by the caller first — di.api does not wrap it.
    api:use`di.api
    logger:use`di.log
    api.init[enlist[`log]!enlist `info`warn`error!(logger.info;logger.warn;logger.error)]
  • init fans the injected log dict out into module-local
    .z.m.loginfo/.z.m.logwarn/.z.m.logerr. add/find log-then-signal on bad input so every
    rejection is observable in the log as well as thrown.

Exported API

api:use`di.api

api.init[deps]                              / wire the required log dependency + empty registry (call first)
api.add[name;public;descrip;params;return]  / register/overwrite one entry, keyed on name (di.torq calls this)
api.getapi[]                                / the whole registry as a keyed table (keyed on name)
api.find[s;p]                               / entries whose name matches pattern s and public flag is in p
api.f[s]                                    / find[s;01b] - all matching entries (public + non-public)
api.p[s]                                    / find[s;1b]  - public matching entries only
api.getapimeta[]                            / THIS module's own metadata rows, for di.torq to collect
  • name — a symbol; the registry key. Re-adding the same name updates it in place.
  • public — a boolean; add rejects a non-boolean.
  • s (pattern) — a symbol (` = all, else a *s* substring) or a string glob; case-insensitive.
  • p (flags) — public flags to include (1b public-only, 01b all).

Registry schema

([name:`u#`symbol$()] public:`boolean$(); descrip:(); params:(); return:())

detailschema is the constant template; .z.m.detail is the live copy, reset to empty on each
init.


Key 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 scan is worse than useless — it would report a process's glue
    as its API. The registry is populated explicitly instead, making it the single source of truth.
  • 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. Names in getapimeta are bare (the module's own);
    di.torq applies process-wide qualification when it calls add. This module dog-foods the
    convention: getapimeta documents its own seven exports.
  • 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 in the di.api registry; the module keeps no resident copy.
    This is the form the modularisation skill now mandates for every module.
  • find unifies the query surface. f and p are projections of find[;01b] / find[;1b], so
    there's 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 rather than stubbed (below).

Not ported from TorQ's .api (deliberate)

The live-introspection half does not fit the module world (module code isn't in root namespaces):

TorQ function Why not ported
u Was just "public minus .q/.Q/.h/.o" filtered out of the live scan; here the registry holds only registered module functions, so u == p.
search / s Greps live function definitions across root namespaces — can't 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.
fullapi (namespace scan) The scan-and-left-join-detail model doesn't 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). Both modules are standalone and don't depend on each other, so the join belongs in di.torq (it has both) — a di.torq-era task, not a di.api change.

Test coverage — test.csv (k4unit), 24 assertions (33 rows incl. before/run setup), all green

Area Coverage
init — dependency validation rejects non-dict deps, missing log key, non-dict log value, log dict missing a key
add / getapi registry starts empty after init; add stores one entry; public flag and description stored; re-adding a name overwrites in place (no duplicate); rejects non-symbol name; rejects non-boolean public
find / f / p f with a null symbol returns all, p returns only public; substring match; case-insensitive; string-glob pattern; f includes non-public entries while p excludes them; rejects a non-symbol/non-string pattern
getapimeta documents exactly the module's exports; rows carry the registry columns; a discovery function (find) is public while framework plumbing (init) is not

Running the tests

export QPATH=/path/to/kx/mod:/path/to/kdbx-modules
k4unit:use`di.k4unit;
k4unit.moduletest`di.api;   / prints the results table; "All tests passed" on success

Caveats — not deployable in a live process until two things land

di.api is complete and tested as a standalone unit. It cannot yet run inside a live process
because:

  1. di.log (the standard logger) is not merged and not final. di.api requires a conforming
    binary `info`warn`error logger injected at init; di.log provides exactly that, but it
    currently lives on the feature-logging branch and its contract isn't approved. If that contract
    changes, re-verify — no di.api code change is expected. Interim callers can hand-roll a logger
    (the tests do).
  2. di.torq does not exist yet. The orchestrator that reads each module's getapimeta, applies
    process-wide name qualification, and calls add for every row hasn't been built. add and
    getapimeta are unit-tested for that role, but the real collect-and-register flow is unproven
    until di.torq lands.

Deferred follow-ups

  • Add a committed di.log integration test once di.log merges (a use`di.log test can't be
    committed on this branch yet).
  • Add a version export + deps.q when di.depcheck ships (repo-wide versioning rollout — no
    module carries these yet).
  • Implement the exportconfig equivalent in di.torq (config values × api descriptions across
    namespaces), which is where both inputs are available.

Comment thread di/api/api.q
/ 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.

@DI-Software-Engineering

Copy link
Copy Markdown

DIReview Summary

0 critical | 1 warning(s) | 0 suggestion(s)

⚠️ Spec check skipped — tracker lookup failed (NO_REF_FOUND). Standards axis only.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants