architect/_archive/2025-11-cleanup/platform-v2-cifra/archive/2025-11-10-restructure-v2/CODE_GENERATION_EXPLAINED.md

Code Generation: YAML → Jinja2 → Код (объяснение)

Дата: 2025-11-10
Версия: 1.0.0


КРАТКИЙ ОТВЕТ

1. Code Generation (YAML → Jinja2 → код)

Это цепочка генерации кода:

YAML описание       →    Jinja2 шаблон    →    Python код
(что нужно)              (как создать)         (готовый файл)

Пример за 30 секунд:

# user.yaml (ЧТО нужно)
entity: User
fields:
  - name: email
    type: string
  - name: age
    type: integer

обрабатывается через

{# template.py.j2 (КАК создать) #}
class {{ entity }}(Base):
    {% for field in fields %}
    {{ field.name }} = Column({{ field.type }})
    {% endfor %}

генерируется

# user.py (ГОТОВЫЙ код)
class User(Base):
    email = Column(String)
    age = Column(Integer)

2. Стандарты (OpenAPI, Prisma, GraphQL)

Это готовые форматы описания:

Каждый решает свою задачу, мы используем лучшее от каждого.


Часть 1: YAML → Jinja2 → Код (детально)

Шаг 1: YAML описание (Декларативное "ЧТО")

YAML - человекочитаемый формат данных.

# schemas/product.yaml

entity:
  name: Product
  table: products
  description: "Товар в магазине"

fields:
  id:
    type: integer
    primary_key: true
    auto_increment: true

  name:
    type: string
    max_length: 200
    required: true

  price:
    type: integer
    default: 0
    min_value: 0

  stock:
    type: integer
    default: 0

  created_at:
    type: datetime
    auto_now_add: true

indexes:
  - fields: [name]
  - fields: [created_at]

api:
  endpoints:
    - method: GET
      path: /products
    - method: POST
      path: /products
    - method: GET
      path: /products/{id}

Это декларативное описание:
- Вы говорите ЧТО нужно (entity Product с полями)
- НЕ говорите КАК это сделать (это работа генератора)


Шаг 2: Python парсит YAML

import yaml

# Загружаем YAML
with open('schemas/product.yaml') as f:
    config = yaml.safe_load(f)

print(config)
# {
#   'entity': {'name': 'Product', 'table': 'products', ...},
#   'fields': {
#     'id': {'type': 'integer', 'primary_key': True, ...},
#     'name': {'type': 'string', 'max_length': 200, ...},
#     ...
#   },
#   'api': {...}
# }

Теперь у нас Python словарь с описанием.


Шаг 3: Jinja2 шаблоны (Процедурное "КАК")

Jinja2 - шаблонизатор (как f-strings, но мощнее).

Шаблон SQLAlchemy модели:

{# templates/sqlalchemy_model.py.j2 #}

from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime

Base = declarative_base()

class {{ entity.name }}(Base):
    """{{ entity.description }}"""
    __tablename__ = "{{ entity.table }}"

    {% for field_name, field_config in fields.items() %}
    {{ field_name }} = Column(
        {% if field_config.type == 'integer' %}Integer{% endif %}
        {% if field_config.type == 'string' %}String({{ field_config.max_length | default(255) }}){% endif %}
        {% if field_config.type == 'datetime' %}DateTime{% endif %},
        {% if field_config.primary_key %}primary_key=True,{% endif %}
        {% if field_config.auto_increment %}autoincrement=True,{% endif %}
        {% if field_config.required %}nullable=False,{% endif %}
        {% if field_config.default is defined %}default={{ field_config.default }},{% endif %}
        {% if field_config.auto_now_add %}default=datetime.utcnow,{% endif %}
    )
    {% endfor %}

    def __repr__(self):
        return f"<{{ entity.name }}(id={self.id})>"

Jinja2 синтаксис:
- {{ variable }} - вывод значения
- {% for item in items %} - цикл
- {% if condition %} - условие
- {{ value | default(100) }} - фильтры


Шаг 4: Генератор объединяет YAML + Jinja2

from jinja2 import Environment, FileSystemLoader

# 1. Загружаем YAML
with open('schemas/product.yaml') as f:
    config = yaml.safe_load(f)

# 2. Настраиваем Jinja2
env = Environment(loader=FileSystemLoader('templates'))
template = env.get_template('sqlalchemy_model.py.j2')

# 3. Рендерим шаблон с данными из YAML
generated_code = template.render(
    entity=config['entity'],
    fields=config['fields']
)

# 4. Сохраняем результат
with open('generated/models/product.py', 'w') as f:
    f.write(generated_code)

print("✅ Модель сгенерирована!")

Шаг 5: Результат - готовый Python код

generated/models/product.py:

from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime

Base = declarative_base()

class Product(Base):
    """Товар в магазине"""
    __tablename__ = "products"

    id = Column(
        Integer,
        primary_key=True,
        autoincrement=True,
    )

    name = Column(
        String(200),
        nullable=False,
    )

    price = Column(
        Integer,
        default=0,
    )

    stock = Column(
        Integer,
        default=0,
    )

    created_at = Column(
        DateTime,
        default=datetime.utcnow,
    )

    def __repr__(self):
        return f"<Product(id={self.id})>"

Готово! Можно использовать:

from generated.models.product import Product

# Создание
product = Product(name="iPhone", price=50000, stock=10)
session.add(product)
session.commit()

# Запрос
products = session.query(Product).filter(Product.price > 10000).all()

Почему YAML → Jinja2?

Альтернативы:

  1. Прямая генерация (без шаблонов):
# Без Jinja2 - сложнее читать
code = f"class {name}(Base):\n"
for field in fields:
    code += f"    {field['name']} = Column({field['type']})\n"

Проблемы:
- ❌ Трудно читать
- ❌ Сложно изменять
- ❌ Перемешан код и логика

  1. С Jinja2 - чисто и понятно:
class {{ name }}(Base):
    {% for field in fields %}
    {{ field.name }} = Column({{ field.type }})
    {% endfor %}

Преимущества:
- ✅ Читается как обычный код
- ✅ Легко менять структуру
- ✅ Разделение логики и шаблона


Часть 2: Стандарты (OpenAPI, Prisma, GraphQL)

Зачем нужны стандарты?

Проблема: Каждый описывает API/модели по-своему.

Решение: Использовать общепринятые стандарты.


1. OpenAPI (стандарт REST API)

Что это: Формат описания REST API (JSON/YAML).

Кто использует: AWS, Google, Microsoft, Stripe, GitHub

Пример:

# openapi.yaml

openapi: 3.1.0
info:
  title: Product API
  version: 1.0.0

paths:
  /products:
    get:
      summary: Список продуктов
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 100
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Product'

    post:
      summary: Создать продукт
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductCreate'
      responses:
        '201':
          description: Created

components:
  schemas:
    Product:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        price:
          type: integer
      required:
        - id
        - name

    ProductCreate:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        price:
          type: integer
          minimum: 0
      required:
        - name

Что можно сделать с OpenAPI:

# 1. Генерация кода
openapi-generator generate -i openapi.yaml -g python-fastapi

# Создаётся:
# - FastAPI endpoints
# - Pydantic models
# - Tests

# 2. Автодокументация
# Swagger UI автоматически создаёт интерактивную документацию

Для нашей платформы:

Мы можем генерировать OpenAPI из CIDL:

# product.cidl
entity: Product
api:
  endpoints:
    - method: GET
      path: /products

↓ генератор

# openapi.yaml (автогенерируется)
openapi: 3.1.0
paths:
  /products:
    get: ...

2. Prisma Schema (ORM стандарт)

Что это: DSL (Domain Specific Language) для описания БД моделей.

Кто использует: Node.js/TypeScript проекты

Пример:

// schema.prisma

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  posts     Post[]

  @@index([email])
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id])

  @@index([authorId])
}

Что генерируется:

$ prisma generate

# Создаётся TypeScript клиент:
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()

// Type-safe запросы!
const user = await prisma.user.create({
  data: {
    email: 'test@example.com',
    name: 'John',
    posts: {
      create: [
        { title: 'Hello World', published: true }
      ]
    }
  },
  include: {
    posts: true  // ← IDE автодополнение!
  }
})

Синтаксис Prisma - САМЫЙ ЛАКОНИЧНЫЙ:

Сравните:

// Prisma (лаконично!)
model User {
  id    Int    @id @default(autoincrement())
  email String @unique
  posts Post[]
}

vs

# SQLAlchemy (многословно)
class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, autoincrement=True)
    email = Column(String, unique=True)
    posts = relationship("Post", back_populates="author")

Для нашей платформы:

Мы берём синтаксис Prisma для CIDL:

# Вдохновлено Prisma, но в YAML
entity:
  name: User

fields:
  id:
    type: integer
    primary_key: true
    auto_increment: true
  email:
    type: string
    unique: true

relationships:
  posts:
    type: one_to_many
    entity: Post

3. GraphQL Schema (API стандарт)

Что это: Язык описания типов данных + API.

Кто использует: Facebook, GitHub, Shopify, Airbnb

Пример:

# schema.graphql

type User {
  id: ID!
  email: String!
  name: String!
  posts: [Post!]!
  createdAt: DateTime!
}

type Post {
  id: ID!
  title: String!
  content: String
  published: Boolean!
  author: User!
}

type Query {
  users(limit: Int = 100): [User!]!
  user(id: ID!): User
  posts(authorId: ID): [Post!]!
}

type Mutation {
  createUser(email: String!, name: String!): User!
  updateUser(id: ID!, email: String, name: String): User!
  deleteUser(id: ID!): Boolean!
}

Что генерируется:

$ graphql-codegen

# Создаётся:
# - TypeScript типы
# - React hooks
# - Resolvers
// Автогенерированные типы
type User = {
  id: string
  email: string
  name: string
  posts: Post[]
  createdAt: Date
}

// Автогенерированные hooks
const { data, loading } = useUsersQuery({
  variables: { limit: 10 }
})

Для нашей платформы:

GraphQL схема может генерироваться из CIDL:

# product.cidl
entity: Product
fields:
  id: integer
  name: string

↓ генератор

# schema.graphql (автогенерируется)
type Product {
  id: Int!
  name: String!
}

Часть 3: Как это всё связано?

Центр системы: CIDL (наш формат)

# product.cidl - ЕДИНЫЙ источник правды

entity: Product
fields:
  id: {type: integer, primary_key: true}
  name: {type: string, max_length: 200}
  price: {type: integer, min_value: 0}

api:
  rest: true
  graphql: true

Из CIDL генерируем ВСЁ:

                    CIDL (product.yaml)
                           │
        ┌──────────────────┼──────────────────┐
        │                  │                  │
        ↓                  ↓                  ↓
  SQLAlchemy          Pydantic           FastAPI
  models.py           schemas.py         api.py
        │                  │                  │
        └──────────────────┼──────────────────┘
                           │
        ┌──────────────────┼──────────────────┐
        │                  │                  │
        ↓                  ↓                  ↓
   OpenAPI             GraphQL            Alembic
   spec.yaml           schema.graphql     migration.py

Генераторы для каждого формата:

# generator.py

class CIDLGenerator:
    def __init__(self, cidl_file):
        self.config = yaml.safe_load(open(cidl_file))

    def generate_sqlalchemy(self):
        """CIDL → SQLAlchemy"""
        template = jinja_env.get_template('sqlalchemy.py.j2')
        return template.render(self.config)

    def generate_pydantic(self):
        """CIDL → Pydantic"""
        template = jinja_env.get_template('pydantic.py.j2')
        return template.render(self.config)

    def generate_fastapi(self):
        """CIDL → FastAPI"""
        template = jinja_env.get_template('fastapi.py.j2')
        return template.render(self.config)

    def generate_openapi(self):
        """CIDL → OpenAPI"""
        # Преобразуем CIDL в формат OpenAPI
        openapi_spec = {
            'openapi': '3.1.0',
            'paths': self._convert_to_openapi_paths(),
            'components': self._convert_to_openapi_schemas()
        }
        return yaml.dump(openapi_spec)

    def generate_graphql(self):
        """CIDL → GraphQL"""
        template = jinja_env.get_template('graphql.j2')
        return template.render(self.config)

# Использование
generator = CIDLGenerator('product.cidl')
generator.generate_sqlalchemy()  # → models/product.py
generator.generate_pydantic()    # → schemas/product.py
generator.generate_fastapi()     # → api/products.py
generator.generate_openapi()     # → openapi.yaml
generator.generate_graphql()     # → schema.graphql

Часть 4: Реальный пример (end-to-end)

1. Описываем entity в CIDL:

# schemas/order.cidl

entity:
  name: Order
  table: orders
  description: "Заказ пользователя"

fields:
  id:
    type: integer
    primary_key: true
    auto_increment: true

  user_id:
    type: integer
    required: true

  total:
    type: decimal
    precision: 10
    scale: 2
    default: 0.00

  status:
    type: enum
    values: [pending, paid, shipped, delivered, cancelled]
    default: pending

  created_at:
    type: datetime
    auto_now_add: true

relationships:
  user:
    type: many_to_one
    entity: User
    foreign_key: user_id

  items:
    type: one_to_many
    entity: OrderItem
    foreign_key: order_id

api:
  rest:
    endpoints:
      - method: GET
        path: /orders
        auth: required
      - method: POST
        path: /orders
        auth: required
      - method: GET
        path: /orders/{id}
        auth: required

  graphql:
    enabled: true

2. Запускаем генератор:

$ cifra-codegen generate schemas/order.cidl --output generated/

Generating...
✅ generated/models/order.py (SQLAlchemy) generated/schemas/order.py (Pydantic) generated/api/orders.py (FastAPI) generated/migrations/001_create_orders.py (Alembic) generated/graphql/order.graphql (GraphQL schema) generated/openapi/order.yaml (OpenAPI spec) generated/tests/test_order.py (pytest)

Done in 1.2s!

3. Получаем готовые файлы:

generated/models/order.py:

from sqlalchemy import Column, Integer, String, Numeric, DateTime, Enum
from sqlalchemy.orm import relationship
from datetime import datetime

class Order(Base):
    """Заказ пользователя"""
    __tablename__ = "orders"

    id = Column(Integer, primary_key=True, autoincrement=True)
    user_id = Column(Integer, nullable=False)
    total = Column(Numeric(10, 2), default=0.00)
    status = Column(Enum('pending', 'paid', 'shipped', 'delivered', 'cancelled'), default='pending')
    created_at = Column(DateTime, default=datetime.utcnow)

    # Relationships
    user = relationship("User", back_populates="orders")
    items = relationship("OrderItem", back_populates="order")

generated/schemas/order.py:

from pydantic import BaseModel, Field
from decimal import Decimal
from datetime import datetime
from enum import Enum

class OrderStatus(str, Enum):
    pending = "pending"
    paid = "paid"
    shipped = "shipped"
    delivered = "delivered"
    cancelled = "cancelled"

class OrderCreate(BaseModel):
    user_id: int
    total: Decimal = Field(default=Decimal("0.00"), decimal_places=2)
    status: OrderStatus = OrderStatus.pending

class OrderResponse(BaseModel):
    id: int
    user_id: int
    total: Decimal
    status: OrderStatus
    created_at: datetime

    class Config:
        orm_mode = True

generated/api/orders.py:

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List

router = APIRouter(prefix="/orders", tags=["orders"])

@router.get("", response_model=List[OrderResponse])
async def list_orders(
    skip: int = 0,
    limit: int = 100,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user)
):
    """Список заказов"""
    orders = db.query(Order).offset(skip).limit(limit).all()
    return orders

@router.post("", response_model=OrderResponse, status_code=201)
async def create_order(
    data: OrderCreate,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user)
):
    """Создать заказ"""
    order = Order(**data.dict())
    db.add(order)
    db.commit()
    db.refresh(order)
    return order

@router.get("/{id}", response_model=OrderResponse)
async def get_order(
    id: int,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user)
):
    """Получить заказ"""
    order = db.query(Order).filter(Order.id == id).first()
    if not order:
        raise HTTPException(status_code=404, detail="Order not found")
    return order

generated/openapi/order.yaml:

openapi: 3.1.0
paths:
  /orders:
    get:
      summary: Список заказов
      security:
        - bearerAuth: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Order'
    post:
      summary: Создать заказ
      security:
        - bearerAuth: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderCreate'

generated/graphql/order.graphql:

type Order {
  id: Int!
  userId: Int!
  total: Float!
  status: OrderStatus!
  createdAt: DateTime!
  user: User!
  items: [OrderItem!]!
}

enum OrderStatus {
  PENDING
  PAID
  SHIPPED
  DELIVERED
  CANCELLED
}

type Query {
  orders(limit: Int = 100, offset: Int = 0): [Order!]!
  order(id: Int!): Order
}

type Mutation {
  createOrder(userId: Int!, total: Float, status: OrderStatus): Order!
}

4. Используем готовый код:

# main.py
from fastapi import FastAPI
from generated.api.orders import router as orders_router

app = FastAPI()
app.include_router(orders_router)

# Готово! API работает:
# GET  /orders
# POST /orders
# GET  /orders/1
$ uvicorn main:app

# Автодокументация доступна:
# http://localhost:8000/docs (Swagger UI)
# http://localhost:8000/redoc

ИТОГО

Code Generation (YAML → Jinja2 → код):

1. YAML - описание (ЧТО нужно)
   
2. Python - парсинг (читаем YAML)
   
3. Jinja2 - шаблоны (КАК создать)
   
4. Generator - рендеринг (объединяем)
   
5. Python код - результат (ГОТОВЫЙ файл)

Стандарты:

Всё вместе:

CIDL (наш формат)
    ↓
Генераторы (YAML → Jinja2)
    ↓
Всё сразу:
- SQLAlchemy models
- Pydantic schemas
- FastAPI endpoints
- OpenAPI spec
- GraphQL schema
- Alembic migrations
- Tests

Один файл CIDL → 7 готовых файлов!


Версия: 1.0.0
Статус: Объяснение завершено