#![expect(clippy::doc_markdown, reason = "Too annoying for code-gen.")]
mod write;
use std::{
cmp::Ordering,
collections::BTreeSet,
fmt,
path::Path,
};
#[derive(Debug, Clone)]
pub struct FlagsBuilder {
name: String,
docs: String,
scope: Scope,
primary: BTreeSet<Flag>,
alias: BTreeSet<Flag>,
default: BTreeSet<String>,
default_all: bool,
}
impl fmt::Display for FlagsBuilder {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let writer = write::FlagsWriter::from_builder(self);
<write::FlagsWriter as fmt::Display>::fmt(&writer, f)
}
}
impl FlagsBuilder {
#[must_use]
pub fn new<S: AsRef<str>>(name: S) -> Self {
let name = flag_ident(name.as_ref());
let docs = format!("# {name}.");
Self {
name,
docs,
scope: Scope::PubCrate,
primary: BTreeSet::new(),
alias: BTreeSet::new(),
default: BTreeSet::new(),
default_all: false,
}
}
#[must_use]
pub fn with_docs<S: AsRef<str>>(mut self, docs: S) -> Self {
let docs = docs.as_ref().trim();
if ! docs.is_empty() { docs.clone_into(&mut self.docs); }
self
}
#[must_use]
pub const fn private(mut self) -> Self {
self.scope = Scope::Private;
self
}
#[must_use]
pub const fn public(mut self) -> Self {
self.scope = Scope::Pub;
self
}
#[must_use]
pub fn with_defaults<S, I>(mut self, flags: I) -> Self
where
S: AsRef<str>,
I: IntoIterator<Item=S>,
{
self.default.extend(flags.into_iter().map(|f| flag_ident(f.as_ref())));
self
}
#[must_use]
pub const fn with_default_all(mut self) -> Self {
self.default_all = true;
self
}
}
impl FlagsBuilder {
fn unique_flag<S: AsRef<str>>(&self, name: S) -> Flag {
let flag = Flag::new(name);
assert!(
! self.alias.contains(&flag) && ! self.primary.contains(&flag),
"TYPO: duplicate flag/alias ({}). (argyle::FlagsBuilder)",
flag.name,
);
flag
}
}
impl FlagsBuilder {
#[must_use]
pub fn with_flag<S: AsRef<str>>(mut self, name: S, docs: Option<S>) -> Self {
let mut flag = self.unique_flag(name);
if let Some(docs) = docs { flag = flag.with_docs(docs); }
self.primary.insert(flag);
self
}
#[must_use]
pub fn with_complex_flag<S, I>(mut self, name: S, flags: I, docs: Option<S>) -> Self
where
S: AsRef<str>,
I: IntoIterator<Item=S>,
{
let mut flag = self.unique_flag(name).with_deps(flags);
if let Some(docs) = docs { flag = flag.with_docs(docs); }
self.primary.insert(flag);
self
}
#[must_use]
pub fn with_alias<S, I>(mut self, name: S, flags: I, docs: Option<S>) -> Self
where
S: AsRef<str>,
I: IntoIterator<Item=S>,
{
let mut flag = self.unique_flag(name).with_deps(flags);
if let Some(docs) = docs { flag = flag.with_docs(docs); }
assert!(
1 < flag.deps.len(),
"TYPO: aliases need at least two references ({}) (argyle::FlagsBuilder)",
flag.name,
);
self.alias.insert(flag);
self
}
}
impl FlagsBuilder {
pub fn save<P: AsRef<Path>>(&self, file: P) {
use std::io::Write;
let file = file.as_ref();
let code = self.to_string();
assert!(
std::fs::File::create(file).and_then(|mut out|
out.write_all(code.as_bytes()).and_then(|()| out.flush())
).is_ok(),
"Unable to write to {file:?}.",
);
}
}
#[derive(Debug, Clone)]
struct Flag {
name: String,
docs: String,
deps: BTreeSet<String>,
}
impl Eq for Flag {}
impl PartialEq for Flag {
fn eq(&self, other: &Self) -> bool { self.name == other.name }
}
impl Ord for Flag {
fn cmp(&self, other: &Self) -> Ordering { self.name.cmp(&other.name) }
}
impl PartialOrd for Flag {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
}
impl Flag {
#[must_use]
fn new<S: AsRef<str>>(name: S) -> Self {
let name = flag_ident(name.as_ref());
let docs = format!("# {name}.");
Self {
name,
docs,
deps: BTreeSet::new(),
}
}
#[must_use]
fn with_docs<S: AsRef<str>>(mut self, docs: S) -> Self {
let docs = docs.as_ref().trim();
if ! docs.is_empty() { docs.clone_into(&mut self.docs); }
self
}
#[must_use]
fn with_deps<I, S>(mut self, flags: I) -> Self
where
S: AsRef<str>,
I: IntoIterator<Item=S>
{
for flag in flags {
let flag = flag_ident(flag.as_ref());
if flag != self.name { self.deps.insert(flag); }
}
self
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum Scope {
Private,
Pub,
PubCrate,
}
impl fmt::Display for Scope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Private => Ok(()),
Self::Pub => f.write_str("pub "),
Self::PubCrate => f.write_str("pub(crate) "),
}
}
}
fn flag_ident(name: &str) -> String {
let name = name.trim();
let name2 = to_pascal_case(name);
assert!(
name == name2,
"TYPO: Ident {name:?} should be formatted {name2:?}. (argyle::FlagsBuilder)"
);
assert!(
name2 != "None" && ! is_generated_flag(name),
"TYPO: Idents may not be called ({name2})",
);
name2
}
const fn is_generated_flag(name: &str) -> bool {
matches!(name.as_bytes(), [b'Z', b'0'..=b'9' | b'a'..=b'f', b'0'..=b'9' | b'a'..=b'f'])
}
fn to_pascal_case(raw: &str) -> String {
let mut out = String::with_capacity(raw.len());
let mut chars = raw.chars();
let first = chars.next()
.expect("TYPO: Idents must start with an ASCII alphabetic. (argyle::FlagsBuilder)")
.to_ascii_uppercase();
assert!(
first.is_ascii_alphabetic(),
"TYPO: Idents must start with an ASCII alphabetic. (argyle::FlagsBuilder)",
);
out.push(first);
let mut under = false;
let mut lower = false;
for c in chars {
match c {
'A'..='Z' | '0'..='9' => {
out.push(c);
under = false;
},
'_' => { under = true; },
'a'..='z' =>
if under {
out.push(c.to_ascii_uppercase());
under = false;
}
else {
out.push(c);
lower = true;
},
_ => panic!("TYPO: Idents must be ASCII alphanumeric. (argyle::FlagsBuilder)"),
}
}
if 1 < out.len() && ! lower { out[1..].make_ascii_lowercase(); }
out
}
fn to_snake_case(raw: &str) -> String {
let mut out = String::with_capacity(raw.len());
let has_lower = raw.chars().any(|c| c.is_ascii_lowercase());
let mut under = true;
for c in raw.chars() {
match c {
'A'..='Z' => {
if has_lower && ! under {
out.push('_');
under = true;
}
out.push(c.to_ascii_lowercase());
},
'_' => if ! under {
out.push('_');
under = true;
},
'a'..='z' | '0'..='9' => {
under = false;
out.push(c);
},
_ => panic!("TYPO: Idents must be ASCII alphanumeric. (argyle::FlagsBuilder)"),
}
}
if out.ends_with('_') { out.truncate(out.len() - 1); }
assert!(
out.chars().next().is_some_and(|c| c.is_ascii_alphabetic()),
"TYPO: Idents must start with an ASCII alphabetic. (argyle::FlagsBuilder)",
);
out
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn t_pascal_case() {
for (raw, ex) in [
("hello", "Hello"),
("HELLO", "Hello"),
("hello_world", "HelloWorld"),
("hello_1world", "Hello1world"),
] {
assert_eq!(
to_pascal_case(raw),
ex,
"Pascal case failed for {raw}."
);
}
}
#[test]
fn t_snake_case() {
for (raw, ex) in [
("hello", "hello"),
("HELLO", "hello"),
("hello_world", "hello_world"),
("HelloWorld", "hello_world"),
] {
assert_eq!(
to_snake_case(raw),
ex,
"Snake case failed for {raw}."
);
}
}
#[test]
#[should_panic(expected = "TYPO: Idents must be ASCII alphanumeric. (argyle::FlagsBuilder)")]
fn t_flag_ident_not_alphanumeric() {
let _res = flag_ident("Hello World");
}
#[test]
#[should_panic(expected = "TYPO: Idents must start with an ASCII alphabetic. (argyle::FlagsBuilder)")]
fn t_flag_ident_not_first_alpha() {
let _res = flag_ident("1Direction");
}
#[test]
#[should_panic]
fn t_flag_ident_not_pascal() {
let _res = flag_ident("wrong_way");
}
#[test]
#[should_panic(expected = "TYPO: Idents may not be called (None)")]
fn t_flag_ident_reserved1() {
let _res = flag_ident("None");
}
#[test]
#[should_panic(expected = "TYPO: Idents may not be called (Zaa)")]
fn t_flag_ident_reserved2() {
let _res = flag_ident("Zaa");
}
#[test]
#[should_panic(expected = "TYPO: duplicate flag/alias (Bar). (argyle::FlagsBuilder)")]
fn t_flag_builder_dupe_flag() {
FlagsBuilder::new("Foo")
.with_flag("Bar", None)
.with_flag("Bar", None)
.to_string();
}
#[test]
#[should_panic(expected = "TYPO: duplicate flag/alias (Bar). (argyle::FlagsBuilder)")]
fn t_flag_builder_dupe_alias() {
FlagsBuilder::new("Foo")
.with_flag("Foo", None)
.with_flag("Bar", None)
.with_flag("Baz", None)
.with_alias("Bar", ["Foo", "Baz"], None)
.to_string();
}
#[test]
#[should_panic(expected = "TYPO: aliases need at least two references (Baz) (argyle::FlagsBuilder)")]
fn t_flag_builder_unalias() {
FlagsBuilder::new("Foo")
.with_flag("Bar", None)
.with_alias("Baz", ["Baz"], None)
.to_string();
}
#[test]
#[should_panic(expected = "TYPO: flag (Baz) is undefined. (argyle::FlagsBuilder)")]
fn t_flag_builder_undefined_ref() {
FlagsBuilder::new("Foo")
.with_flag("Bar", None)
.with_complex_flag("Foo", ["Baz"], None)
.to_string();
}
}