---
edition = "2024"
[dependencies]
fs-err = "3"
color-eyre = "0.6"
eyre = "0.6"
strum = { version = "0.27", features = ["derive"] }
---
use eyre::Result;
use fs_err as fs;
fn main() -> Result<()> {
color_eyre::install()?;
for file in fs::read_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/src"))?.flatten() {
let file_content = fs::read_to_string(file.path())?;
let Some(new_content) = Marker::EnumParse.replace_content(&file_content) else {
continue;
};
let Some(new_content) = Marker::RenameAll.replace_content(&file_content) else {
continue;
};
fs::write(file.path(), new_content.as_bytes())?;
}
Ok(())
}
#[derive(Copy, Clone, strum::Display)]
#[strum(serialize_all = "snake_case")]
enum Marker {
EnumParse,
RenameAll,
}
impl Marker {
fn content(self) -> String {
let content = match self {
Self::RenameAll => {
r#"
const STREN_RENAME_ALL: Option<$crate::private::const_format::Case> = {
match $crate::extract_field!(rename_all: $(#[$($enum_attr)*])*) {
Some("lowercase") => Some($crate::private::const_format::Case::Lower),
Some("UPPERCASE") => Some($crate::private::const_format::Case::Upper),
Some("PascalCase") => Some($crate::private::const_format::Case::Pascal),
Some("camelCase") => Some($crate::private::const_format::Case::Camel),
Some("snake_case") => Some($crate::private::const_format::Case::Snake),
Some("SCREAMING_SNAKE_CASE") => Some($crate::private::const_format::Case::UpperSnake),
Some("kebab-case") => Some($crate::private::const_format::Case::Kebab),
Some("SCREAMING-KEBAB-CASE") => Some($crate::private::const_format::Case::UpperKebab),
Some(unknown) => panic!("invalid value for `#[stren(rename_all_fields)]`"),
None => None
}
};"#
}
Self::EnumParse => {
r#"
$(#[$($enum_attr:tt)*])*
$enum_vis:vis enum $enum_ident:ident
// a best-effort parsing of generics
//
// what's missing:
//
// - more than 1 lifetime bound (e.g. 'a: 'b + 'c)
// - more than 1 trait bound (e.g. T: A + B + C)
// - support for "use" TypeParam in trait bounds
// - `const` type parameters are totally unsupported
// - `for<..>` lifetimes in `where` clause
$(<
$(
$(#[$($type_param_attr:tt)*])*
// lifetime parameter
$($type_param_lifetime:lifetime $(: $type_param_lifetime_super:lifetime)?)?
// type parameter
$($type_param_type:ident
$(:
$($type_param_lifetime_bound:lifetime)?
$($type_param_type_bound:path)?
)? $(= $type_param_default:ty)?
)?
),*
$(,)?
>)?
$(where $(
$($where_lifetime:lifetime: $where_lifetime_bounds:lifetime)?
$($where_type_param_ty:ty: $where_type_param_bounds:path)?
),*)?
{
$(
$(#[$($enum_variant_attr:tt)*])*
$enum_variant:ident
// enum with named fields
$({
$(
$(#[$($enum_variant_named_field_attr:tt)*])*
$enum_variant_named_field_ident:ident: $enum_variant_named_field_ty:ty
),* $(,)?
})?
// enum with unnamed fields
$((
$(
$(#[$($enum_variant_unnamed_field_attr:tt)*])*
$enum_variant_unnamed_field_ty:ty
),* $(,)?
))?
// discriminant
$(= $enum_variant_discriminant:expr)?
),* $(,)?
}"#
}
};
format!(
"{} NOTE: generated by `generate.rs`{content}\n// {}",
self.opening(),
self.closing()
)
}
fn replace_content(self, content: &str) -> Option<String> {
let (prefix, rest) = content.split_once(&self.opening())?;
let indent = prefix
.chars()
.rev()
.skip(3)
.take_while(|ch| *ch == ' ')
.collect::<String>();
let (mid, suffix) = rest.split_once(&self.closing())?;
let content = self
.content()
.lines()
.enumerate()
.map(|(i, line)| {
if i == 0 {
line.to_string()
} else {
format!("{indent}{line}")
}
})
.collect::<Vec<_>>()
.join("\n");
Some(format!("{prefix}{content}{suffix}"))
}
fn opening(self) -> String {
format!("<{self}: start>")
}
fn closing(self) -> String {
format!("<{self}: end>")
}
}