use crate::codegen::naming::{PublicIdentifierKind, public_casing};
use crate::core::config::Language;
use heck::{ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
pub(crate) fn lang_display_name(lang: Language) -> &'static str {
match lang {
Language::Python => "Python",
Language::Node => "TypeScript",
Language::Ruby => "Ruby",
Language::Php => "PHP",
Language::Elixir => "Elixir",
Language::Go => "Go",
Language::Java => "Java",
Language::Csharp => "C#",
Language::Ffi | Language::C | Language::Jni => "C",
Language::Wasm => "WebAssembly",
Language::R => "R",
Language::Rust => "Rust",
Language::Kotlin => "Kotlin",
Language::KotlinAndroid => "Kotlin (Android)",
Language::Swift => "Swift",
Language::Dart => "Dart",
Language::Gleam => "Gleam",
Language::Zig => "Zig",
}
}
pub(crate) fn lang_slug(lang: Language) -> &'static str {
match lang {
Language::Python => "python",
Language::Node => "typescript",
Language::Ruby => "ruby",
Language::Php => "php",
Language::Elixir => "elixir",
Language::Go => "go",
Language::Java => "java",
Language::Csharp => "csharp",
Language::Ffi | Language::C | Language::Jni => "c",
Language::Wasm => "wasm",
Language::R => "r",
Language::Rust => "rust",
Language::Kotlin => "kotlin",
Language::KotlinAndroid => "kotlin-android",
Language::Swift => "swift",
Language::Dart => "dart",
Language::Gleam => "gleam",
Language::Zig => "zig",
}
}
pub(crate) fn lang_code_fence(lang: Language) -> &'static str {
match lang {
Language::Python => "python",
Language::Node | Language::Wasm => "typescript",
Language::Ruby => "ruby",
Language::Php => "php",
Language::Elixir => "elixir",
Language::Go => "go",
Language::Java => "java",
Language::Csharp => "csharp",
Language::Ffi | Language::C | Language::Jni => "c",
Language::R => "r",
Language::Rust => "rust",
Language::Kotlin | Language::KotlinAndroid => "kotlin",
Language::Swift => "swift",
Language::Dart => "dart",
Language::Gleam => "gleam",
Language::Zig => "zig",
}
}
pub(crate) fn type_name(name: &str, lang: Language, ffi_prefix: &str) -> String {
let short = name.rsplit("::").next().unwrap_or(name);
let cased = public_casing(lang, PublicIdentifierKind::Type, short);
match lang {
Language::Ffi | Language::C | Language::Jni => format!("{}{}", ffi_prefix.to_shouty_snake_case(), cased),
_ => cased,
}
}
pub(crate) fn func_name(name: &str, lang: Language, ffi_prefix: &str) -> String {
let base = match lang {
Language::Ffi | Language::C | Language::Jni => {
crate::codegen::c_consumer::free_function_symbol(&ffi_prefix.to_snake_case(), name)
}
_ => public_casing(lang, PublicIdentifierKind::Function, name),
};
match (lang, base.as_str()) {
(Language::Java, "default") => "defaultInstance".to_string(),
(Language::Java, "new") => "create".to_string(),
(Language::Java, other) if crate::core::keywords::JAVA_KEYWORDS.contains(&other) => format!("{other}_"),
(Language::Csharp, "Default") => "CreateDefault".to_string(),
(Language::Dart, "new") => "create".to_string(),
(lang, other)
if !matches!(lang, Language::Java | Language::Node | Language::Wasm | Language::Php)
&& crate::docs::formatting::identifier_violation(
other,
lang,
crate::docs::formatting::IdentifierPosition::Declaration,
) == Some("reserved word") =>
{
format!("{other}_")
}
_ => base,
}
}
pub(crate) fn method_name(owner_type: &str, name: &str, lang: Language, ffi_prefix: &str) -> String {
match lang {
Language::Ffi | Language::C | Language::Jni => {
crate::codegen::c_consumer::method_symbol(&ffi_prefix.to_snake_case(), owner_type, name)
}
_ => func_name(name, lang, ffi_prefix),
}
}
pub(crate) fn csharp_async_member_name(name: &str, is_async: bool) -> String {
if is_async && !name.ends_with("Async") {
format!("{name}Async")
} else {
name.to_string()
}
}
pub(crate) fn field_name(name: &str, lang: Language) -> String {
public_casing(lang, PublicIdentifierKind::Field, name)
}
pub(crate) fn enum_variant_name(name: &str, lang: Language, ffi_prefix: &str) -> String {
let cased = public_casing(lang, PublicIdentifierKind::EnumVariant, name);
match lang {
Language::Ffi | Language::C | Language::Jni => format!("{}_{}", ffi_prefix.to_shouty_snake_case(), cased),
_ => cased,
}
}
pub(crate) fn to_camel_case(s: &str) -> String {
let pascal = s.to_upper_camel_case();
let mut chars = pascal.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_lowercase().to_string() + chars.as_str(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::Language;
use crate::docs::test_helpers::TEST_PREFIX;
#[test]
fn test_enum_variant_name_python() {
assert_eq!(enum_variant_name("Atx", Language::Python, TEST_PREFIX), "ATX");
assert_eq!(
enum_variant_name("SnakeCase", Language::Python, TEST_PREFIX),
"SNAKE_CASE"
);
}
#[test]
fn test_enum_variant_name_java() {
assert_eq!(enum_variant_name("Atx", Language::Java, TEST_PREFIX), "ATX");
}
#[test]
fn test_enum_variant_name_ffi() {
assert_eq!(enum_variant_name("Atx", Language::Ffi, TEST_PREFIX), "HTM_ATX");
}
#[test]
fn test_type_name_ffi_uses_prefix() {
assert_eq!(
type_name("ParseOptions", Language::Ffi, "SampleCrate"),
"SAMPLE_CRATEParseOptions"
);
assert_eq!(
type_name("ParseOutput", Language::Ffi, "SampleCrate"),
"SAMPLE_CRATEParseOutput"
);
}
#[test]
fn type_name_ffi_matches_cbindgen_export_prefix() {
for (ffi_prefix, expected_export_prefix) in [
("demoapi", "DEMOAPI"),
("Demoapi", "DEMOAPI"),
("demo_api", "DEMO_API"),
("SampleCore", "SAMPLE_CORE"),
] {
assert_eq!(
type_name("DefaultClient", Language::C, ffi_prefix),
format!("{expected_export_prefix}DefaultClient"),
"docs must use cbindgen's export prefix for `{ffi_prefix}`"
);
assert_eq!(
type_name("sample_crate::DefaultClient", Language::Ffi, ffi_prefix),
format!("{expected_export_prefix}DefaultClient"),
"a fully qualified rust path must resolve to the same header symbol"
);
}
}
#[test]
fn test_func_name_ffi_uses_prefix() {
assert_eq!(
func_name("convert", Language::Ffi, "SampleCrate"),
"sample_crate_convert"
);
}
#[test]
fn test_method_name_ffi_folds_in_owning_type() {
assert_eq!(
method_name("Converter", "convert", Language::Ffi, TEST_PREFIX),
"htm_converter_convert"
);
assert_ne!(
method_name("Converter", "convert", Language::Ffi, TEST_PREFIX),
func_name("convert", Language::Ffi, TEST_PREFIX),
"a method symbol must not collide with the free-function symbol of the same name"
);
}
#[test]
fn test_method_name_non_c_delegates_to_func_name() {
assert_eq!(method_name("Type", "new", Language::Java, TEST_PREFIX), "create");
assert_eq!(
method_name("Type", "new", Language::Java, TEST_PREFIX),
func_name("new", Language::Java, TEST_PREFIX)
);
for lang in [
Language::Python,
Language::Node,
Language::Go,
Language::Csharp,
Language::Ruby,
Language::Zig,
] {
assert_eq!(
method_name("Document", "parse_document", lang, TEST_PREFIX),
func_name("parse_document", lang, TEST_PREFIX),
"{lang} must delegate to func_name unchanged"
);
}
}
#[test]
fn test_csharp_async_member_name_appends_async_suffix() {
assert_eq!(csharp_async_member_name("ParseDocument", true), "ParseDocumentAsync");
assert_eq!(csharp_async_member_name("ParseDocument", false), "ParseDocument");
assert_eq!(csharp_async_member_name("ExtractAsync", true), "ExtractAsync");
}
#[test]
fn test_enum_variant_name_ffi_uses_prefix() {
assert_eq!(
enum_variant_name("Atx", Language::Ffi, "SampleCrate"),
"SAMPLE_CRATE_ATX"
);
}
#[test]
fn test_field_name_go_pascal_case() {
assert_eq!(field_name("heading_style", Language::Go), "HeadingStyle");
assert_eq!(field_name("list_indent_type", Language::Go), "ListIndentType");
}
#[test]
fn test_func_name_conventions() {
assert_eq!(func_name("convert", Language::Python, TEST_PREFIX), "convert");
assert_eq!(
func_name("parse_document", Language::Node, TEST_PREFIX),
"parseDocument"
);
assert_eq!(func_name("parse_document", Language::Go, TEST_PREFIX), "ParseDocument");
assert_eq!(func_name("convert", Language::Ffi, TEST_PREFIX), "htm_convert");
}
#[test]
fn test_type_name_ffi_prefix() {
assert_eq!(type_name("ParseOptions", Language::Ffi, TEST_PREFIX), "HTMParseOptions");
assert_eq!(type_name("ParseOutput", Language::Ffi, TEST_PREFIX), "HTMParseOutput");
}
#[test]
fn test_lang_slug_kotlin_vs_kotlin_android() {
assert_eq!(lang_slug(Language::Kotlin), "kotlin");
assert_eq!(lang_slug(Language::KotlinAndroid), "kotlin-android");
}
#[test]
fn test_lang_display_name_kotlin_vs_kotlin_android() {
assert_eq!(lang_display_name(Language::Kotlin), "Kotlin");
assert_eq!(lang_display_name(Language::KotlinAndroid), "Kotlin (Android)");
}
#[test]
fn test_lang_code_fence_kotlin_android_uses_kotlin() {
assert_eq!(lang_code_fence(Language::Kotlin), "kotlin");
assert_eq!(lang_code_fence(Language::KotlinAndroid), "kotlin");
}
#[test]
fn test_func_name_zig_uses_snake_case() {
assert_eq!(func_name("create_engine", Language::Zig, TEST_PREFIX), "create_engine");
assert_eq!(func_name("map_urls", Language::Zig, TEST_PREFIX), "map_urls");
assert_eq!(func_name("batch_scrape", Language::Zig, TEST_PREFIX), "batch_scrape");
}
#[test]
fn test_field_name_zig_uses_snake_case() {
assert_eq!(field_name("max_depth", Language::Zig), "max_depth");
assert_eq!(field_name("user_agent", Language::Zig), "user_agent");
}
#[test]
fn test_enum_variant_name_zig_uses_snake_case() {
assert_eq!(enum_variant_name("Auto", Language::Zig, TEST_PREFIX), "auto");
assert_eq!(enum_variant_name("Stealth", Language::Zig, TEST_PREFIX), "stealth");
assert_eq!(
enum_variant_name("NetworkIdle", Language::Zig, TEST_PREFIX),
"network_idle"
);
}
#[test]
fn test_enum_variant_name_zig_rdfa_uses_acronym_aware_snake_case() {
assert_eq!(enum_variant_name("RDFa", Language::Zig, TEST_PREFIX), "rdfa");
}
#[test]
fn test_type_name_zig_preserves_pascal_case() {
assert_eq!(type_name("BrowserMode", Language::Zig, TEST_PREFIX), "BrowserMode");
assert_eq!(type_name("CrawlConfig", Language::Zig, TEST_PREFIX), "CrawlConfig");
}
#[test]
fn test_func_name_java_renames_reserved_new_to_create() {
assert_eq!(func_name("new", Language::Java, TEST_PREFIX), "create");
}
#[test]
fn test_func_name_java_renames_default_to_default_instance() {
assert_eq!(func_name("default", Language::Java, TEST_PREFIX), "defaultInstance");
}
#[test]
fn test_func_name_java_suffixes_other_reserved_words() {
assert_eq!(func_name("class", Language::Java, TEST_PREFIX), "class_");
assert_eq!(func_name("static", Language::Java, TEST_PREFIX), "static_");
}
#[test]
fn test_func_name_java_leaves_ordinary_names_alone() {
assert_eq!(
func_name("parse_document", Language::Java, TEST_PREFIX),
"parseDocument"
);
assert_eq!(func_name("create", Language::Java, TEST_PREFIX), "create");
}
#[test]
fn test_func_name_java_matches_backend_safe_java_method_name() {
let ordinary = ["parse_document", "to_json", "create", "with_options", "from_str", "id"];
for name in crate::core::keywords::JAVA_KEYWORDS.iter().copied().chain(ordinary) {
assert_eq!(
func_name(name, Language::Java, TEST_PREFIX),
crate::backends::java::gen_bindings::helpers::safe_java_method_name(name),
"docs must name `{name}` exactly as the Java backend does"
);
}
}
#[test]
fn test_func_name_java_output_passes_the_identifier_gate() {
use crate::docs::formatting::{IdentifierPosition, check_identifier};
let ordinary = ["parse_document", "to_json", "create"];
for name in crate::core::keywords::JAVA_KEYWORDS.iter().copied().chain(ordinary) {
let rendered = func_name(name, Language::Java, TEST_PREFIX);
assert_eq!(
check_identifier(&rendered, Language::Java, IdentifierPosition::Member, "a naming test"),
Ok(()),
"`{name}` renders as `{rendered}`, which Java rejects as a member name"
);
}
}
#[test]
fn test_enum_variant_name_java_uses_screaming_snake_case_for_multi_word_names() {
assert_eq!(
enum_variant_name("SnakeCase", Language::Java, TEST_PREFIX),
"SNAKE_CASE"
);
}
#[test]
fn test_go_field_type_and_func_names_apply_initialisms() {
assert_eq!(field_name("base_url", Language::Go), "BaseURL");
assert_eq!(type_name("base_url", Language::Go, TEST_PREFIX), "BaseURL");
assert_eq!(func_name("base_url", Language::Go, TEST_PREFIX), "BaseURL");
}
#[test]
fn test_csharp_type_name_preserves_graphql_initialism_but_not_url() {
assert_eq!(
type_name("GraphQLRouteConfig", Language::Csharp, TEST_PREFIX),
"GraphQLRouteConfig"
);
assert_eq!(field_name("base_url", Language::Csharp), "BaseUrl");
}
#[test]
fn test_gleam_field_and_func_names_use_snake_case() {
assert_eq!(field_name("base_url", Language::Gleam), "base_url");
assert_eq!(func_name("base_url", Language::Gleam, TEST_PREFIX), "base_url");
}
#[test]
fn test_gleam_enum_variant_name_uses_pascal_case() {
assert_eq!(
enum_variant_name("SnakeCase", Language::Gleam, TEST_PREFIX),
"SnakeCase"
);
}
#[test]
fn test_field_type_and_enum_variant_casing_agrees_with_public_host_identifier() {
use crate::codegen::naming::public_host_identifier;
for lang in Language::ALL {
if matches!(lang, Language::Ffi | Language::C | Language::Jni) {
continue;
}
assert_eq!(
field_name("widget_count", lang),
public_host_identifier(lang, PublicIdentifierKind::Field, "widget_count"),
"{lang:?} field casing must agree with the shared authority"
);
assert_eq!(
type_name("WidgetSettings", lang, TEST_PREFIX),
public_host_identifier(lang, PublicIdentifierKind::Type, "WidgetSettings"),
"{lang:?} type casing must agree with the shared authority"
);
assert_eq!(
enum_variant_name("WidgetVariant", lang, TEST_PREFIX),
public_host_identifier(lang, PublicIdentifierKind::EnumVariant, "WidgetVariant"),
"{lang:?} enum variant casing must agree with the shared authority"
);
}
}
}