openapi-nexus 0.2.3

OpenAPI 3.x multi-language code generator
Documentation
//! Project-level files: pyproject.toml, README, __init__.py barrels, py.typed.

use crate::codegen::traits::file_writer::FileInfo;
use crate::generators::request_inputs::RequestInputModel;
use crate::ir::types::{IrInfo, IrOperation, IrSchema};
use heck::{ToPascalCase, ToSnakeCase};
use indexmap::IndexMap;

/// Generate all project-level files.
pub fn generate_project_files(
    info: &IrInfo,
    package_name: &str,
    header: &str,
    schemas: &IndexMap<String, IrSchema>,
    operations: &[IrOperation],
    request_inputs: &[RequestInputModel],
    include_upload_file: bool,
) -> Vec<FileInfo> {
    let files = vec![
        pyproject_toml(info, package_name),
        readme_file(info, package_name),
        py_typed(package_name),
        top_level_init(package_name, header, include_upload_file),
        models_init(schemas, request_inputs, header),
        apis_init(operations, header),
    ];

    files
}

fn pyproject_toml(info: &IrInfo, package_name: &str) -> FileInfo {
    let description = info
        .description
        .as_deref()
        .unwrap_or("Generated Python SDK.")
        .lines()
        .next()
        .unwrap_or("Generated Python SDK.");
    let content = format!(
        r#"[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "{package_name}"
version = "{version}"
description = "{description}"
requires-python = ">=3.12"
dependencies = ["httpx>=0.27"]
"#,
        version = info.version,
    );
    FileInfo::project("pyproject.toml".to_string(), content)
}

fn readme_file(info: &IrInfo, package_name: &str) -> FileInfo {
    let title = &info.title;
    let version = &info.version;
    let description = info
        .description
        .clone()
        .unwrap_or_else(|| "Generated Python SDK.".to_string());
    let content = format!(
        "# {title}\n\n{description}\n\nVersion: `{version}`\n\nGenerated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `{package_name}`.\n"
    );
    FileInfo::readme("README.md".to_string(), content)
}

fn py_typed(package_name: &str) -> FileInfo {
    FileInfo::project(format!("{package_name}/py.typed"), String::new())
}

fn top_level_init(package_name: &str, header: &str, include_upload_file: bool) -> FileInfo {
    let mut content = String::new();
    content.push_str(header);
    content.push_str("from .runtime import ApiKeyAuth as ApiKeyAuth\n");
    content.push_str("from .runtime import Authenticator as Authenticator\n");
    content.push_str("from .runtime import BearerAuth as BearerAuth\n");
    content.push_str("from .runtime import ApiResponse as ApiResponse\n");
    content.push_str("from .runtime import Client as Client\n");
    content.push_str("from .runtime import ApiError as ApiError\n");
    if include_upload_file {
        content.push_str("from .runtime import UploadFile as UploadFile\n");
    }
    FileInfo::project(format!("{package_name}/__init__.py"), content)
}

fn models_init(
    schemas: &IndexMap<String, IrSchema>,
    request_inputs: &[RequestInputModel],
    header: &str,
) -> FileInfo {
    let mut content = String::new();
    content.push_str(header);

    let mut entries: Vec<(String, String)> = Vec::new();
    for (_key, schema) in schemas {
        let py_name = schema.name.to_pascal_case();
        let module = schema.name.to_snake_case();
        entries.push((module, py_name));
    }
    for model in request_inputs {
        let py_name = model.name.to_pascal_case();
        let module = model.name.to_snake_case();
        entries.push((module, py_name));
    }

    entries.sort_by(|a, b| a.0.cmp(&b.0));
    for (module, name) in &entries {
        content.push_str(&format!("from .{module} import {name} as {name}\n"));
    }

    FileInfo::model("__init__.py".to_string(), content)
}

fn apis_init(operations: &[IrOperation], header: &str) -> FileInfo {
    let mut content = String::new();
    content.push_str(header);

    let mut tags: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
    for op in operations {
        if op.tags.is_empty() {
            tags.insert("default".to_string());
        } else {
            for tag in &op.tags {
                tags.insert(tag.clone());
            }
        }
    }

    for tag in &tags {
        let module = tag.to_snake_case();
        let class_name = format!("{}Api", tag.to_pascal_case());
        content.push_str(&format!(
            "from .{module}_api import {class_name} as {class_name}\n"
        ));
    }

    FileInfo::api("__init__.py".to_string(), content)
}