vectrixdb 2.2.0

Client for VectrixDB: collections, documents, text search, sources.
Documentation
"""Write src/generated.rs from ../../docs/reference/openapi.json: the request bodies the client sends.

    python3 -I scripts/generate.py        # run from sdk/rust

Only the schemas named in ROOTS, and the ones they reference, are emitted.
Field names and optionality are exactly the spec's: a field the schema does
not list as required is an Option that is left out of the JSON when None.
"""

from __future__ import annotations

import json
import shutil
import subprocess
from pathlib import Path

ROOTS = [
    "CreateCollectionRequestV2",
    "TextUpsertRequest",
    "TextSearchRequest",
    "AddSourceRequest",
    "RefreshRequest",
]

HERE = Path(__file__).resolve().parent.parent
SPEC = HERE.parent.parent / "docs" / "reference" / "openapi.json"
OUT = HERE / "src" / "generated.rs"


def rust_type(schema: dict, schemas: dict, needed: list) -> str:
    """The Rust type for a property schema (never wrapped in Option here)."""
    if "$ref" in schema:
        name = schema["$ref"].rsplit("/", 1)[1]
        if name not in needed:
            needed.append(name)
        return name
    if "anyOf" in schema:
        inner = [s for s in schema["anyOf"] if s.get("type") != "null"]
        if len(inner) == 1:
            return rust_type(inner[0], schemas, needed)
        return "serde_json::Value"
    kind = schema.get("type")
    if kind == "string":
        return "String"
    if kind == "integer":
        return "i64"
    if kind == "number":
        return "f64"
    if kind == "boolean":
        return "bool"
    if kind == "array":
        return f"Vec<{rust_type(schema.get('items', {}), schemas, needed)}>"
    if kind == "object":
        return "serde_json::Map<String, serde_json::Value>"
    return "serde_json::Value"


def doc_lines(text: str | None, indent: str) -> list[str]:
    if not text:
        return []
    first = text.strip().splitlines()[0].strip()
    return [f"{indent}/// {first}"]


def emit_struct(name: str, schema: dict, schemas: dict, needed: list) -> list[str]:
    required = set(schema.get("required", []))
    lines = doc_lines(schema.get("description"), "")
    lines += [
        "#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]",
        f"pub struct {name} {{",
    ]
    for field, prop in sorted(schema.get("properties", {}).items()):
        lines += doc_lines(prop.get("description"), "    ")
        ty = rust_type(prop, schemas, needed)
        if field in required:
            lines.append(f"    pub {field}: {ty},")
        else:
            lines.append('    #[serde(default, skip_serializing_if = "Option::is_none")]')
            lines.append(f"    pub {field}: Option<{ty}>,")
    lines.append("}")
    lines.append("")
    return lines


def main() -> None:
    spec = json.loads(SPEC.read_text())
    schemas = spec["components"]["schemas"]
    needed = list(ROOTS)
    out = [
        "// Generated by scripts/generate.py from docs/reference/openapi.json; do not edit.",
        "// Regenerate with: python3 -I scripts/generate.py (run from sdk/rust).",
        f"// Schemas: {', '.join(ROOTS)} and the ones they reference.",
        "//",
        "// The request bodies the client sends. Every field the spec does not list",
        "// as required is an Option that is omitted from the JSON when None.",
        "",
    ]
    i = 0
    while i < len(needed):
        name = needed[i]
        out += emit_struct(name, schemas[name], schemas, needed)
        i += 1
    OUT.write_text("\n".join(out).rstrip() + "\n")
    if shutil.which("rustfmt"):
        subprocess.run(["rustfmt", "--edition", "2021", str(OUT)], check=True)
    print(f"wrote {OUT} ({len(needed)} structs)")


if __name__ == "__main__":
    main()