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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This catalog is the foundation for generating language bindings (Python, Java, R
- [Getting started](#getting-started)
- [Output format](#output-format)
- [Adding metadata](#adding-metadata)
- [OpenAPI generation](#openapi-generation)

## How it works

Expand Down Expand Up @@ -83,3 +84,30 @@ A typical function entry looks like this:
## Adding metadata

Manual annotations (ownership rules, additional documentation, deprecation flags, etc.) live in `meta/meos-meta.json`. The merger applies them on top of the libclang-parsed structure when generating the final catalog.

## OpenAPI generation

The enriched catalog (the `network` / `wire` / `typeEncodings` produced by the
service-projection pass) can be projected onto an **OpenAPI 3.1** contract —
this is the concrete "OpenAPI is a projection of MEOS-API" step:

```bash
python run.py # produce the enriched catalog
python generate_openapi.py # output/meos-idl.json -> output/meos-openapi.json
```

Every *stateless-exposable* MEOS function becomes one RPC-style
`POST /{function}` operation (≈ an OGC API – Processes "process"); opaque
values cross the wire as strings carried in their `typeEncodings`
(text / MF-JSON / HexWKB), surfaced as reusable component schemas. `x-meos-*`
extensions carry the decode/encode function names and category so a
downstream server or MCP generator can consume the same document.

Against the live MobilityDB `master` catalog this yields **1952 operations**
(90% of the public API; internal `meos_internal*.h` policy-excluded),
including array-of-string params for builders. The generator is pure
`dict` → `dict` (no libclang,
no MEOS runtime); see [`docs/openapi.md`](docs/openapi.md) for the projection
rules, `x-meos-*` extensions, and roadmap (OGC API, MCP, runtime server), and
[`tests/test_openapi.py`](tests/test_openapi.py) for worked examples
(`python3 tests/test_openapi.py`).
75 changes: 75 additions & 0 deletions docs/openapi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# OpenAPI projection

`generator/openapi.py` turns the **enriched** catalog (`meos-idl.json` with
`category` / `network` / `wire` / `typeEncodings` — see
[`enrichment.md`](enrichment.md)) into an **OpenAPI 3.1** document. It is the
concrete realisation of "OpenAPI is a projection of MEOS-API": the canonical
semantic catalog is the single source, OpenAPI is one rendering of it.

```bash
python run.py # enriched catalog -> output/meos-idl.json
python generate_openapi.py # -> output/meos-openapi.json
```

Pure `dict` → `dict`: no libclang, no MEOS runtime, deterministic output
(sorted paths/schemas) so generated diffs are reviewable.

## Projection rules

| MEOS concept | OpenAPI |
|---|---|
| stateless-exposable function | one `POST /{function}` operation, `operationId = function` |
| `category` | operation `tags` + `x-meos-category`; spec-level `tags` list |
| parameter | property of the JSON request body (all required, `additionalProperties:false`) |
| `wire.kind = json` scalar | `{"type": integer\|number\|boolean\|string}` |
| `wire` enum | `$ref` to a component enum schema (string, real C constant names) |
| `wire.kind = serialized` | `allOf` → `$ref` to the type's component schema, plus `x-meos-decode` (request) / `x-meos-encode` (response) |
| `wire.kind = array` (builder `(Elem **,count)`) | `{"type":"array","items":<element schema>}` + `x-meos-decode`; the C `count` is the array length |
| out-parameter result (`from_outparam`) | the out-param value is the response (scalar JSON or serialized); `presence_return` false ⇒ `204` |
| `wire.result.kind = void` | `204 No Content` |
| any error | `default` → `#/components/responses/MeosError` |
| `typeEncodings[T]` | `components.schemas.T` = `{"type":"string", x-meos-encodings, x-meos-in, x-meos-out}` |

RPC-style, not resource-style, is deliberate: MEOS is a value algebra, so a
function ≈ an **OGC API – Processes** "process". A resource model
(OGC API – Moving Features collections) is a different projection, layered
later (and already partly served by
[MobilityAPI](https://github.com/MobilityDB/MobilityAPI)).

## `x-meos-*` extensions

The spec is self-describing for downstream generators (server, MCP, gRPC):

- `info.x-meos-coverage` — `{functions, exposed}`.
- operation `x-meos-category`, `x-meos-encode`.
- serialized request property `x-meos-decode` — the MEOS parse function.
- component schema `x-meos-encodings` / `x-meos-in` / `x-meos-out` — the wire
encodings and the MEOS in/out function names.

A server generator marshals a request by calling `x-meos-decode` on each
serialized string, invoking the function, and calling `x-meos-encode` on the
result — no extra metadata needed beyond this document.

## Coverage (live MobilityDB `master`)

2161 **public** functions → **1952 operations (85%)** — the internal
`meos_internal*.h` programmer API (511 fns, `Datum`-generic) is
policy-excluded. Tagged across `predicate`, `transformation`, `accessor`,
`io`, `conversion`, `setop`, `aggregate`, `constructor`. The remaining
public functions (multi-out/array builders, opaque-no-codec, polymorphic
input-decode, lifecycle/index) carry a truthful `reason` and are
overridable via `meta/meos-meta.json`.

## Limitations / roadmap

- **No OpenAPI conformance validation** in-tree yet (structural checks only:
every path a single `POST` with responses, all `$ref`s resolve). Adding
`openapi-spec-validator` to CI is a follow-up.
- **MCP tool manifest** — the same `wire`/`typeEncodings` model maps directly
onto MCP tool schemas; a sibling generator is the natural next unit.
- **Runtime server** — a generated marshaling server (decode → call → encode)
is out of scope here; this PR delivers the *contract*, not the server.
- **OGC API – Moving Features** resource projection is a separate effort.
- Preferred in/out per type currently follows catalog scan order (e.g.
`tbool_in` may be picked over `temporal_in`); both are valid decoders, but
refining the preference is a small enrichment-side follow-up.
40 changes: 40 additions & 0 deletions generate_openapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Generate an OpenAPI 3.1 contract from the enriched MEOS catalog.
#
# Usage:
# python run.py # first, to produce the catalog
# python generate_openapi.py # reads output/meos-idl.json
# python generate_openapi.py path.json [out.json]

import json
import sys
from pathlib import Path

from generator.openapi import build_openapi

IN_PATH = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("output/meos-idl.json")
OUT_PATH = Path(sys.argv[2]) if len(sys.argv) > 2 else Path("output/meos-openapi.json")


def main() -> None:
if not IN_PATH.exists():
sys.exit(f"Catalog not found: {IN_PATH} — run `python run.py` first.")

catalog = json.loads(IN_PATH.read_text())
if "functions" not in catalog or not any(
"network" in f for f in catalog["functions"]
):
sys.exit(f"{IN_PATH} is not enriched (no `network` fields). "
"Run the enrichment pass first.")

spec = build_openapi(catalog)

OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
OUT_PATH.write_text(json.dumps(spec, indent=2))

print(f"[openapi] {len(spec['paths'])} operations, "
f"{len(spec['components']['schemas'])} component schemas "
f"→ {OUT_PATH}", file=sys.stderr)


if __name__ == "__main__":
main()
209 changes: 209 additions & 0 deletions generator/openapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
"""OpenAPI 3.1 generator.

Projects the *enriched* MEOS catalog (``meos-idl.json`` with ``category`` /
``network`` / ``wire`` / ``typeEncodings``, produced by ``parser/enrich.py``)
onto an OpenAPI 3.1 service contract.

The projection is deliberately RPC-style — MEOS is a value algebra, not a
REST resource model, so each *stateless-exposable* function becomes one
``POST /{function}`` operation (≈ an OGC API – Processes "process"). Opaque
values cross the wire as strings carried in their `typeEncodings` (text /
MF-JSON / WKB); each opaque type and referenced enum becomes a reusable
component schema. ``x-meos-*`` extensions carry the decode/encode function
names and category so a downstream server or MCP generator can consume this
same document.

Pure ``dict`` → ``dict``; no libclang and no MEOS runtime. Only functions
with ``network.exposable == true`` are emitted; the rest are reported by
``build_openapi``'s return-value count via the caller.
"""

import re

_QUAL_RE = re.compile(r"\b(const|volatile|struct|union|enum)\b")

_PRIMITIVE = {
"integer": {"type": "integer"},
"number": {"type": "number"},
"boolean": {"type": "boolean"},
"string": {"type": "string"},
}


def _clean_type(c_type: str) -> str:
"""``const struct Temporal *`` -> ``Temporal`` (matches typeEncodings keys)."""
return " ".join(_QUAL_RE.sub(" ", c_type).replace("*", " ").split())


def _scalar_schema(wire: dict, used_enums: set) -> dict:
if wire.get("enum"):
used_enums.add(wire["enum"])
return {"$ref": f"#/components/schemas/{wire['enum']}"}
return dict(_PRIMITIVE.get(wire.get("json", "string"), {"type": "string"}))


def _value_schema(wire: dict, used_types: set, used_enums: set) -> dict:
"""Schema for one parameter or the result."""
kind = wire["kind"]
if kind == "json":
return _scalar_schema(wire, used_enums)
if kind == "serialized":
t = _clean_type(wire["cType"])
used_types.add(t)
return {"$ref": f"#/components/schemas/{t}"}
if kind == "array":
return {"type": "array",
"items": _value_schema(wire["element"], used_types,
used_enums)}
# Should not happen for an exposable function.
return {"type": "string"}


def _operation(fn: dict, used_types: set, used_enums: set) -> dict:
wire = fn["wire"]
op = {
"operationId": fn["name"],
"summary": fn.get("doc") or fn["name"],
"tags": [fn["category"]],
"x-meos-category": fn["category"],
}

params = wire.get("params", [])
if params:
props, required = {}, []
for p in params:
props[p["name"]] = _value_schema(p, used_types, used_enums)
required.append(p["name"])
if p["kind"] == "serialized":
props[p["name"]] = {
"allOf": [props[p["name"]]],
"x-meos-decode": p["decode"],
}
elif p["kind"] == "array":
props[p["name"]] = {
**props[p["name"]],
"x-meos-decode": p["element"]["decode"],
}
op["requestBody"] = {
"required": True,
"content": {"application/json": {"schema": {
"type": "object",
"required": required,
"additionalProperties": False,
"properties": props,
}}},
}

result = wire["result"]
if result["kind"] == "void":
op["responses"] = {"204": {"description": "No content"}}
else:
schema = _value_schema(result, used_types, used_enums)
content_schema = schema
if result["kind"] == "serialized":
op["x-meos-encode"] = result["encode"]
op["responses"] = {"200": {
"description": "Result",
"content": {"application/json": {"schema": content_schema}},
}}
op["responses"]["default"] = {
"$ref": "#/components/responses/MeosError"
}
return op


def _type_schema(name: str, type_encodings: dict) -> dict:
te = type_encodings.get(name)
if not te:
return {"type": "string", "title": name}
encs = te.get("encodings", [])
return {
"type": "string",
"title": name,
"description": (
f"Serialized MEOS {name}. Wire encodings: {', '.join(encs)} "
f"(e.g. WKT / MF-JSON / HexWKB)."
),
"x-meos-encodings": encs,
"x-meos-in": te.get("in"),
"x-meos-out": te.get("out"),
}


def _enum_schema(name: str, enums: list) -> dict:
for e in enums:
if e["name"] == name:
return {
"type": "string",
"title": name,
"enum": [v["name"] for v in e.get("values", [])],
"x-meos-c-enum": True,
}
return {"type": "string", "title": name}


def build_openapi(catalog: dict, *, title: str = "MEOS API",
version: str = "0.1.0") -> dict:
"""Build an OpenAPI 3.1 document from an enriched catalog."""
functions = sorted(
(f for f in catalog.get("functions", [])
if f.get("network", {}).get("exposable")),
key=lambda f: f["name"],
)
type_encodings = catalog.get("typeEncodings", {})
enums = catalog.get("enums", [])

used_types: set = set()
used_enums: set = set()
paths: dict = {}
tags_seen: set = set()

for fn in functions:
paths[f"/{fn['name']}"] = {
"post": _operation(fn, used_types, used_enums)
}
tags_seen.add(fn["category"])

schemas = {}
for t in sorted(used_types):
schemas[t] = _type_schema(t, type_encodings)
for e in sorted(used_enums):
schemas[e] = _enum_schema(e, enums)

total = len(catalog.get("functions", []))
return {
"openapi": "3.1.0",
"info": {
"title": title,
"version": version,
"description": (
"Auto-generated from the MEOS-API catalog. Each operation is "
"a stateless-exposable MEOS function projected RPC-style as "
"`POST /{function}`; opaque values cross the wire as strings "
"in the encodings listed on their component schema. "
"Generated, do not edit by hand."
),
"x-meos-coverage": {
"functions": total,
"exposed": len(functions),
},
},
"tags": [{"name": t} for t in sorted(tags_seen)],
"paths": dict(sorted(paths.items())),
"components": {
"schemas": schemas,
"responses": {
"MeosError": {
"description": "MEOS error",
"content": {"application/json": {"schema": {
"type": "object",
"properties": {
"error": {"type": "string"},
"code": {"type": "integer"},
},
"required": ["error"],
}}},
}
},
},
}
Loading