diff --git a/parser/enrich.py b/parser/enrich.py index e699e85..30aa980 100644 --- a/parser/enrich.py +++ b/parser/enrich.py @@ -283,11 +283,12 @@ def classify_category(fn: dict) -> str: return "transformation" -# MEOS splits its API: the public *user* surface (meos.h + the public type -# headers) and the *internal* programmer surface. The latter is type-erased -# (``Datum``-generic), undocumented for end users, and must not be projected -# onto a network service — it is policy-excluded, like lifecycle/index. -_INTERNAL_FILES = {"meos_internal.h", "meos_internal_geo.h"} +# MEOS splits its API into a public *user* surface and an *internal* programmer +# surface. The single authored signal for that split is the doxygen `@ingroup`: +# a function is public iff it carries a group that is not `meos_internal_*`; a +# function with no group, or a `meos_internal_*` group, is internal. This ties +# the binding/network surface to the reference manual — the two derive from one +# human-authored tag and cannot drift. def _outparam(fn: dict, enums: set, type_encodings: dict): @@ -517,7 +518,9 @@ def enrich_idl(idl: dict) -> dict: type_encodings = build_type_encodings(functions, opaque_names) for fn in functions: - fn["api"] = ("internal" if fn.get("file") in _INTERNAL_FILES + group = fn.get("group") + fn["api"] = ("internal" + if (not group or group.startswith("meos_internal_")) else "public") fn["category"] = classify_category(fn) network, wire = assess(fn, type_encodings, enum_names) diff --git a/run.py b/run.py index 09a012e..016d6f7 100644 --- a/run.py +++ b/run.py @@ -117,7 +117,17 @@ def main(): for fn, pn, reason in out_drift: print(f" {fn}({pn}) — {reason}", file=sys.stderr) - # 1e. Derive service-projection metadata (category / encodings / network). + # 1e. Attach the doxygen @ingroup groups BEFORE enrich: the catalog `api` + # field derives from the group (public unless the group is + # `meos_internal_*` or absent), so the group must already be attached when + # enrich computes api and the api-dependent network projection. + _grp_root = Path(os.environ.get("MDB_SRC_ROOT", "./_mobilitydb")) + if (_grp_root / "meos" / "src").exists(): + idl, ngrp = attach_groups(idl, _grp_root / "meos" / "src", + _grp_root / "pgtypes") + print(f" attached {ngrp} doxygen @ingroup groups", file=sys.stderr) + + # 1f. Derive service-projection metadata (category / encodings / network). # Runs before the merge so manual annotations override the heuristics. idl = enrich_idl(idl) @@ -206,15 +216,6 @@ def main(): for fn, pn, reason in ba_drift: print(f" {fn}({pn}) — {reason}", file=sys.stderr) - # 5. Attach the doxygen module group (@ingroup) from the vendored source, so - # bindings organize their generated surface like the reference manual. The - # exported base-type functions (text_out, date_in, timestamp_cmp_internal, - # …) live in the vendored `pgtypes/` tree at the repo root and carry - # `@ingroup meos_base_*` there, so scan it alongside meos/src. - PGTYPES_SRC = SRC_ROOT / "pgtypes" - if MEOS_SRC.exists(): - idl, ngrp = attach_groups(idl, MEOS_SRC, PGTYPES_SRC) - print(f"[5/5] Attached {ngrp} doxygen @ingroup groups", file=sys.stderr) # Surface any forward-declared external ABI struct pointer in the API, so a # new one is classified explicitly instead of diverging per binding. diff --git a/tests/test_enrich.py b/tests/test_enrich.py index df2b823..6723b87 100644 --- a/tests/test_enrich.py +++ b/tests/test_enrich.py @@ -20,6 +20,9 @@ def fn(name, ret, *params): return { "name": name, "file": "meos.h", + # A public doxygen group: api derives from @ingroup, so a fixture + # function is public unless given a meos_internal_* group (or none). + "group": "meos_temporal", "returnType": {"c": ret, "canonical": ret}, "params": [{"name": n, "cType": t, "canonical": t} for t, n in params], } @@ -59,10 +62,10 @@ def fn(name, ret, *params): ("const struct Box *", "box"), ("int", "maxdd")), fn("weird_in", "struct Weird *", ("const char *", "str"), ("int", "basetype")), - # An otherwise-exposable function living in the internal header: it must - # be policy-excluded (api=internal), like the programmer Datum API. + # An otherwise-exposable function carrying an internal doxygen group: it + # must be policy-excluded (api=internal), like the programmer Datum API. dict(fn("internal_op", "struct Temporal *", (T, "temp")), - file="meos_internal.h"), + file="meos_internal.h", group="meos_internal_temporal"), # Scalar out-parameter accessor: bool f(.., int *result) — the value is # returned through the trailing out-param, the bool is a presence flag. fn("setspan_value_n", "int", @@ -227,6 +230,17 @@ def test_public_default(self): self.assertEqual(self.fns["temporal_eq"]["api"], "public") self.assertTrue(self.fns["temporal_eq"]["network"]["exposable"]) + def test_no_group_is_internal(self): + # A function with no doxygen @ingroup is internal (not documented, + # not part of the public surface), even in a public header. + idl = enrich_idl({ + "functions": [{k: v for k, v in fn("ungrouped_op", "int", + (T, "temp")).items() + if k != "group"}], + "structs": [dict(s) for s in STRUCTS], "enums": [], + }) + self.assertEqual(idl["functions"][0]["api"], "internal") + def test_scalar_outparam_projected_as_result(self): f = self.fns["setspan_value_n"] self.assertTrue(f["network"]["exposable"])