use indexmap::IndexSet;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Symbols {
Single(String),
Many(Vec<String>),
}
impl Symbols {
#[must_use]
pub fn normalized(self) -> Self {
match self {
Symbols::Single(s) => {
let t = s.trim();
if t.is_empty() {
Symbols::Many(Vec::new())
} else {
Symbols::Single(t.to_owned())
}
}
Symbols::Many(v) => {
let mut dedup = IndexSet::with_capacity(v.len());
for s in v {
let t = s.trim();
if !t.is_empty() {
dedup.insert(t.to_owned());
}
}
let mut out: Vec<String> = dedup.into_iter().collect();
if out.len() == 1 {
Symbols::Single(out.pop().expect("len == 1"))
} else {
Symbols::Many(out)
}
}
}
}
#[must_use]
pub fn len(&self) -> usize {
match self {
Symbols::Single(_) => 1,
Symbols::Many(v) => v.len(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
match self {
Symbols::Single(_) => false,
Symbols::Many(v) => v.is_empty(),
}
}
pub fn iter(&self) -> Box<dyn Iterator<Item = &str> + '_> {
match self {
Symbols::Single(s) => Box::new(std::iter::once(s.as_str())),
Symbols::Many(v) => Box::new(v.iter().map(String::as_str)),
}
}
#[must_use]
pub fn chunked(self, max_per_chunk: usize) -> Vec<Symbols> {
assert!(max_per_chunk > 0, "max_per_chunk must be non-zero");
let items: Vec<String> = match self {
Symbols::Single(s) => vec![s],
Symbols::Many(v) => v,
};
if items.is_empty() {
return Vec::new();
}
items
.chunks(max_per_chunk)
.map(|chunk| {
if chunk.len() == 1 {
Symbols::Single(chunk[0].clone())
} else {
Symbols::Many(chunk.to_vec())
}
})
.collect()
}
}
pub const SUBSCRIPTION_BATCH_LIMIT: Option<usize> = None;
impl From<&str> for Symbols {
fn from(s: &str) -> Self {
Symbols::Single(s.to_owned())
}
}
impl From<String> for Symbols {
fn from(s: String) -> Self {
Symbols::Single(s)
}
}
impl From<&String> for Symbols {
fn from(s: &String) -> Self {
Symbols::Single(s.clone())
}
}
impl From<Vec<String>> for Symbols {
fn from(v: Vec<String>) -> Self {
Symbols::Many(v)
}
}
impl From<Vec<&str>> for Symbols {
fn from(v: Vec<&str>) -> Self {
Symbols::Many(v.into_iter().map(String::from).collect())
}
}
impl<const N: usize> From<[&str; N]> for Symbols {
fn from(arr: [&str; N]) -> Self {
Symbols::Many(arr.iter().map(|s| (*s).to_owned()).collect())
}
}
impl<const N: usize> From<[String; N]> for Symbols {
fn from(arr: [String; N]) -> Self {
Symbols::Many(arr.into_iter().collect())
}
}
impl From<&[&str]> for Symbols {
fn from(s: &[&str]) -> Self {
Symbols::Many(s.iter().map(|s| (*s).to_owned()).collect())
}
}
impl From<&[String]> for Symbols {
fn from(s: &[String]) -> Self {
Symbols::Many(s.to_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_str_produces_single() {
assert_eq!(Symbols::from("2330"), Symbols::Single("2330".to_string()));
}
#[test]
fn from_vec_produces_many() {
assert_eq!(
Symbols::from(vec!["2330", "2454"]),
Symbols::Many(vec!["2330".into(), "2454".into()])
);
}
#[test]
fn normalize_preserves_case() {
let s = Symbols::from(vec!["TXFB6", "txfb6", "TxFb6"]).normalized();
assert_eq!(
s,
Symbols::Many(vec!["TXFB6".into(), "txfb6".into(), "TxFb6".into()])
);
}
#[test]
fn normalize_preserves_case_after_whitespace_trim() {
let s = Symbols::from(vec![" TXFB6 ", "txfb6"]).normalized();
assert_eq!(s, Symbols::Many(vec!["TXFB6".into(), "txfb6".into()]));
}
#[test]
fn normalize_trims_whitespace() {
let s = Symbols::from(vec![" 2330 ", "2454\n"]).normalized();
assert_eq!(s, Symbols::Many(vec!["2330".into(), "2454".into()]));
}
#[test]
fn normalize_dedup_preserves_order() {
let s = Symbols::from(vec!["2330", "2454", "2330", "2317"]).normalized();
assert_eq!(
s,
Symbols::Many(vec!["2330".into(), "2454".into(), "2317".into()])
);
}
#[test]
fn normalize_drops_empties() {
let s = Symbols::from(vec!["2330", "", " ", "2454"]).normalized();
assert_eq!(s, Symbols::Many(vec!["2330".into(), "2454".into()]));
}
#[test]
fn normalize_collapses_many_of_one_to_single() {
let s = Symbols::from(vec!["2330"]).normalized();
assert_eq!(s, Symbols::Single("2330".to_string()));
}
#[test]
fn normalize_single_trims() {
let s = Symbols::Single(" 2330 ".to_string()).normalized();
assert_eq!(s, Symbols::Single("2330".to_string()));
}
#[test]
fn normalize_single_empty_becomes_empty_many() {
let s = Symbols::Single(" ".to_string()).normalized();
assert!(s.is_empty());
}
#[test]
fn len_reflects_symbol_count() {
assert_eq!(Symbols::from("2330").len(), 1);
assert_eq!(Symbols::from(vec!["2330", "2454"]).len(), 2);
}
#[test]
fn iter_yields_in_order() {
let s = Symbols::from(vec!["A", "B", "C"]);
let collected: Vec<&str> = s.iter().collect();
assert_eq!(collected, vec!["A", "B", "C"]);
}
#[test]
fn chunked_splits_into_requested_sizes() {
let chunks = Symbols::from(vec!["A", "B", "C", "D", "E"]).chunked(2);
assert_eq!(chunks.len(), 3);
assert_eq!(chunks[0], Symbols::Many(vec!["A".into(), "B".into()]));
assert_eq!(chunks[1], Symbols::Many(vec!["C".into(), "D".into()]));
assert_eq!(chunks[2], Symbols::Single("E".into()));
}
#[test]
#[should_panic(expected = "max_per_chunk must be non-zero")]
fn chunked_zero_panics() {
let _ = Symbols::from(vec!["A"]).chunked(0);
}
#[test]
fn subscription_batch_limit_is_none() {
assert_eq!(SUBSCRIPTION_BATCH_LIMIT, None);
}
}