openapi-nexus 0.1.3

OpenAPI 3.x multi-language code generator
Documentation
use std::collections::HashSet;

use crate::ir::types::{IrPrimitive, IrTypeExpr};
use heck::{ToLowerCamelCase, ToPascalCase};

pub fn kt_type_str(expr: &IrTypeExpr) -> String {
    match expr {
        IrTypeExpr::Named(name) => name.to_pascal_case(),
        IrTypeExpr::Primitive(p) => kt_primitive(p).to_string(),
        IrTypeExpr::Array(inner) => format!("List<{}>", kt_type_str(inner)),
        IrTypeExpr::Map(inner) => format!("Map<String, {}>", kt_type_str(inner)),
        IrTypeExpr::Nullable(inner) => format!("{}?", kt_type_str(inner)),
        IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => "String".to_string(),
        IrTypeExpr::Union(_) | IrTypeExpr::Any => "Any".to_string(),
    }
}

pub fn kt_primitive(p: &IrPrimitive) -> &'static str {
    match p {
        IrPrimitive::String | IrPrimitive::StringWithFormat(_) => "String",
        IrPrimitive::Date | IrPrimitive::DateTime => "String",
        IrPrimitive::Uuid => "String",
        IrPrimitive::Binary => "ByteArray",
        IrPrimitive::Integer => "Int",
        IrPrimitive::IntegerWithFormat(format) => match format.as_str() {
            "int64" => "Long",
            _ => "Int",
        },
        IrPrimitive::Number => "Double",
        IrPrimitive::NumberWithFormat(format) => match format.as_str() {
            "float" => "Float",
            _ => "Double",
        },
        IrPrimitive::Boolean => "Boolean",
    }
}

pub fn kt_field_name(name: &str) -> String {
    let camel = name.to_lower_camel_case();
    if camel.is_empty() {
        return "value".to_string();
    }
    if is_kotlin_reserved(&camel) {
        format!("`{camel}`")
    } else {
        camel
    }
}

pub fn kt_ident(name: &str) -> String {
    let camel = name.to_lower_camel_case();
    if camel.is_empty() {
        return "arg".to_string();
    }
    if is_kotlin_reserved(&camel) {
        format!("`{camel}`")
    } else {
        camel
    }
}

pub fn is_kotlin_reserved(name: &str) -> bool {
    matches!(
        name,
        "as" | "break"
            | "class"
            | "continue"
            | "do"
            | "else"
            | "false"
            | "for"
            | "fun"
            | "if"
            | "in"
            | "interface"
            | "is"
            | "null"
            | "object"
            | "package"
            | "return"
            | "super"
            | "this"
            | "throw"
            | "true"
            | "try"
            | "typealias"
            | "typeof"
            | "val"
            | "var"
            | "when"
            | "while"
    )
}

pub fn escape_kt_string(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('$', "\\$")
        .replace('\n', "\\n")
        .replace('\r', "\\r")
        .replace('\t', "\\t")
}

pub fn unique_name(desired: &str, used: &mut HashSet<String>) -> String {
    if used.insert(desired.to_string()) {
        return desired.to_string();
    }
    for i in 2..=u32::MAX {
        let candidate = format!("{desired}{i}");
        if used.insert(candidate.clone()) {
            return candidate;
        }
    }
    unreachable!()
}

pub fn sanitize_operation_id(op_id: &str, method: &str, path: &str) -> String {
    if !op_id.is_empty() {
        return op_id.to_string();
    }
    let path_part: String = path
        .chars()
        .map(|c| if c.is_alphanumeric() { c } else { '_' })
        .collect();
    format!("{method}_{path_part}")
}

pub fn render_value_as_string(value_expr: &str, t: &IrTypeExpr) -> String {
    match t {
        IrTypeExpr::Primitive(
            IrPrimitive::String
            | IrPrimitive::Date
            | IrPrimitive::DateTime
            | IrPrimitive::Uuid
            | IrPrimitive::StringWithFormat(_),
        )
        | IrTypeExpr::StringLiteral(_)
        | IrTypeExpr::StringEnum(_) => value_expr.to_string(),
        IrTypeExpr::Primitive(IrPrimitive::Boolean)
        | IrTypeExpr::Primitive(IrPrimitive::Integer)
        | IrTypeExpr::Primitive(IrPrimitive::IntegerWithFormat(_))
        | IrTypeExpr::Primitive(IrPrimitive::Number)
        | IrTypeExpr::Primitive(IrPrimitive::NumberWithFormat(_)) => {
            format!("{value_expr}.toString()")
        }
        IrTypeExpr::Nullable(inner) => {
            let inner_str = render_value_as_string(value_expr, inner);
            if inner_str == value_expr {
                format!("{value_expr} ?: \"\"")
            } else {
                inner_str
            }
        }
        IrTypeExpr::Array(_) => format!("{value_expr}.joinToString(\",\")"),
        IrTypeExpr::Named(_) => format!("{value_expr}.toString()"),
        _ => format!("{value_expr}.toString()"),
    }
}