use clap::{Parser, ValueEnum};
use std::io::{self, Read};
#[derive(Parser)]
#[command(name = "case-converter")]
#[command(about = "Converts text between different case styles")]
struct Args {
#[arg(
short = 'c',
long = "case",
value_enum,
required_unless_present = "show_version"
)]
case: Option<Case>,
text: Option<String>,
#[arg(short = 'v', long = "version")]
show_version: bool,
}
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
enum Case {
#[value(name = "snake_case")]
#[value(alias = "snake")]
#[value(help = "snake_case format (alias: snake)")]
Snake,
#[value(name = "UPPER_SNAKE_CASE")]
#[value(alias = "upper_snake")]
#[value(alias = "upper-snake")]
#[value(help = "UPPER_SNAKE_CASE format (aliases: upper_snake, upper-snake)")]
UpperSnake,
#[value(name = "camelCase")]
#[value(alias = "camel")]
#[value(help = "camelCase format (alias: camel)")]
Camel,
#[value(name = "PascalCase")]
#[value(alias = "pascal")]
#[value(help = "PascalCase format (alias: pascal)")]
Pascal,
#[value(name = "kebab-case")]
#[value(alias = "kebab")]
#[value(help = "kebab-case format (alias: kebab)")]
Kebab,
}
fn main() {
let args = Args::parse();
if args.show_version {
println!("case-converter {}", env!("CARGO_PKG_VERSION"));
return;
}
let case = args
.case
.expect("Case should be present when not showing version");
let input = match args.text {
Some(text) => text,
None => {
let mut buffer = String::new();
io::stdin()
.read_to_string(&mut buffer)
.expect("Failed to read from stdin");
buffer.trim().to_string()
}
};
let output = convert_case(&input, case);
println!("{}", output);
}
fn convert_case(input: &str, case: Case) -> String {
let words = split_into_words(input);
match case {
Case::Snake => words.join("_").to_lowercase(),
Case::UpperSnake => words.join("_").to_uppercase(),
Case::Camel => {
if words.is_empty() {
String::new()
} else {
let mut result = words[0].to_lowercase();
for word in &words[1..] {
result.push_str(&capitalize(word));
}
result
}
}
Case::Pascal => words
.iter()
.map(|word| capitalize(word))
.collect::<Vec<_>>()
.join(""),
Case::Kebab => words.join("-").to_lowercase(),
}
}
fn split_into_words(input: &str) -> Vec<String> {
if input.is_empty() {
return vec![];
}
let mut words = Vec::new();
let mut current_word = String::new();
let add_word = |words: &mut Vec<String>, current_word: &mut String| {
if !current_word.is_empty() {
words.push(current_word.clone());
current_word.clear();
}
};
let chars: Vec<char> = input.chars().collect();
let mut i = 0;
while i < chars.len() {
match chars[i] {
'_' | '-' | ' ' => {
add_word(&mut words, &mut current_word);
}
c if c.is_uppercase() => {
if i > 0 && !current_word.is_empty() && chars[i - 1].is_lowercase() {
add_word(&mut words, &mut current_word);
}
current_word.push(c);
}
c => {
current_word.push(c);
}
}
i += 1;
}
add_word(&mut words, &mut current_word);
words
}
fn capitalize(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
}
}