openapi-nexus 0.2.3

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

use crate::ir::types::{IrPrimitive, IrTypeExpr};
use heck::{ToLowerCamelCase, ToPascalCase};
#[allow(unused_imports)]
use sigil_stitch::lang::java::Java;
use sigil_stitch::prelude::*;

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

pub fn java_boxed_type_str(expr: &IrTypeExpr) -> String {
    match expr {
        IrTypeExpr::Primitive(p) => java_primitive_boxed(p).to_string(),
        _ => java_type_str(expr),
    }
}

pub fn java_primitive(p: &IrPrimitive) -> &'static str {
    match p {
        IrPrimitive::String | IrPrimitive::StringWithFormat(_) => "String",
        IrPrimitive::Date | IrPrimitive::DateTime => "String",
        IrPrimitive::Uuid => "String",
        IrPrimitive::Binary => "byte[]",
        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 java_primitive_boxed(p: &IrPrimitive) -> &'static str {
    match p {
        IrPrimitive::String | IrPrimitive::StringWithFormat(_) => "String",
        IrPrimitive::Date | IrPrimitive::DateTime => "String",
        IrPrimitive::Uuid => "String",
        IrPrimitive::Binary => "byte[]",
        IrPrimitive::Integer => "Integer",
        IrPrimitive::IntegerWithFormat(format) => match format.as_str() {
            "int64" => "Long",
            _ => "Integer",
        },
        IrPrimitive::Number => "Double",
        IrPrimitive::NumberWithFormat(format) => match format.as_str() {
            "float" => "Float",
            _ => "Double",
        },
        IrPrimitive::Boolean => "Boolean",
    }
}

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

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

pub fn java_getter_name(name: &str) -> String {
    let getter = format!("get{}", name.to_pascal_case());
    if getter == "getClass" {
        "getClass_".to_string()
    } else {
        getter
    }
}

pub fn is_java_reserved(name: &str) -> bool {
    matches!(
        name,
        "abstract"
            | "assert"
            | "boolean"
            | "break"
            | "byte"
            | "case"
            | "catch"
            | "char"
            | "class"
            | "const"
            | "continue"
            | "default"
            | "do"
            | "double"
            | "else"
            | "enum"
            | "extends"
            | "final"
            | "finally"
            | "float"
            | "for"
            | "goto"
            | "if"
            | "implements"
            | "import"
            | "instanceof"
            | "int"
            | "interface"
            | "long"
            | "native"
            | "new"
            | "package"
            | "private"
            | "protected"
            | "public"
            | "return"
            | "short"
            | "static"
            | "strictfp"
            | "super"
            | "switch"
            | "synchronized"
            | "this"
            | "throw"
            | "throws"
            | "transient"
            | "try"
            | "void"
            | "volatile"
            | "while"
    )
}

pub fn escape_java_string(s: &str) -> String {
    s.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!("String.valueOf({value_expr})")
        }
        IrTypeExpr::Nullable(inner) => render_value_as_string(value_expr, inner),
        IrTypeExpr::Array(inner) => {
            if matches!(
                inner.as_ref(),
                IrTypeExpr::Primitive(
                    IrPrimitive::String
                        | IrPrimitive::Date
                        | IrPrimitive::DateTime
                        | IrPrimitive::Uuid
                        | IrPrimitive::StringWithFormat(_)
                ) | IrTypeExpr::StringLiteral(_)
                    | IrTypeExpr::StringEnum(_)
            ) {
                format!("String.join(\",\", {value_expr})")
            } else {
                format!(
                    "{value_expr}.stream().map(Object::toString).collect(java.util.stream.Collectors.joining(\",\"))"
                )
            }
        }
        IrTypeExpr::Named(_) => format!("String.valueOf({value_expr})"),
        _ => format!("String.valueOf({value_expr})"),
    }
}

pub fn build_java_getter(getter_name: &str, type_str: &str, field_name: &str) -> FunSpec {
    let mut getter = FunSpec::builder(getter_name);
    getter = getter.visibility(Visibility::Public);
    getter = getter.returns(TypeName::primitive(type_str));
    let body = sigil_quote!(Java {
        return this.$L(field_name);
    })
    .expect("getter body");
    getter = getter.body(body);
    getter.build().expect("getter")
}

pub fn type_uses_list(expr: &IrTypeExpr) -> bool {
    match expr {
        IrTypeExpr::Array(_) => true,
        IrTypeExpr::Nullable(inner) => type_uses_list(inner),
        IrTypeExpr::Map(inner) => type_uses_list(inner),
        _ => false,
    }
}

pub fn type_uses_map(expr: &IrTypeExpr) -> bool {
    match expr {
        IrTypeExpr::Map(_) => true,
        IrTypeExpr::Nullable(inner) => type_uses_map(inner),
        IrTypeExpr::Array(inner) => type_uses_map(inner),
        _ => false,
    }
}