use std::collections::HashMap;
#[cfg(test)]
use std::collections::HashSet;
use std::sync::{OnceLock, RwLock};
use super::FieldDesc;
static DECLARED: OnceLock<RwLock<HashMap<String, &'static [FieldDesc]>>> = OnceLock::new();
fn registry() -> &'static RwLock<HashMap<String, &'static [FieldDesc]>> {
DECLARED.get_or_init(|| RwLock::new(HashMap::new()))
}
#[cfg(test)]
static DECLARED_NOTHING: OnceLock<RwLock<HashSet<String>>> = OnceLock::new();
#[cfg(test)]
fn declared_nothing() -> &'static RwLock<HashSet<String>> {
DECLARED_NOTHING.get_or_init(|| RwLock::new(HashSet::new()))
}
#[cfg(test)]
pub fn record_types_declaring_nothing() -> Vec<String> {
let mut names: Vec<String> = declared_nothing()
.read()
.expect("declared-field registry lock poisoned")
.iter()
.cloned()
.collect();
names.sort();
names
}
#[cfg(test)]
fn note_declared_nothing(record_type: &str) {
declared_nothing()
.write()
.expect("declared-field registry lock poisoned")
.insert(record_type.to_string());
}
#[cfg(not(test))]
fn note_declared_nothing(_record_type: &str) {}
pub fn register_declared_fields(record_type: &str, fields: &'static [FieldDesc]) {
if fields.is_empty() {
note_declared_nothing(record_type);
return;
}
registry()
.write()
.expect("declared-field registry lock poisoned")
.insert(record_type.to_string(), fields);
}
pub fn declared_field_table(record_type: &str) -> Option<&'static [FieldDesc]> {
match super::dbd_generated::record_fields(record_type) {
Some(generated) => Some(generated),
None => registry()
.read()
.expect("declared-field registry lock poisoned")
.get(record_type)
.copied(),
}
}
pub fn declared_fields(record_type: &str) -> impl Iterator<Item = &'static FieldDesc> {
super::dbd_generated::DB_COMMON_FIELDS
.iter()
.chain(declared_field_table(record_type).unwrap_or(&[]).iter())
}
pub fn declared_field(record_type: &str, field: &str) -> Option<&'static FieldDesc> {
let named = |t: &'static [FieldDesc]| t.iter().find(|f| f.name.eq_ignore_ascii_case(field));
declared_field_table(record_type)
.and_then(named)
.or_else(|| named(super::dbd_generated::DB_COMMON_FIELDS))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::server::record::dbd_generated::RECORD_TYPES;
#[test]
fn every_serveable_record_type_declares_its_own_fields() {
let missing: Vec<&str> = RECORD_TYPES
.iter()
.copied()
.filter(|rt| declared_field_table(rt).is_none_or(<[FieldDesc]>::is_empty))
.collect();
assert!(
missing.is_empty(),
"record types with no declaration of their own: {missing:?}"
);
let forgot: Vec<&&str> = RECORD_TYPES
.iter()
.filter(|rt| record_types_declaring_nothing().iter().any(|n| n == *rt))
.collect();
assert!(
forgot.is_empty(),
"record types that registered an EMPTY declaration: {forgot:?}"
);
}
#[test]
fn a_type_that_declares_nothing_is_recorded_and_loses_its_own_fields() {
register_declared_fields("zzForgotToDeclare", &[]);
assert!(
record_types_declaring_nothing()
.iter()
.any(|n| n == "zzForgotToDeclare"),
"an empty registration must leave a trace"
);
assert!(declared_field_table("zzForgotToDeclare").is_none());
assert!(declared_field("zzForgotToDeclare", "FLNK").is_some());
assert!(declared_field("zzForgotToDeclare", "INP").is_none());
assert_eq!(
crate::types::dbf_link_class("zzForgotToDeclare", "INP"),
None
);
}
}