#!/usr/bin/env python3 """Reference binding generator for ocs_doc_api (plan ยง10.2). Reads `gen/binding_schema.json` and emits a Python module `ocs_doc.py` with the object model (DocApi -> Document -> collections -> handles) whose methods build `DocApiEnvelope` ops/queries and ship them through a caller-supplied transport. This is the *contract* in executable form (bindings/README.md): one versioned schema, no Rust FFI, thin serializer. Lua/JS generators follow the same shape. Usage: python bindings/python/generate.py gen/binding_schema.json > ocs_doc.py The emitted module expects a `transport` object with: def apply(self, envelope: DocApiEnvelope) -> Receipt which the bridge author implements (the session glue over the plugin channel). """ import json import sys from typing import List, Dict, Any HEADER = '''"""Auto-generated ocs_doc_api Python bindings. Do NOT edit by hand. Generated by bindings/python/generate.py from binding_schema.json (envelope_version {ver}). Check envelope_version before use; see bindings/README.md.""" from dataclasses import dataclass from typing import Any, List, Optional # --- wire DTOs (thin, schema-driven) ----------------------------------------- ObjectId = int # u64 @dataclass class Receipt: outcome: Any query_results: List[Any] new_revision: int @dataclass class DocApiEnvelope: version: int body: Any # ("Op", operation_dict) | ("Queries", [query_dict]) ENVELOPE_VERSION = {ver} def _check_version(schema_version: int) -> None: if schema_version != ENVELOPE_VERSION: raise RuntimeError( f"binding_schema envelope_version {{schema_version}} != {{ENVELOPE_VERSION}}" ) ''' HANDLE_TEMPLATE = ''' @dataclass class {handle}: id: ObjectId _doc: "Document" {methods} ''' # Rust type -> Python annotation (schema types are Rust-flavoured). PY_TYPES = { "Solid": "'Solid'", "Line": "'Line'", "Circle": "'Circle'", "Polyline": "'Polyline'", "Point": "'Point'", "Entity": "'Entity'", "ArcCurve": "'ArcCurve'", "Ellipse": "'Ellipse'", "Spline": "'Spline'", "Ray": "'Ray'", "XLine": "'XLine'", "Text": "'Text'", "MText": "'MText'", "Dimension": "'Dimension'", "f64": "float", "bool": "bool", "usize": "int", "i32": "int", "[f64; 3]": "tuple", "[f64; 2]": "tuple", "ObjectId": "ObjectId", "String": "str", "&str": "str", "()": "None", "None": "None", "Vec<(String, String)>": "list", "Vec>": "list", "([f64; 3], f64)": "tuple", "Vec": "list", "Vec": "list", } def py_type(rust_ty: str) -> str: return PY_TYPES.get(rust_ty, "Any") def py_lit(v: Any) -> str: """A JSON value as a Python literal (handles booleans/None).""" if v is True: return "True" if v is False: return "False" if v is None: return "None" return json.dumps(v) METHOD_OP = ''' def {name}(self{sig}) -> {ret}: op = {{"{variant}": {body}}} return self._doc._apply_op(op) ''' METHOD_QUERY = ''' def {name}(self{sig}) -> {ret}: q = {{"{variant}": {body}}} return self._doc._apply_query(q) ''' def render_method(m: Dict[str, Any]) -> str: """Emit one facade method. `self.id` is the receiver's ObjectId (`a`/`id`); each handle-typed arg contributes its `.id`. Fixed fields are injected from the schema node.""" name = m["name"] args = m.get("args", []) sig = "".join(f", {a['name']}" for a in args) ret = py_type(m.get("returns") or "None") # Receiver is `a` for SolidBoolean, `id` otherwise. Handle args map to .id. field_for_arg = {} for a in args: # a handle arg becomes `_id` in the payload when the variant needs ids field_for_arg[a["name"]] = a["name"] body_items: List[str] = [] variant = m["op"] if m["kind"] == "op" else m["query"] if variant == "SolidBoolean": body_items.append("'op': '%s'" % (m.get("fixed", {}).get("op", ""))) body_items.append("'a': self.id") if args: body_items.append(f"'b': {args[0]['name']}.id") body_items.append(f"'erase_sources': {py_lit((m.get('fixed') or {}).get('erase_sources', True))}") elif variant in ("GetVolume", "GetCentroid", "GetBounds", "GetEntity", "GetTextContent", "GetHatchBoundary", "GetDimensionMeasurement", "GetAttributes", "GetViewportView"): body_items.append("'id': self.id") elif variant in ("SetTextContent", "SetAttribute", "SetViewportView", "Transform", "Delete", "AddVertex"): # Op variants that carry the receiver ObjectId: inject it first. body_items.append("'id': self.id") for a in args: body_items.append(f"'{a['name']}': {a['name']}") elif variant == "GetIntersects": body_items.append("'a': self.id") if args: body_items.append(f"'b': {args[0]['name']}.id") else: for a in args: body_items.append(f"'{a['name']}': {a['name']}") body = "{" + ", ".join(body_items) + "}" if m["kind"] == "op": return METHOD_OP.format(name=name, sig=sig, ret=ret, variant=variant, body=body) return METHOD_QUERY.format(name=name, sig=sig, ret=ret, variant=variant, body=body) def render(schema: Dict[str, Any]) -> str: out = [HEADER.format(ver=schema["envelope_version"])] handles = set(schema["object_model"]["handles"]) for fam in schema["families"]: handle = fam.get("handle") or fam["name"] methods = "\n".join(render_method(m) for m in fam.get("methods", [])) if not methods.strip(): methods = " pass\n" out.append(HANDLE_TEMPLATE.format(handle=handle, methods=methods)) out.append( ''' @dataclass class Document: _transport: Any _tab: int def _apply_op(self, op, want=None): env = DocApiEnvelope(ENVELOPE_VERSION, ("Op", op)) return self._transport.apply(env) def _apply_query(self, q, want=None): env = DocApiEnvelope(ENVELOPE_VERSION, ("Queries", [q])) return self._transport.apply(env) def solids(self): return Solids(self) def curves(self): return Curves(self) def entities(self): return Entities(self) class Solids: def __init__(self, doc): self._doc = doc class Curves: def __init__(self, doc): self._doc = doc class Entities: def __init__(self, doc): self._doc = doc @dataclass class DocApi: _transport: Any def document(self, tab: int) -> Document: return Document(self._transport, tab) ''' ) return "\n".join(out) def main() -> None: path = sys.argv[1] if len(sys.argv) > 1 else "gen/binding_schema.json" with open(path, "r", encoding="utf-8") as f: schema = json.load(f) sys.stdout.write(render(schema)) if __name__ == "__main__": main()