use std::{any::TypeId, collections::HashSet};
use crate::{
command::CommandRegistration, core::item::ItemRegistration, query::QueryRegistration,
report::ReportRegistration, view::ViewRegistration,
};
#[derive(Debug, PartialEq)]
pub enum TypegenConstValue {
Str(&'static str),
Int(i64),
Float(f64),
Bool(bool),
}
pub struct TypegenConstRegistration {
pub name: &'static str,
pub value: TypegenConstValue,
pub crate_path: &'static str,
}
inventory::collect!(TypegenConstRegistration);
pub struct TypegenTypeRegistration {
pub id: &'static str,
pub type_name: &'static str,
pub crate_path: &'static str,
}
inventory::collect!(TypegenTypeRegistration);
pub struct FrameworkTypegenRegistration {
pub type_id: &'static str,
}
inventory::collect!(FrameworkTypegenRegistration);
pub trait RegisteredType: 'static {}
impl<T: 'static> RegisteredType for T {}
pub trait TypegenGroup: 'static {}
pub struct TypegenGroupMemberRegistration {
pub group_type_id: fn() -> TypeId,
pub crate_path: &'static str,
}
inventory::collect!(TypegenGroupMemberRegistration);
pub struct TypegenModuleRegistration {
pub id: &'static str,
pub crate_path: &'static str,
pub build: fn() -> crate::typegen_module::TypegenModule,
}
inventory::collect!(TypegenModuleRegistration);
pub struct TypegenCatalog {
pub types: Vec<&'static TypegenTypeRegistration>,
pub constants: Vec<&'static TypegenConstRegistration>,
pub modules: Vec<&'static TypegenModuleRegistration>,
pub items: Vec<&'static ItemRegistration>,
pub queries: Vec<&'static QueryRegistration>,
pub views: Vec<&'static ViewRegistration>,
pub reports: Vec<&'static ReportRegistration>,
pub commands: Vec<&'static CommandRegistration>,
}
impl TypegenCatalog {
#[must_use]
pub fn collect(crate_name: &str) -> Self {
Self::collect_crates([crate_name])
}
#[must_use]
pub fn collect_crates<I, S>(crate_names: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let crate_names = crate_names
.into_iter()
.map(|name| name.as_ref().to_owned())
.collect::<HashSet<_>>();
Self::collect_matching(|path| {
path.split("::")
.next()
.is_some_and(|name| crate_names.contains(name))
})
}
#[must_use]
pub fn collect_framework_types() -> Self {
Self {
types: {
let framework_ids = inventory::iter::<FrameworkTypegenRegistration>
.into_iter()
.map(|entry| entry.type_id)
.collect::<HashSet<_>>();
inventory::iter::<TypegenTypeRegistration>
.into_iter()
.filter(|entry| framework_ids.contains(entry.id))
.collect()
},
constants: Vec::new(),
modules: Vec::new(),
items: Vec::new(),
queries: Vec::new(),
views: Vec::new(),
reports: Vec::new(),
commands: Vec::new(),
}
}
#[must_use]
pub fn collect_group<G: TypegenGroup>() -> Self {
let crate_names = inventory::iter::<TypegenGroupMemberRegistration>
.into_iter()
.filter(|entry| (entry.group_type_id)() == TypeId::of::<G>())
.filter_map(|entry| entry.crate_path.split("::").next())
.collect::<HashSet<_>>();
Self::collect_matching(|path| {
path.split("::")
.next()
.is_some_and(|name| crate_names.contains(name))
})
}
#[must_use]
pub fn collect_crate_family(family: &str) -> Self {
Self::collect_matching(|path| registration_belongs_to_crate_family(path, family))
}
fn collect_matching(selected: impl Fn(&str) -> bool) -> Self {
Self {
types: inventory::iter::<TypegenTypeRegistration>
.into_iter()
.filter(|entry| selected(entry.crate_path))
.collect(),
constants: inventory::iter::<TypegenConstRegistration>
.into_iter()
.filter(|entry| selected(entry.crate_path))
.collect(),
modules: inventory::iter::<TypegenModuleRegistration>
.into_iter()
.filter(|entry| selected(entry.crate_path))
.collect(),
items: inventory::iter::<ItemRegistration>
.into_iter()
.filter(|entry| selected(entry.crate_name))
.collect(),
queries: inventory::iter::<QueryRegistration>
.into_iter()
.filter(|entry| entry.include_in_typegen && selected(entry.crate_name))
.collect(),
views: inventory::iter::<ViewRegistration>
.into_iter()
.filter(|entry| selected(entry.crate_name))
.collect(),
reports: inventory::iter::<ReportRegistration>
.into_iter()
.filter(|entry| selected(entry.crate_name))
.collect(),
commands: inventory::iter::<CommandRegistration>
.into_iter()
.filter(|entry| selected(entry.crate_name))
.collect(),
}
}
#[must_use]
pub fn merge(mut self, other: Self) -> Self {
extend_unique(&mut self.types, other.types);
extend_unique(&mut self.constants, other.constants);
extend_unique(&mut self.modules, other.modules);
extend_unique(&mut self.items, other.items);
extend_unique(&mut self.queries, other.queries);
extend_unique(&mut self.views, other.views);
extend_unique(&mut self.reports, other.reports);
extend_unique(&mut self.commands, other.commands);
self
}
#[must_use]
pub fn type_ids(&self) -> HashSet<&'static str> {
self.types
.iter()
.map(|registration| registration.id)
.collect()
}
}
fn extend_unique<T: 'static>(target: &mut Vec<&'static T>, source: Vec<&'static T>) {
let mut seen = target
.iter()
.map(|entry| std::ptr::from_ref(*entry))
.collect::<HashSet<_>>();
target.extend(
source
.into_iter()
.filter(|entry| seen.insert(std::ptr::from_ref(*entry))),
);
}
#[must_use]
pub fn registration_belongs_to_crate(registration_path: &str, crate_name: &str) -> bool {
registration_path.split("::").next() == Some(crate_name)
}
#[must_use]
pub fn registration_belongs_to_crate_family(registration_path: &str, family: &str) -> bool {
registration_path.split("::").next().is_some_and(|root| {
root == family
|| root
.strip_prefix(family)
.is_some_and(|suffix| suffix.starts_with('_'))
})
}
#[cfg(test)]
mod tests {
#[cfg(feature = "typegen")]
use std::collections::HashSet;
use super::{
TypegenCatalog, TypegenTypeRegistration, registration_belongs_to_crate,
registration_belongs_to_crate_family,
};
struct AggregateGroup;
impl super::TypegenGroup for AggregateGroup {}
inventory::submit! {
super::TypegenGroupMemberRegistration {
group_type_id: || std::any::TypeId::of::<AggregateGroup>(),
crate_path: "rship_core::provider",
}
}
inventory::submit! {
TypegenTypeRegistration {
id: "rship::Own",
type_name: "Own",
crate_path: "rship::nested",
}
}
inventory::submit! {
TypegenTypeRegistration {
id: "rship_core::Foreign",
type_name: "Foreign",
crate_path: "rship_core",
}
}
#[test]
fn catalog_excludes_a_sibling_crate_with_a_shared_name_prefix() {
let catalog = TypegenCatalog::collect("rship");
let ids = catalog.type_ids();
assert!(ids.contains("rship::Own"));
assert!(!ids.contains("rship_core::Foreign"));
}
#[test]
fn aggregate_catalog_collects_only_explicit_crates() {
let catalog = TypegenCatalog::collect_crates(["rship_core", "unregistered_crate"]);
let ids = catalog.type_ids();
assert!(!ids.contains("rship::Own"));
assert!(ids.contains("rship_core::Foreign"));
}
#[test]
fn typed_group_collects_only_enrolled_crates() {
let ids = TypegenCatalog::collect_group::<AggregateGroup>().type_ids();
assert!(ids.contains("rship_core::Foreign"));
assert!(!ids.contains("rship::Own"));
}
#[test]
fn crate_ownership_compares_the_module_path_root() {
assert!(registration_belongs_to_crate("rship", "rship"));
assert!(registration_belongs_to_crate("rship::nested", "rship"));
assert!(!registration_belongs_to_crate("rship_core", "rship"));
assert!(!registration_belongs_to_crate("my_rship::nested", "rship"));
assert!(!registration_belongs_to_crate("rshipper", "rship"));
}
#[test]
fn crate_family_requires_an_underscore_delimiter() {
assert!(registration_belongs_to_crate_family(
"rship_entities::nested",
"rship_entities"
));
assert!(registration_belongs_to_crate_family(
"rship_entities_nodes::nested",
"rship_entities"
));
assert!(!registration_belongs_to_crate_family(
"rship_entities2::nested",
"rship_entities"
));
assert!(!registration_belongs_to_crate_family(
"other_rship_entities::nested",
"rship_entities"
));
}
#[cfg(feature = "typegen")]
#[test]
fn framework_types_compose_without_framework_operations_or_duplicates() {
let framework = TypegenCatalog::collect_framework_types();
let type_names = framework
.types
.iter()
.map(|entry| entry.type_name)
.collect::<HashSet<_>>();
assert!(type_names.contains("IdFilter<Arc<str>>"));
assert!(type_names.contains("StringFilter"));
assert!(type_names.contains("ClientId"));
assert!(type_names.contains("EntityRef"));
assert!(type_names.contains("PairPolicy"));
assert!(type_names.contains("GraphPlanTelemetry"));
assert_eq!(type_names.len(), 15);
assert!(framework.items.is_empty());
assert!(framework.queries.is_empty());
assert!(framework.commands.is_empty());
let type_count = framework.types.len();
let merged = framework.merge(TypegenCatalog::collect_framework_types());
assert_eq!(merged.types.len(), type_count);
}
}