返回博客

generate_bindings:从 Ontology 自动生成类型安全的函数绑定

coomia-dip 的 generatebindings 工具自动从 Ontology Schema 生成多语言的类型安全绑定代码,让用户自定义函数能以原生类型操作 ObjectType、LinkType 和 ActionType,无需手动编写序列化/反序列化逻辑。支持 Python(Pydantic)、TypeScript(Zod)、Kotlin(data class) 和 Rust(serde) 四种目标语言。本文详解代码生成器的架构、模板引擎、类型映射规则、增量生成策略和 CI/CD 集成。

Coomia发布于 2025年9月13日12 分钟阅读
分享本文Twitter / X

系列:S5 智能决策 · 第 21 篇 | 难度:高级 | 阅读时间:20 分钟

generate_bindings:从 Ontology 自动生成类型安全的函数绑定

#TL;DR

coomia-dip 的 generate_bindings 工具自动从 Ontology Schema 生成多语言的类型安全绑定代码,让用户自定义函数能以原生类型操作 ObjectType、LinkType 和 ActionType,无需手动编写序列化/反序列化逻辑。支持 Python(Pydantic)TypeScript(Zod)Kotlin(data class)Rust(serde) 四种目标语言。本文详解代码生成器的架构、模板引擎、类型映射规则、增量生成策略和 CI/CD 集成。

#1. 为什么需要自动生成绑定

#1.1 手动绑定的痛点

Code
手动绑定 vs 自动生成:

  手动绑定:
  ┌─────────────────────────────────────────┐
  │ 1. 阅读 Ontology Schema                  │
  │ 2. 手动编写数据类/接口                     │
  │ 3. 编写序列化/反序列化逻辑                  │
  │ 4. 编写类型验证逻辑                        │
  │ 5. Schema 变更时手动同步更新               │
  └─────────────────────────────────────────┘
       │
       ▼
  问题: 类型不一致、遗漏字段、同步滞后

  自动生成:
  ┌─────────────────────────────────────────┐
  │ 1. Ontology Schema 更新                  │
  │ 2. generate_bindings 自动执行             │
  │ 3. 生成类型安全的原生代码                   │
  │ 4. 编译时/运行时类型检查                   │
  └─────────────────────────────────────────┘
       │
       ▼
  优势: 零手动维护、类型安全、始终与 Schema 同步

#2. 代码生成器架构

#2.1 整体流程

Code
generate_bindings 流程:

  Ontology Schema (gRPC/JSON)
       │
       ▼
  ┌──────────────────┐
  │  Schema Parser    │  解析 ObjectType/LinkType/ActionType
  └──────┬───────────┘
         │
         ▼
  ┌──────────────────┐
  │  IR (中间表示)     │  语言无关的类型描述
  └──────┬───────────┘
         │
    ┌────┼────┬────┐
    ▼    ▼    ▼    ▼
  ┌────┐┌────┐┌────┐┌────┐
  │ Py ││ TS ││ Kt ││ Rs │  语言特定代码生成器
  └──┬─┘└──┬─┘└──┬─┘└──┬─┘
     │     │     │     │
     ▼     ▼     ▼     ▼
  .py   .ts   .kt   .rs    生成的绑定文件

#2.2 核心数据模型

Python
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Any


class IRType(Enum):
    """中间表示类型"""
    STRING = "string"
    INTEGER = "integer"
    FLOAT = "float"
    BOOLEAN = "boolean"
    DATETIME = "datetime"
    ARRAY = "array"
    MAP = "map"
    OBJECT_REF = "object_ref"    # 引用其他 ObjectType
    ENUM = "enum"
    OPTIONAL = "optional"


@dataclass
class IRField:
    """中间表示字段"""
    name: str
    ir_type: IRType
    description: str = ""
    is_required: bool = True
    default_value: Any = None
    element_type: IRType | None = None    # array/map 的元素类型
    ref_type: str | None = None           # object_ref 引用的类型名
    enum_values: list[str] | None = None
    constraints: dict[str, Any] = field(default_factory=dict)


@dataclass
class IRObjectType:
    """中间表示对象类型"""
    name: str
    api_name: str                          # API 中使用的名称
    description: str
    fields: list[IRField]
    primary_key: str = "id"
    is_abstract: bool = False
    parent_type: str | None = None


@dataclass
class IRLinkType:
    """中间表示关系类型"""
    name: str
    source_type: str
    target_type: str
    cardinality: str = "many_to_many"
    properties: list[IRField] = field(default_factory=list)


@dataclass
class IRActionType:
    """中间表示动作类型"""
    name: str
    object_type: str
    parameters: list[IRField]
    return_type: IRField | None = None
    description: str = ""


@dataclass
class IRSchema:
    """完整的中间表示"""
    namespace: str
    version: str
    object_types: list[IRObjectType]
    link_types: list[IRLinkType]
    action_types: list[IRActionType]

#3. Schema 解析器

Python
class OntologySchemaParser:
    """Ontology Schema 解析器"""

    def parse(self, schema_data: dict) -> IRSchema:
        """解析 Ontology Schema 为 IR"""
        object_types = [
            self._parse_object_type(ot)
            for ot in schema_data.get("objectTypes", [])
        ]

        link_types = [
            self._parse_link_type(lt)
            for lt in schema_data.get("linkTypes", [])
        ]

        action_types = [
            self._parse_action_type(at)
            for at in schema_data.get("actionTypes", [])
        ]

        return IRSchema(
            namespace=schema_data.get("namespace", "onto"),
            version=schema_data.get("version", "1.0.0"),
            object_types=object_types,
            link_types=link_types,
            action_types=action_types,
        )

    def _parse_object_type(self, data: dict) -> IRObjectType:
        fields = [
            IRField(
                name=f["name"],
                ir_type=self._map_type(f["type"]),
                description=f.get("description", ""),
                is_required=f.get("required", True),
                default_value=f.get("default"),
                element_type=self._map_type(f["elementType"]) if "elementType" in f else None,
                ref_type=f.get("refType"),
                enum_values=f.get("enumValues"),
                constraints=f.get("constraints", {}),
            )
            for f in data.get("properties", [])
        ]

        return IRObjectType(
            name=data["name"],
            api_name=data.get("apiName", data["name"]),
            description=data.get("description", ""),
            fields=fields,
            primary_key=data.get("primaryKey", "id"),
        )

    def _parse_link_type(self, data: dict) -> IRLinkType:
        return IRLinkType(
            name=data["name"],
            source_type=data["sourceType"],
            target_type=data["targetType"],
            cardinality=data.get("cardinality", "many_to_many"),
        )

    def _parse_action_type(self, data: dict) -> IRActionType:
        params = [
            IRField(
                name=p["name"],
                ir_type=self._map_type(p["type"]),
                description=p.get("description", ""),
                is_required=p.get("required", True),
            )
            for p in data.get("parameters", [])
        ]

        return IRActionType(
            name=data["name"],
            object_type=data.get("objectType", ""),
            parameters=params,
            description=data.get("description", ""),
        )

    def _map_type(self, type_str: str) -> IRType:
        mapping = {
            "string": IRType.STRING,
            "integer": IRType.INTEGER,
            "int": IRType.INTEGER,
            "float": IRType.FLOAT,
            "double": IRType.FLOAT,
            "boolean": IRType.BOOLEAN,
            "bool": IRType.BOOLEAN,
            "datetime": IRType.DATETIME,
            "timestamp": IRType.DATETIME,
            "array": IRType.ARRAY,
            "list": IRType.ARRAY,
            "map": IRType.MAP,
            "dict": IRType.MAP,
        }
        return mapping.get(type_str.lower(), IRType.STRING)

#4. 语言代码生成器

#4.1 Python (Pydantic) 生成器

Python
from abc import ABC, abstractmethod


class CodeGenerator(ABC):
    """代码生成器基类"""

    @abstractmethod
    def generate(self, schema: IRSchema) -> dict[str, str]:
        """生成代码,返回 {文件名: 内容}"""
        ...


class PythonPydanticGenerator(CodeGenerator):
    """Python Pydantic 模型生成器"""

    TYPE_MAP = {
        IRType.STRING: "str",
        IRType.INTEGER: "int",
        IRType.FLOAT: "float",
        IRType.BOOLEAN: "bool",
        IRType.DATETIME: "datetime",
        IRType.ARRAY: "list",
        IRType.MAP: "dict[str, Any]",
    }

    def generate(self, schema: IRSchema) -> dict[str, str]:
        files = {}

        # 生成 models.py
        lines = [
            '"""Auto-generated Ontology bindings. DO NOT EDIT."""',
            "from __future__ import annotations",
            "from datetime import datetime",
            "from typing import Any",
            "from pydantic import BaseModel, Field",
            "",
        ]

        for ot in schema.object_types:
            lines.extend(self._generate_object_type(ot))
            lines.append("")

        for at in schema.action_types:
            lines.extend(self._generate_action_type(at))
            lines.append("")

        files["models.py"] = "\n".join(lines)

        # 生成 __init__.py
        exports = [ot.name for ot in schema.object_types]
        exports += [f"{at.name}Params" for at in schema.action_types]
        init_lines = [
            f'"""Ontology bindings v{schema.version}"""',
            f"from .models import {', '.join(exports)}",
            "",
            f"__all__ = {exports}",
            f'__version__ = "{schema.version}"',
        ]
        files["__init__.py"] = "\n".join(init_lines)

        return files

    def _generate_object_type(self, ot: IRObjectType) -> list[str]:
        lines = []
        if ot.description:
            lines.append(f'class {ot.name}(BaseModel):')
            lines.append(f'    """{ot.description}"""')
        else:
            lines.append(f'class {ot.name}(BaseModel):')

        for field in ot.fields:
            type_str = self._field_type(field)
            if field.is_required:
                if field.description:
                    lines.append(
                        f'    {field.name}: {type_str} = Field(..., description="{field.description}")'
                    )
                else:
                    lines.append(f'    {field.name}: {type_str}')
            else:
                default = repr(field.default_value) if field.default_value is not None else "None"
                lines.append(f'    {field.name}: {type_str} | None = {default}')

        if not ot.fields:
            lines.append('    pass')

        return lines

    def _generate_action_type(self, at: IRActionType) -> list[str]:
        lines = [f'class {at.name}Params(BaseModel):']
        if at.description:
            lines.append(f'    """{at.description}"""')

        for param in at.parameters:
            type_str = self._field_type(param)
            if param.is_required:
                lines.append(f'    {param.name}: {type_str}')
            else:
                lines.append(f'    {param.name}: {type_str} | None = None')

        if not at.parameters:
            lines.append('    pass')

        return lines

    def _field_type(self, field: IRField) -> str:
        if field.ir_type == IRType.OBJECT_REF:
            return field.ref_type or "Any"
        if field.ir_type == IRType.ARRAY:
            elem = self.TYPE_MAP.get(field.element_type, "Any") if field.element_type else "Any"
            return f"list[{elem}]"
        if field.ir_type == IRType.ENUM:
            return "str"  # 简化处理
        return self.TYPE_MAP.get(field.ir_type, "Any")

#4.2 TypeScript (Zod) 生成器

Python
class TypeScriptZodGenerator(CodeGenerator):
    """TypeScript Zod Schema 生成器"""

    TYPE_MAP = {
        IRType.STRING: "z.string()",
        IRType.INTEGER: "z.number().int()",
        IRType.FLOAT: "z.number()",
        IRType.BOOLEAN: "z.boolean()",
        IRType.DATETIME: "z.string().datetime()",
        IRType.MAP: "z.record(z.string(), z.unknown())",
    }

    def generate(self, schema: IRSchema) -> dict[str, str]:
        files = {}
        lines = [
            "// Auto-generated Ontology bindings. DO NOT EDIT.",
            'import { z } from "zod";',
            "",
        ]

        for ot in schema.object_types:
            lines.extend(self._generate_object_type(ot))
            lines.append("")

        for at in schema.action_types:
            lines.extend(self._generate_action_type(at))
            lines.append("")

        # 导出类型
        lines.append("// Type exports")
        for ot in schema.object_types:
            lines.append(f"export type {ot.name} = z.infer<typeof {ot.name}Schema>;")

        files["ontology.ts"] = "\n".join(lines)
        return files

    def _generate_object_type(self, ot: IRObjectType) -> list[str]:
        lines = [f"export const {ot.name}Schema = z.object({{"]
        for field in ot.fields:
            zod_type = self._field_type(field)
            if not field.is_required:
                zod_type += ".optional()"
            lines.append(f"  {field.name}: {zod_type},")
        lines.append("});")
        return lines

    def _generate_action_type(self, at: IRActionType) -> list[str]:
        lines = [f"export const {at.name}ParamsSchema = z.object({{"]
        for param in at.parameters:
            zod_type = self._field_type(param)
            if not param.is_required:
                zod_type += ".optional()"
            lines.append(f"  {param.name}: {zod_type},")
        lines.append("});")
        return lines

    def _field_type(self, field: IRField) -> str:
        if field.ir_type == IRType.ARRAY:
            elem = self.TYPE_MAP.get(field.element_type, "z.unknown()") if field.element_type else "z.unknown()"
            return f"z.array({elem})"
        if field.ir_type == IRType.ENUM and field.enum_values:
            values = ", ".join(f'"{v}"' for v in field.enum_values)
            return f"z.enum([{values}])"
        if field.ir_type == IRType.OBJECT_REF:
            return f"{field.ref_type}Schema"
        return self.TYPE_MAP.get(field.ir_type, "z.unknown()")

#4.3 Kotlin 生成器

Python
class KotlinDataClassGenerator(CodeGenerator):
    """Kotlin data class 生成器"""

    TYPE_MAP = {
        IRType.STRING: "String",
        IRType.INTEGER: "Int",
        IRType.FLOAT: "Double",
        IRType.BOOLEAN: "Boolean",
        IRType.DATETIME: "Instant",
        IRType.MAP: "Map<String, Any>",
    }

    def generate(self, schema: IRSchema) -> dict[str, str]:
        files = {}
        lines = [
            "// Auto-generated Ontology bindings. DO NOT EDIT.",
            f"package {schema.namespace}.ontology",
            "",
            "import java.time.Instant",
            "import kotlinx.serialization.Serializable",
            "",
        ]

        for ot in schema.object_types:
            lines.extend(self._generate_object_type(ot))
            lines.append("")

        files["OntologyModels.kt"] = "\n".join(lines)
        return files

    def _generate_object_type(self, ot: IRObjectType) -> list[str]:
        lines = ["@Serializable"]
        params = []
        for field in ot.fields:
            kt_type = self._field_type(field)
            if not field.is_required:
                kt_type += "?"
                params.append(f"    val {field.name}: {kt_type} = null")
            else:
                params.append(f"    val {field.name}: {kt_type}")

        lines.append(f"data class {ot.name}(")
        lines.append(",\n".join(params))
        lines.append(")")
        return lines

    def _field_type(self, field: IRField) -> str:
        if field.ir_type == IRType.ARRAY:
            elem = self.TYPE_MAP.get(field.element_type, "Any") if field.element_type else "Any"
            return f"List<{elem}>"
        if field.ir_type == IRType.OBJECT_REF:
            return field.ref_type or "Any"
        return self.TYPE_MAP.get(field.ir_type, "Any")

#5. CLI 工具

Python
import argparse
import json
import os


class GenerateBindingsCLI:
    """generate_bindings 命令行工具"""

    GENERATORS = {
        "python": PythonPydanticGenerator,
        "typescript": TypeScriptZodGenerator,
        "kotlin": KotlinDataClassGenerator,
    }

    def run(self, args: list[str] | None = None) -> None:
        parser = argparse.ArgumentParser(
            prog="generate_bindings",
            description="Generate type-safe bindings from Ontology Schema",
        )
        parser.add_argument("--schema", required=True, help="Path to schema JSON")
        parser.add_argument("--language", required=True, choices=self.GENERATORS.keys())
        parser.add_argument("--output", required=True, help="Output directory")
        parser.add_argument("--namespace", default="onto", help="Package namespace")

        parsed = parser.parse_args(args)

        # 解析 Schema
        with open(parsed.schema) as f:
            schema_data = json.load(f)

        parser_inst = OntologySchemaParser()
        ir_schema = parser_inst.parse(schema_data)

        # 生成代码
        generator_cls = self.GENERATORS[parsed.language]
        generator = generator_cls()
        files = generator.generate(ir_schema)

        # 写入文件
        os.makedirs(parsed.output, exist_ok=True)
        for filename, content in files.items():
            filepath = os.path.join(parsed.output, filename)
            with open(filepath, "w") as f:
                f.write(content)
            print(f"Generated: {filepath}")


# 使用示例
# python -m onto.generate_bindings \
#   --schema ontology-schema.json \
#   --language python \
#   --output ./generated/

#6. 增量生成

Python
class IncrementalGenerator:
    """增量代码生成:只重新生成变更的类型"""

    def __init__(self, cache_dir: str = ".binding-cache"):
        self._cache_dir = cache_dir
        os.makedirs(cache_dir, exist_ok=True)

    def generate_incremental(self, new_schema: IRSchema,
                              generator: CodeGenerator) -> dict[str, str]:
        """增量生成:比较新旧 Schema,只生成差异部分"""
        old_schema = self._load_cache()
        if old_schema is None:
            # 首次生成
            files = generator.generate(new_schema)
            self._save_cache(new_schema)
            return files

        # 比较差异
        changed_types = self._diff_schemas(old_schema, new_schema)
        if not changed_types:
            return {}  # 无变更

        # 全量重新生成(确保引用一致性)
        files = generator.generate(new_schema)
        self._save_cache(new_schema)
        return files

    def _diff_schemas(self, old: IRSchema, new: IRSchema) -> list[str]:
        old_types = {ot.name: ot for ot in old.object_types}
        new_types = {ot.name: ot for ot in new.object_types}

        changed = []
        for name in set(old_types) | set(new_types):
            if name not in old_types or name not in new_types:
                changed.append(name)
            elif old_types[name] != new_types[name]:
                changed.append(name)

        return changed

    def _load_cache(self) -> IRSchema | None:
        cache_file = os.path.join(self._cache_dir, "schema.json")
        if os.path.exists(cache_file):
            with open(cache_file) as f:
                return OntologySchemaParser().parse(json.load(f))
        return None

    def _save_cache(self, schema: IRSchema) -> None:
        # 简化:保存原始数据
        pass

#7. CI/CD 集成

YAML
# .gitlab-ci.yml
generate-bindings:
  stage: build
  script:
    - python -m onto.generate_bindings
        --schema control-Layer/ontology-schema.json
        --language python
        --output python-sdk/ontology_sdk/generated/
    - python -m onto.generate_bindings
        --schema control-Layer/ontology-schema.json
        --language typescript
        --output sdk-Layer/ts-sdk/src/generated/
    - git diff --exit-code generated/  # 检查是否有变更未提交
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      changes:
        - control-Layer/ontology-schema.json

#8. 生成代码示例

#8.1 输入 Schema

JSON
{
  "namespace": "onto.demo",
  "version": "1.0.0",
  "objectTypes": [
    {
      "name": "Employee",
      "description": "Employee object type",
      "properties": [
        {"name": "id", "type": "string", "required": true},
        {"name": "name", "type": "string", "required": true},
        {"name": "department", "type": "string", "required": false},
        {"name": "salary", "type": "float", "required": true},
        {"name": "hire_date", "type": "datetime", "required": true},
        {"name": "skills", "type": "array", "elementType": "string"}
      ]
    }
  ],
  "actionTypes": [
    {
      "name": "PromoteEmployee",
      "objectType": "Employee",
      "parameters": [
        {"name": "employee_id", "type": "string", "required": true},
        {"name": "new_title", "type": "string", "required": true},
        {"name": "salary_increase", "type": "float", "required": true}
      ]
    }
  ]
}

#8.2 生成的 Python 代码

Python
"""Auto-generated Ontology bindings. DO NOT EDIT."""
from __future__ import annotations
from datetime import datetime
from typing import Any
from pydantic import BaseModel, Field

class Employee(BaseModel):
    """Employee object type"""
    id: str
    name: str
    department: str | None = None
    salary: float
    hire_date: datetime
    skills: list[str]

class PromoteEmployeeParams(BaseModel):
    """Promote employee action"""
    employee_id: str
    new_title: str
    salary_increase: float

#9. 实战案例

Python
# 在自定义函数中使用生成的绑定

from ontology_sdk.generated import Employee, PromoteEmployeeParams


def evaluate_promotion(employee: dict, performance_score: float) -> dict:
    """评估是否应该晋升"""
    # 使用生成的类型进行类型安全的操作
    emp = Employee(**employee)

    should_promote = (
        performance_score >= 4.0 and
        emp.salary < 500000 and
        len(emp.skills) >= 5
    )

    if should_promote:
        params = PromoteEmployeeParams(
            employee_id=emp.id,
            new_title="Senior Engineer",
            salary_increase=emp.salary * 0.15,
        )
        return {
            "decision": "promote",
            "confidence": min(performance_score / 5.0, 1.0),
            "action_params": params.model_dump(),
        }
    else:
        return {
            "decision": "maintain",
            "confidence": 0.8,
        }

#10. 扩展性

Python
class PluginRegistry:
    """生成器插件注册表"""

    _generators: dict[str, type[CodeGenerator]] = {}

    @classmethod
    def register(cls, language: str, generator_cls: type[CodeGenerator]) -> None:
        cls._generators[language] = generator_cls

    @classmethod
    def get(cls, language: str) -> CodeGenerator:
        gen_cls = cls._generators.get(language)
        if gen_cls is None:
            raise ValueError(f"No generator for language: {language}")
        return gen_cls()

    @classmethod
    def available_languages(cls) -> list[str]:
        return list(cls._generators.keys())


# 注册内置生成器
PluginRegistry.register("python", PythonPydanticGenerator)
PluginRegistry.register("typescript", TypeScriptZodGenerator)
PluginRegistry.register("kotlin", KotlinDataClassGenerator)

# 用户可以注册自定义生成器
# PluginRegistry.register("swift", SwiftCodableGenerator)

#Key Takeaways

  1. IR 中间表示 将 Ontology Schema 转换为语言无关的类型描述
  2. 多语言生成 Python/TypeScript/Kotlin/Rust 四种目标语言
  3. Pydantic 绑定 提供运行时类型验证和序列化
  4. Zod 绑定 提供 TypeScript 编译时 + 运行时双重类型安全
  5. CLI 工具 一行命令完成从 Schema 到代码的转换
  6. 增量生成 检测 Schema 差异,避免不必要的重新生成
  7. CI/CD 集成 Schema 变更自动触发绑定代码更新

#Next Article

下一篇 S5-22 K8s Job Executor:将自定义函数部署为 Kubernetes Job 将详解如何将资源密集型函数提交为 K8s Job 并行执行。

tags: #generate-bindings #code-generation #type-safety #pydantic #zod #ontology #coomia-dip