use std::{
fmt::{Display, Write},
ops::Deref,
};
use heck::{AsKebabCase, AsPascalCase, AsSnekCase};
use itertools::Itertools;
use ploidy_core::{
arena::Arena,
codegen::{UniqueNames, unique::WordSegments},
ir::{PrimitiveType, StructFieldNameHint, UntaggedVariantNameHint},
};
use proc_macro2::{Ident, Span, TokenStream};
use quote::{IdentFragment, ToTokens, TokenStreamExt};
use ref_cast::{RefCastCustom, ref_cast_custom};
const KEYWORDS: &[&str] = &["crate", "self", "super", "Self"];
#[derive(Debug, Eq, Hash, Ord, PartialEq, PartialOrd, RefCastCustom)]
#[repr(transparent)]
pub struct CodegenIdent(str);
impl CodegenIdent {
#[ref_cast_custom]
fn new(s: &str) -> &Self;
}
#[derive(Debug, Eq, Hash, Ord, PartialEq, PartialOrd, RefCastCustom)]
#[repr(transparent)]
pub struct UniqueIdent(str);
impl UniqueIdent {
#[ref_cast_custom]
fn new(s: &str) -> &Self;
}
impl Deref for UniqueIdent {
type Target = CodegenIdent;
#[inline]
fn deref(&self) -> &Self::Target {
CodegenIdent::new(&self.0)
}
}
#[derive(Clone, Copy, Debug)]
pub enum CodegenIdentUsage<'a> {
Module(&'a CodegenIdent),
Type(&'a CodegenIdent),
Field(&'a UniqueIdent),
Variant(&'a UniqueIdent),
Param(&'a UniqueIdent),
Method(&'a UniqueIdent),
}
impl<'a> CodegenIdentUsage<'a> {
pub fn display(self) -> impl Display {
struct DisplayUsage<'a>(CodegenIdentUsage<'a>);
impl Display for DisplayUsage<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = self.0.as_str();
if !s.starts_with(unicode_ident::is_xid_start) {
f.write_char('_')?;
}
match self.0 {
CodegenIdentUsage::Type(_) | CodegenIdentUsage::Variant(_) => {
write!(f, "{}", AsPascalCase(s))
}
CodegenIdentUsage::Module(_)
| CodegenIdentUsage::Field(_)
| CodegenIdentUsage::Param(_)
| CodegenIdentUsage::Method(_) => write!(f, "{}", AsSnekCase(s)),
}
}
}
DisplayUsage(self)
}
#[inline]
fn as_str(&self) -> &str {
match self {
CodegenIdentUsage::Type(s) => &s.0,
CodegenIdentUsage::Variant(s) => &s.0,
CodegenIdentUsage::Module(s) => &s.0,
CodegenIdentUsage::Field(s) => &s.0,
CodegenIdentUsage::Param(s) => &s.0,
CodegenIdentUsage::Method(s) => &s.0,
}
}
}
impl IdentFragment for CodegenIdentUsage<'_> {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.display())
}
}
impl ToTokens for CodegenIdentUsage<'_> {
#[inline]
fn to_tokens(&self, tokens: &mut TokenStream) {
let s = self.display().to_string();
let ident = syn::parse_str(&s).unwrap_or_else(|_| Ident::new_raw(&s, Span::call_site()));
tokens.append(ident);
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ResourceGroup<'a> {
Named(&'a UniqueIdent),
#[default]
Default,
}
impl<'a> ResourceGroup<'a> {
#[inline]
pub fn name(self) -> Option<&'a UniqueIdent> {
match self {
Self::Named(name) => Some(name),
Self::Default => None,
}
}
#[inline]
pub fn is_default(&self) -> bool {
matches!(self, Self::Default)
}
}
#[derive(Clone, Copy, Debug)]
pub struct AsFeatureName<'a>(pub &'a UniqueIdent);
impl Display for AsFeatureName<'_> {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", AsKebabCase(&self.0.0))
}
}
#[derive(Debug)]
pub struct UniqueIdents<'a>(UniqueNames<'a>);
impl<'a> UniqueIdents<'a> {
#[inline]
pub fn new(arena: &'a Arena) -> Self {
Self::with_reserved(arena, &[])
}
#[inline]
pub fn with_reserved(arena: &'a Arena, reserved: &[&str]) -> Self {
Self(UniqueNames::with_reserved(
arena,
reserved.iter().chain(KEYWORDS).copied(),
))
}
#[inline]
pub fn ident(&mut self, name: &str) -> &'a UniqueIdent {
UniqueIdent::new(self.0.uniquify(&clean(name)))
}
#[inline]
pub fn field_name_hint(&mut self, hint: StructFieldNameHint) -> &'a UniqueIdent {
use StructFieldNameHint::*;
UniqueIdent::new(match hint {
Index(index) => self.0.uniquify(&format!("variant_{index}")),
AdditionalProperties => self.0.uniquify("additional_properties"),
})
}
#[inline]
pub fn variant_name_hint(&mut self, hint: UntaggedVariantNameHint) -> &'a UniqueIdent {
use {PrimitiveType::*, UntaggedVariantNameHint::*};
UniqueIdent::new(match hint {
Primitive(String) => self.0.uniquify("String"),
Primitive(I8) => self.0.uniquify("I8"),
Primitive(U8) => self.0.uniquify("U8"),
Primitive(I16) => self.0.uniquify("I16"),
Primitive(U16) => self.0.uniquify("U16"),
Primitive(I32) => self.0.uniquify("I32"),
Primitive(U32) => self.0.uniquify("U32"),
Primitive(I64) => self.0.uniquify("I64"),
Primitive(U64) => self.0.uniquify("U64"),
Primitive(F32) => self.0.uniquify("F32"),
Primitive(F64) => self.0.uniquify("F64"),
Primitive(Bool) => self.0.uniquify("Bool"),
Primitive(DateTime) => self.0.uniquify("DateTime"),
Primitive(UnixTime) => self.0.uniquify("UnixTime"),
Primitive(Date) => self.0.uniquify("Date"),
Primitive(Url) => self.0.uniquify("Url"),
Primitive(Uuid) => self.0.uniquify("Uuid"),
Primitive(Bytes) => self.0.uniquify("Bytes"),
Primitive(Binary) => self.0.uniquify("Binary"),
Array => self.0.uniquify("Array"),
Map => self.0.uniquify("Map"),
Index(index) => self.0.uniquify(&format!("V{index}")),
})
}
}
#[inline]
fn clean(s: &str) -> String {
WordSegments::new(s)
.flat_map(|(_, s)| s.split(|c| !unicode_ident::is_xid_continue(c)))
.join("_")
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use syn::parse_quote;
#[test]
fn test_codegen_ident_type() {
let arena = Arena::new();
let mut scope = UniqueIdents::new(&arena);
let ident = scope.ident("pet_store");
let usage = CodegenIdentUsage::Type(ident);
let actual: syn::Ident = parse_quote!(#usage);
let expected: syn::Ident = parse_quote!(PetStore);
assert_eq!(actual, expected);
}
#[test]
fn test_codegen_ident_field() {
let arena = Arena::new();
let mut scope = UniqueIdents::new(&arena);
let ident = scope.ident("petStore");
let usage = CodegenIdentUsage::Field(ident);
let actual: syn::Ident = parse_quote!(#usage);
let expected: syn::Ident = parse_quote!(pet_store);
assert_eq!(actual, expected);
}
#[test]
fn test_codegen_ident_module() {
let arena = Arena::new();
let mut scope = UniqueIdents::new(&arena);
let ident = scope.ident("MyModule");
let usage = CodegenIdentUsage::Module(ident);
let actual: syn::Ident = parse_quote!(#usage);
let expected: syn::Ident = parse_quote!(my_module);
assert_eq!(actual, expected);
}
#[test]
fn test_codegen_ident_variant() {
let arena = Arena::new();
let mut scope = UniqueIdents::new(&arena);
let ident = scope.ident("http_error");
let usage = CodegenIdentUsage::Variant(ident);
let actual: syn::Ident = parse_quote!(#usage);
let expected: syn::Ident = parse_quote!(HttpError);
assert_eq!(actual, expected);
}
#[test]
fn test_codegen_ident_param() {
let arena = Arena::new();
let mut scope = UniqueIdents::new(&arena);
let ident = scope.ident("userId");
let usage = CodegenIdentUsage::Param(ident);
let actual: syn::Ident = parse_quote!(#usage);
let expected: syn::Ident = parse_quote!(user_id);
assert_eq!(actual, expected);
}
#[test]
fn test_codegen_ident_method() {
let arena = Arena::new();
let mut scope = UniqueIdents::new(&arena);
let ident = scope.ident("getUserById");
let usage = CodegenIdentUsage::Method(ident);
let actual: syn::Ident = parse_quote!(#usage);
let expected: syn::Ident = parse_quote!(get_user_by_id);
assert_eq!(actual, expected);
}
#[test]
fn test_codegen_ident_handles_rust_keywords() {
let arena = Arena::new();
let mut scope = UniqueIdents::new(&arena);
let ident = scope.ident("type");
let usage = CodegenIdentUsage::Field(ident);
let actual: syn::Ident = parse_quote!(#usage);
let expected: syn::Ident = parse_quote!(r#type);
assert_eq!(actual, expected);
}
#[test]
fn test_codegen_ident_handles_invalid_start_chars() {
let arena = Arena::new();
let mut scope = UniqueIdents::new(&arena);
let ident = scope.ident("123foo");
let usage = CodegenIdentUsage::Field(ident);
let actual: syn::Ident = parse_quote!(#usage);
let expected: syn::Ident = parse_quote!(_123_foo);
assert_eq!(actual, expected);
}
#[test]
fn test_codegen_ident_handles_special_chars() {
let arena = Arena::new();
let mut scope = UniqueIdents::new(&arena);
let ident = scope.ident("foo-bar-baz");
let usage = CodegenIdentUsage::Field(ident);
let actual: syn::Ident = parse_quote!(#usage);
let expected: syn::Ident = parse_quote!(foo_bar_baz);
assert_eq!(actual, expected);
}
#[test]
fn test_codegen_ident_handles_number_prefix() {
let arena = Arena::new();
let mut scope = UniqueIdents::new(&arena);
let ident = scope.ident("1099KStatus");
let usage = CodegenIdentUsage::Field(ident);
let actual: syn::Ident = parse_quote!(#usage);
let expected: syn::Ident = parse_quote!(_1099_k_status);
assert_eq!(actual, expected);
let usage = CodegenIdentUsage::Type(ident);
let actual: syn::Ident = parse_quote!(#usage);
let expected: syn::Ident = parse_quote!(_1099KStatus);
assert_eq!(actual, expected);
}
#[test]
fn test_untagged_variant_name_index() {
let arena = Arena::new();
let mut scope = UniqueIdents::new(&arena);
let ident = scope.variant_name_hint(UntaggedVariantNameHint::Index(0));
assert_eq!(&ident.0, "V");
let ident = scope.variant_name_hint(UntaggedVariantNameHint::Index(42));
assert_eq!(&ident.0, "V42");
}
#[test]
fn test_struct_field_name_index() {
let arena = Arena::new();
let mut scope = UniqueIdents::new(&arena);
let ident0 = scope.field_name_hint(StructFieldNameHint::Index(0));
let usage = CodegenIdentUsage::Field(ident0);
let actual: syn::Ident = parse_quote!(#usage);
let expected: syn::Ident = parse_quote!(variant);
assert_eq!(actual, expected);
let ident5 = scope.field_name_hint(StructFieldNameHint::Index(5));
let usage = CodegenIdentUsage::Field(ident5);
let actual: syn::Ident = parse_quote!(#usage);
let expected: syn::Ident = parse_quote!(variant5);
assert_eq!(actual, expected);
}
#[test]
fn test_struct_field_name_additional_properties() {
let arena = Arena::new();
let mut scope = UniqueIdents::new(&arena);
let ident = scope.field_name_hint(StructFieldNameHint::AdditionalProperties);
let usage = CodegenIdentUsage::Field(ident);
let actual: syn::Ident = parse_quote!(#usage);
let expected: syn::Ident = parse_quote!(additional_properties);
assert_eq!(actual, expected);
}
#[test]
fn test_clean() {
assert_eq!(clean("foo-bar"), "foo_bar");
assert_eq!(clean("foo.bar"), "foo_bar");
assert_eq!(clean("foo bar"), "foo_bar");
assert_eq!(clean("foo@bar"), "foo_bar");
assert_eq!(clean("foo#bar"), "foo_bar");
assert_eq!(clean("foo!bar"), "foo_bar");
assert_eq!(clean("foo_bar"), "foo_bar");
assert_eq!(clean("FooBar"), "Foo_Bar");
assert_eq!(clean("foo123"), "foo_123");
assert_eq!(clean("_foo"), "foo");
assert_eq!(clean("_foo"), "foo");
assert_eq!(clean("__foo"), "foo");
assert_eq!(clean("123foo"), "123_foo");
assert_eq!(clean("9bar"), "9_bar");
assert_eq!(clean("café"), "café");
assert_eq!(clean("foo™bar"), "foo_bar");
assert_eq!(clean("foo---bar"), "foo_bar");
assert_eq!(clean("foo...bar"), "foo_bar");
}
#[test]
fn test_codegen_ident_scope_handles_empty() {
let arena = Arena::new();
let mut scope = UniqueIdents::new(&arena);
let ident = scope.ident("");
let usage = CodegenIdentUsage::Field(ident);
let actual: syn::Ident = parse_quote!(#usage);
let expected: syn::Ident = parse_quote!(_1);
assert_eq!(actual, expected);
let usage = CodegenIdentUsage::Type(ident);
let actual: syn::Ident = parse_quote!(#usage);
let expected: syn::Ident = parse_quote!(_1);
assert_eq!(actual, expected);
}
#[test]
fn test_codegen_ident_scope_handles_numeric_names() {
let arena = Arena::new();
let mut scope = UniqueIdents::new(&arena);
let ident = scope.ident("0");
let usage = CodegenIdentUsage::Field(ident);
let actual: syn::Ident = parse_quote!(#usage);
let expected: syn::Ident = parse_quote!(_1);
assert_eq!(actual, expected);
let ident = scope.ident("1");
let usage = CodegenIdentUsage::Type(ident);
let actual: syn::Ident = parse_quote!(#usage);
let expected: syn::Ident = parse_quote!(_2);
assert_eq!(actual, expected);
}
#[test]
fn test_codegen_ident_scope_handles_reserved_suffixes() {
let arena = Arena::new();
let mut scope = UniqueIdents::new(&arena);
let ident = scope.ident("crate");
let usage = CodegenIdentUsage::Method(ident);
let actual: syn::Ident = parse_quote!(#usage);
let expected: syn::Ident = parse_quote!(crate2);
assert_eq!(actual, expected);
let ident = scope.ident("crate2");
let usage = CodegenIdentUsage::Method(ident);
let actual: syn::Ident = parse_quote!(#usage);
let expected: syn::Ident = parse_quote!(crate3);
assert_eq!(actual, expected);
}
}