#codegen #macro #struct-macro #json #struct

build unistructgen-codegen

Code generation backend for unistructgen - Rust code renderer

1 unstable release

new 0.1.1 Feb 20, 2026

#314 in Build Utils


Used in unistructgen-openapi-pars…

MIT/Apache

215KB
3.5K SLoC

🦀 UniStructGen Codegen

Генератор идиоматичного Rust-кода из Intermediate Representation

Crate Docs License


📋 Содержание


🎯 Обзор

unistructgen-codegen — это модуль генерации Rust-кода из IR (Intermediate Representation). Он обеспечивает:

  • Идиоматичный Rust — правильное форматирование, стиль и соглашения
  • Derive macros — автоматическое добавление Debug, Clone, PartialEq, serde
  • Документация — сохранение doc-комментариев из IR
  • Атрибуты — поддержка #[serde(...)], #[validate(...)], etc.
  • Type safety — корректное маппирование IR типов в Rust типы
  • Вложенные типы — генерация всех связанных структур

Поддерживаемые возможности

Возможность Описание
Structs Публичные структуры с полями
Enums Перечисления с вариантами
Doc comments /// Документация
Derives #[derive(...)]
Serde attributes #[serde(rename = "...")]
Validation #[validate(...)]
Nested types Автоматическое разрешение зависимостей
Option/Vec/Map Option<T>, Vec<T>, HashMap<K, V>

📦 Установка

[dependencies]
unistructgen-codegen = "0.1"
unistructgen-core = "0.1"

🚀 Быстрый старт

use unistructgen_codegen::{RustRenderer, RenderOptions};
use unistructgen_core::{
    CodeGenerator, IRModule, IRStruct, IRType, IRField,
    IRTypeRef, PrimitiveKind
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Создаём IR модуль
    let mut module = IRModule::new("users".to_string());

    // Создаём структуру
    let mut user = IRStruct::new("User".to_string());
    user.doc = Some("Represents a user in the system".to_string());
    user.add_derive("Debug".to_string());
    user.add_derive("Clone".to_string());
    user.add_derive("serde::Serialize".to_string());
    user.add_derive("serde::Deserialize".to_string());

    user.add_field(IRField::new(
        "id".to_string(),
        IRTypeRef::Primitive(PrimitiveKind::I64)
    ));

    user.add_field(IRField::new(
        "email".to_string(),
        IRTypeRef::Primitive(PrimitiveKind::String)
    ));

    let mut age = IRField::new(
        "age".to_string(),
        IRTypeRef::Option(Box::new(IRTypeRef::Primitive(PrimitiveKind::I32)))
    );
    age.doc = Some("User's age (optional)".to_string());
    user.add_field(age);

    module.add_type(IRType::Struct(user));

    // Генерируем код
    let renderer = RustRenderer::new(RenderOptions::default());
    let code = renderer.generate(&module)?;

    println!("{}", code);

    Ok(())
}

Результат:

// Generated by unistructgen v0.1.0
// Do not edit this file manually

#![allow(dead_code)]
#![allow(unused_imports)]

/// Represents a user in the system
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct User {
    pub id: i64,
    pub email: String,
    /// User's age (optional)
    pub age: Option<i32>,
}

🎨 RustRenderer

Основной класс для генерации Rust-кода.

Создание

use unistructgen_codegen::{RustRenderer, RenderOptions};

// С дефолтными опциями
let renderer = RustRenderer::new(RenderOptions::default());

// С кастомными опциями
let renderer = RustRenderer::new(RenderOptions {
    add_header: false,        // Без заголовка "Generated by..."
    add_clippy_allows: false, // Без #![allow(...)]
});

Методы

use unistructgen_core::CodeGenerator;

// Основной метод генерации (через трейт CodeGenerator)
let code = renderer.generate(&module)?;

// Альтернативный метод (прямой вызов)
let code = renderer.render(&module)?;

// Метаданные генератора
let metadata = renderer.metadata();
println!("Language: {}", renderer.language());    // "Rust"
println!("Extension: {}", renderer.file_extension()); // "rs"

// Валидация IR перед генерацией
renderer.validate(&module)?;

// Форматирование (placeholder для rustfmt)
let formatted = renderer.format(code)?;

⚙️ RenderOptions

Конфигурация генерации кода.

Опции

Опция Тип По умолчанию Описание
add_header bool true Добавлять заголовок с версией
add_clippy_allows bool true Добавлять #![allow(...)]

Примеры

use unistructgen_codegen::RenderOptions;

// Для production: с заголовком и clippy allows
let prod_options = RenderOptions::default();

// Для тестов/макросов: чистый код
let test_options = RenderOptions {
    add_header: false,
    add_clippy_allows: false,
};

// Только заголовок, без clippy
let custom_options = RenderOptions {
    add_header: true,
    add_clippy_allows: false,
};

🏗️ RustRendererBuilder

Fluent-билдер для создания рендерера.

use unistructgen_codegen::RustRendererBuilder;

let renderer = RustRendererBuilder::new()
    .with_header(true)
    .with_clippy_allows(true)
    .build();

🔤 Обработка типов

Примитивные типы

PrimitiveKind Rust Type
String String
I8 i8
I16 i16
I32 i32
I64 i64
I128 i128
U8 u8
U16 u16
U32 u32
U64 u64
U128 u128
F32 f32
F64 f64
Bool bool
Char char
DateTime chrono::DateTime<chrono::Utc>
Uuid uuid::Uuid
Decimal rust_decimal::Decimal
Json serde_json::Value

Составные типы

// Option<T>
IRTypeRef::Option(Box::new(IRTypeRef::Primitive(PrimitiveKind::String)))
// → Option<String>

// Vec<T>
IRTypeRef::Vec(Box::new(IRTypeRef::Primitive(PrimitiveKind::I32)))
// → Vec<i32>

// HashMap<K, V>
IRTypeRef::Map(
    Box::new(IRTypeRef::Primitive(PrimitiveKind::String)),
    Box::new(IRTypeRef::Primitive(PrimitiveKind::I64))
)
// → std::collections::HashMap<String, i64>

// Ссылка на тип
IRTypeRef::Named("Address".to_string())
// → Address

Вложенные типы

// Option<Vec<String>>
IRTypeRef::Option(Box::new(
    IRTypeRef::Vec(Box::new(
        IRTypeRef::Primitive(PrimitiveKind::String)
    ))
))
// → Option<Vec<String>>

// Vec<HashMap<String, User>>
IRTypeRef::Vec(Box::new(
    IRTypeRef::Map(
        Box::new(IRTypeRef::Primitive(PrimitiveKind::String)),
        Box::new(IRTypeRef::Named("User".to_string()))
    )
))
// → Vec<std::collections::HashMap<String, User>>

✅ Валидация

Генератор валидирует атрибуты из FieldConstraints и создаёт #[validate(...)].

Поддерживаемые ограничения

use unistructgen_core::FieldConstraints;

let constraints = FieldConstraints {
    // Длина строки/массива
    min_length: Some(3),
    max_length: Some(100),
    // → #[validate(length(min = 3, max = 100))]

    // Диапазон чисел
    min_value: Some(0.0),
    max_value: Some(100.0),
    // → #[validate(range(min = 0, max = 100))]

    // Regex паттерн
    pattern: Some(r"^\w+$".to_string()),
    // → #[validate(regex = "^\w+$")]

    // Формат
    format: Some("email".to_string()),
    // → #[validate(email)]
};

Пример генерации

let mut field = IRField::new("email".to_string(), IRTypeRef::Primitive(PrimitiveKind::String));
field.constraints = FieldConstraints {
    min_length: Some(5),
    max_length: Some(255),
    format: Some("email".to_string()),
    ..Default::default()
};

// Результат:
// #[validate(length(min = 5, max = 255), email)]
// pub email: String,

❌ Обработка ошибок

CodegenError

use unistructgen_codegen::CodegenError;

pub enum CodegenError {
    /// Ошибка рендеринга
    RenderError {
        component: String,  // "struct", "field", "enum"
        context: String,    // Имя типа
        message: String,
    },

    /// Ошибка форматирования
    FormatError {
        context: String,
        source: std::fmt::Error,
    },

    /// Ошибка валидации
    ValidationError {
        reason: String,
        suggestion: Option<String>,
    },

    /// Невалидный идентификатор
    InvalidIdentifier {
        name: String,
        context: String,  // "struct name", "field name"
        reason: String,
    },

    /// Неподдерживаемый тип
    UnsupportedType {
        type_name: String,
        context: String,
        reason: String,
        alternative: Option<String>,
    },

    /// Превышена максимальная глубина вложенности
    MaxDepthExceeded {
        context: String,
        max_depth: usize,
    },
}

Обработка ошибок

use unistructgen_codegen::{RustRenderer, RenderOptions, CodegenError};

let renderer = RustRenderer::new(RenderOptions::default());

match renderer.generate(&module) {
    Ok(code) => println!("{}", code),
    Err(CodegenError::ValidationError { reason, suggestion }) => {
        eprintln!("Validation failed: {}", reason);
        if let Some(sug) = suggestion {
            eprintln!("Suggestion: {}", sug);
        }
    },
    Err(CodegenError::InvalidIdentifier { name, context, reason }) => {
        eprintln!("Invalid {} '{}': {}", context, name, reason);
    },
    Err(e) => eprintln!("Error: {}", e),
}

Добавление suggestions

use unistructgen_codegen::CodegenError;

let error = CodegenError::validation_error("Module is empty")
    .with_suggestion("Add at least one struct or enum to the module");

📝 Примеры

Генерация структуры с serde

use unistructgen_codegen::{RustRenderer, RenderOptions};
use unistructgen_core::*;

let mut user = IRStruct::new("User".to_string());
user.add_derive("serde::Serialize".to_string());
user.add_derive("serde::Deserialize".to_string());

let mut id_field = IRField::new("id".to_string(), IRTypeRef::Primitive(PrimitiveKind::I64));

let mut name_field = IRField::new("user_name".to_string(), IRTypeRef::Primitive(PrimitiveKind::String));
name_field.source_name = Some("userName".to_string());
name_field.attributes.push("serde(rename = \"userName\")".to_string());

user.add_field(id_field);
user.add_field(name_field);

let mut module = IRModule::new("users".to_string());
module.add_type(IRType::Struct(user));

let renderer = RustRenderer::new(RenderOptions::default());
let code = renderer.generate(&module)?;

Результат:

#[derive(serde::Serialize, serde::Deserialize)]
pub struct User {
    pub id: i64,
    #[serde(rename = "userName")]
    pub user_name: String,
}

Генерация enum

use unistructgen_core::*;

let status = IREnum {
    name: "OrderStatus".to_string(),
    variants: vec![
        IREnumVariant {
            name: "Pending".to_string(),
            source_value: None,
            doc: Some("Order is pending".to_string()),
        },
        IREnumVariant {
            name: "InProgress".to_string(),
            source_value: Some("in_progress".to_string()),
            doc: None,
        },
        IREnumVariant {
            name: "Completed".to_string(),
            source_value: None,
            doc: None,
        },
    ],
    derives: vec!["Debug".to_string(), "Clone".to_string(), "serde::Serialize".to_string()],
    doc: Some("Order status enum".to_string()),
};

let mut module = IRModule::new("orders".to_string());
module.add_type(IRType::Enum(status));

let renderer = RustRenderer::new(RenderOptions::default());
let code = renderer.generate(&module)?;

Результат:

/// Order status enum
#[derive(Debug, Clone, serde::Serialize)]
pub enum OrderStatus {
    /// Order is pending
    Pending,
    #[serde(rename = "in_progress")]
    InProgress,
    Completed,
}

Генерация с валидацией

let mut email_field = IRField::new(
    "email".to_string(),
    IRTypeRef::Primitive(PrimitiveKind::String)
);
email_field.doc = Some("User's email address".to_string());
email_field.constraints = FieldConstraints {
    min_length: Some(5),
    max_length: Some(255),
    format: Some("email".to_string()),
    pattern: Some(r"^[\w@.]+$".to_string()),
    ..Default::default()
};

let mut age_field = IRField::new(
    "age".to_string(),
    IRTypeRef::Primitive(PrimitiveKind::I32)
);
age_field.constraints = FieldConstraints {
    min_value: Some(0.0),
    max_value: Some(150.0),
    ..Default::default()
};

Результат:

/// User's email address
#[validate(length(min = 5, max = 255), regex = "^[\w@.]+$", email)]
pub email: String,

#[validate(range(min = 0, max = 150))]
pub age: i32,

🔗 Связанные модули


🗺 Roadmap

  • Интеграция с rustfmt для форматирования
  • Генерация impl блоков
  • Генерация From/Into трейтов
  • Генерация Builder паттерна
  • Поддержка #[cfg(...)] атрибутов

📜 Лицензия

MIT или Apache-2.0 — на ваш выбор.

Dependencies

~1–2.2MB
~45K SLoC