use crate::symbol::SymbolId;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use std::collections::HashMap;
pub use super::specflow_common::{ConstraintKind, IntentKind, SpecSource};
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Serialize, Deserialize)]
#[repr(transparent)]
pub struct SpecNodeId(u32);
impl SpecNodeId {
const KIND_SHIFT: u32 = 30;
const KIND_MASK: u32 = 0xC000_0000;
const INDEX_MASK: u32 = 0x3FFF_FFFF;
const KIND_GROUP: u32 = 0;
const KIND_SPEC: u32 = 1;
const KIND_CONSTRAINT: u32 = 2;
const KIND_INTENT: u32 = 3;
#[inline]
pub const fn group(idx: u32) -> Self {
Self((Self::KIND_GROUP << Self::KIND_SHIFT) | idx)
}
#[inline]
pub const fn spec(idx: u32) -> Self {
Self((Self::KIND_SPEC << Self::KIND_SHIFT) | idx)
}
#[inline]
pub const fn constraint(idx: u32) -> Self {
Self((Self::KIND_CONSTRAINT << Self::KIND_SHIFT) | idx)
}
#[inline]
pub const fn intent(idx: u32) -> Self {
Self((Self::KIND_INTENT << Self::KIND_SHIFT) | idx)
}
#[inline]
pub const fn kind(self) -> SpecNodeKind {
match (self.0 & Self::KIND_MASK) >> Self::KIND_SHIFT {
Self::KIND_GROUP => SpecNodeKind::Group,
Self::KIND_SPEC => SpecNodeKind::SpecAlias,
Self::KIND_CONSTRAINT => SpecNodeKind::Constraint,
Self::KIND_INTENT => SpecNodeKind::Intent,
_ => unreachable!(),
}
}
#[inline]
pub const fn index(self) -> u32 {
self.0 & Self::INDEX_MASK
}
#[inline]
pub const fn as_u32(self) -> u32 {
self.0
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum SpecNodeKind {
Group,
SpecAlias,
Constraint,
Intent,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GroupData {
pub name: String,
pub description: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SpecAliasData {
pub alias_id: SymbolId,
pub alias_name: String,
pub wrapped_type_id: Option<SymbolId>,
pub wrapped_type_name: Option<String>,
pub group_idx: u32,
pub source: SpecSource,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ConstraintData {
pub kind: ConstraintKind,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct IntentData {
pub description: String,
pub kind: IntentKind,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct SpecLookupTable {
pub symbol_to_spec: HashMap<SymbolId, u32>,
pub name_to_group: HashMap<String, u32>,
pub group_to_specs: Vec<SmallVec<[u32; 8]>>,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct SpecFlowGraphV2 {
groups: Vec<GroupData>,
spec_aliases: Vec<SpecAliasData>,
constraints: Vec<ConstraintData>,
intents: Vec<IntentData>,
spec_dependencies: HashMap<u32, SmallVec<[u32; 2]>>,
spec_to_constraints: HashMap<u32, SmallVec<[u32; 2]>>,
spec_to_intents: HashMap<u32, SmallVec<[u32; 2]>>,
spec_related: HashMap<u32, SmallVec<[u32; 2]>>,
lookup: SpecLookupTable,
}
impl SpecFlowGraphV2 {
pub fn new() -> Self {
Self::default()
}
pub fn add_group(
&mut self,
name: impl Into<String>,
description: Option<String>,
) -> SpecNodeId {
let name = name.into();
if let Some(&idx) = self.lookup.name_to_group.get(&name) {
if description.is_some() {
self.groups[idx as usize].description = description;
}
return SpecNodeId::group(idx);
}
let idx = self.groups.len() as u32;
self.groups.push(GroupData {
name: name.clone(),
description,
});
self.lookup.name_to_group.insert(name, idx);
self.lookup.group_to_specs.push(SmallVec::new());
SpecNodeId::group(idx)
}
pub fn add_spec_alias(
&mut self,
alias_id: SymbolId,
alias_name: String,
group_name: &str,
wrapped_type_id: Option<SymbolId>,
wrapped_type_name: Option<String>,
source: SpecSource,
) -> SpecNodeId {
let group_node = self.add_group(group_name, None);
let group_idx = group_node.index();
let spec_idx = self.spec_aliases.len() as u32;
self.spec_aliases.push(SpecAliasData {
alias_id,
alias_name,
wrapped_type_id,
wrapped_type_name,
group_idx,
source,
});
self.lookup.symbol_to_spec.insert(alias_id, spec_idx);
self.lookup.group_to_specs[group_idx as usize].push(spec_idx);
SpecNodeId::spec(spec_idx)
}
pub fn add_constraint(&mut self, kind: ConstraintKind) -> SpecNodeId {
let idx = self.constraints.len() as u32;
self.constraints.push(ConstraintData { kind });
SpecNodeId::constraint(idx)
}
pub fn add_intent(&mut self, description: impl Into<String>, kind: IntentKind) -> SpecNodeId {
let idx = self.intents.len() as u32;
self.intents.push(IntentData {
description: description.into(),
kind,
});
SpecNodeId::intent(idx)
}
pub fn add_dependency(&mut self, from: SpecNodeId, to: SpecNodeId) {
debug_assert!(from.kind() == SpecNodeKind::SpecAlias);
debug_assert!(to.kind() == SpecNodeKind::SpecAlias);
self.spec_dependencies
.entry(from.index())
.or_default()
.push(to.index());
}
pub fn add_related(&mut self, a: SpecNodeId, b: SpecNodeId) {
debug_assert!(a.kind() == SpecNodeKind::SpecAlias);
debug_assert!(b.kind() == SpecNodeKind::SpecAlias);
self.spec_related
.entry(a.index())
.or_default()
.push(b.index());
self.spec_related
.entry(b.index())
.or_default()
.push(a.index());
}
pub fn link_constraint(&mut self, spec: SpecNodeId, constraint: SpecNodeId) {
debug_assert!(spec.kind() == SpecNodeKind::SpecAlias);
debug_assert!(constraint.kind() == SpecNodeKind::Constraint);
self.spec_to_constraints
.entry(spec.index())
.or_default()
.push(constraint.index());
}
pub fn link_intent(&mut self, spec: SpecNodeId, intent: SpecNodeId) {
debug_assert!(spec.kind() == SpecNodeKind::SpecAlias);
debug_assert!(intent.kind() == SpecNodeKind::Intent);
self.spec_to_intents
.entry(spec.index())
.or_default()
.push(intent.index());
}
pub fn group_by_name(&self, name: &str) -> Option<SpecNodeId> {
self.lookup
.name_to_group
.get(name)
.map(|&idx| SpecNodeId::group(idx))
}
pub fn spec_by_symbol(&self, id: SymbolId) -> Option<SpecNodeId> {
self.lookup
.symbol_to_spec
.get(&id)
.map(|&idx| SpecNodeId::spec(idx))
}
pub fn specs_in_group(&self, group: SpecNodeId) -> impl Iterator<Item = SpecNodeId> + '_ {
debug_assert!(group.kind() == SpecNodeKind::Group);
self.lookup
.group_to_specs
.get(group.index() as usize)
.into_iter()
.flat_map(|v| v.iter().map(|&idx| SpecNodeId::spec(idx)))
}
pub fn dependencies(&self, spec: SpecNodeId) -> impl Iterator<Item = SpecNodeId> + '_ {
debug_assert!(spec.kind() == SpecNodeKind::SpecAlias);
self.spec_dependencies
.get(&spec.index())
.into_iter()
.flat_map(|v| v.iter().map(|&idx| SpecNodeId::spec(idx)))
}
pub fn constraints(&self, spec: SpecNodeId) -> impl Iterator<Item = SpecNodeId> + '_ {
debug_assert!(spec.kind() == SpecNodeKind::SpecAlias);
self.spec_to_constraints
.get(&spec.index())
.into_iter()
.flat_map(|v| v.iter().map(|&idx| SpecNodeId::constraint(idx)))
}
pub fn get_group(&self, id: SpecNodeId) -> Option<&GroupData> {
debug_assert!(id.kind() == SpecNodeKind::Group);
self.groups.get(id.index() as usize)
}
pub fn get_spec_alias(&self, id: SpecNodeId) -> Option<&SpecAliasData> {
debug_assert!(id.kind() == SpecNodeKind::SpecAlias);
self.spec_aliases.get(id.index() as usize)
}
pub fn get_constraint(&self, id: SpecNodeId) -> Option<&ConstraintData> {
debug_assert!(id.kind() == SpecNodeKind::Constraint);
self.constraints.get(id.index() as usize)
}
pub fn get_intent(&self, id: SpecNodeId) -> Option<&IntentData> {
debug_assert!(id.kind() == SpecNodeKind::Intent);
self.intents.get(id.index() as usize)
}
pub fn all_groups(&self) -> impl Iterator<Item = (SpecNodeId, &GroupData)> {
self.groups
.iter()
.enumerate()
.map(|(i, data)| (SpecNodeId::group(i as u32), data))
}
pub fn all_spec_aliases(&self) -> impl Iterator<Item = (SpecNodeId, &SpecAliasData)> {
self.spec_aliases
.iter()
.enumerate()
.map(|(i, data)| (SpecNodeId::spec(i as u32), data))
}
pub fn group_count(&self) -> usize {
self.groups.len()
}
pub fn spec_count(&self) -> usize {
self.spec_aliases.len()
}
pub fn constraint_count(&self) -> usize {
self.constraints.len()
}
pub fn node_count(&self) -> usize {
self.groups.len() + self.spec_aliases.len() + self.constraints.len() + self.intents.len()
}
pub fn is_empty(&self) -> bool {
self.node_count() == 0
}
pub fn group_names(&self) -> impl Iterator<Item = &str> {
self.groups.iter().map(|g| g.name.as_str())
}
pub fn specs_in_group_by_name(&self, name: &str) -> impl Iterator<Item = SpecNodeId> + '_ {
self.lookup
.name_to_group
.get(name)
.and_then(|&idx| self.lookup.group_to_specs.get(idx as usize))
.into_iter()
.flat_map(|v| v.iter().map(|&idx| SpecNodeId::spec(idx)))
}
pub fn dependents(&self, spec: SpecNodeId) -> impl Iterator<Item = SpecNodeId> + '_ {
debug_assert!(spec.kind() == SpecNodeKind::SpecAlias);
let target_idx = spec.index();
self.spec_dependencies
.iter()
.filter(move |(_, deps)| deps.contains(&target_idx))
.map(|(&from_idx, _)| SpecNodeId::spec(from_idx))
}
pub fn edge_count(&self) -> usize {
let dep_edges: usize = self.spec_dependencies.values().map(|v| v.len()).sum();
let constraint_edges: usize = self.spec_to_constraints.values().map(|v| v.len()).sum();
let intent_edges: usize = self.spec_to_intents.values().map(|v| v.len()).sum();
let related_edges: usize = self.spec_related.values().map(|v| v.len()).sum();
let belongs_to_edges = self.spec_aliases.len();
dep_edges + constraint_edges + intent_edges + related_edges + belongs_to_edges
}
pub fn related(&self, spec: SpecNodeId) -> impl Iterator<Item = SpecNodeId> + '_ {
debug_assert!(spec.kind() == SpecNodeKind::SpecAlias);
self.spec_related
.get(&spec.index())
.into_iter()
.flat_map(|v| v.iter().map(|&idx| SpecNodeId::spec(idx)))
}
pub fn intents(&self, spec: SpecNodeId) -> impl Iterator<Item = SpecNodeId> + '_ {
debug_assert!(spec.kind() == SpecNodeKind::SpecAlias);
self.spec_to_intents
.get(&spec.index())
.into_iter()
.flat_map(|v| v.iter().map(|&idx| SpecNodeId::intent(idx)))
}
pub fn spec_name(&self, id: SpecNodeId) -> Option<&str> {
self.get_spec_alias(id).map(|data| data.alias_name.as_str())
}
pub fn wrapped_type_name(&self, id: SpecNodeId) -> Option<&str> {
self.get_spec_alias(id)
.and_then(|data| data.wrapped_type_name.as_deref())
}
pub fn spec_group_name(&self, id: SpecNodeId) -> Option<&str> {
self.get_spec_alias(id)
.and_then(|data| self.groups.get(data.group_idx as usize))
.map(|g| g.name.as_str())
}
pub fn spec_by_name(&self, name: &str) -> Option<SpecNodeId> {
for (id, data) in self.all_spec_aliases() {
if data.alias_name == name {
return Some(id);
}
}
None
}
}
use super::type_alias_registry::TypeAliasRegistry;
use crate::symbol::SymbolRegistry;
pub struct SpecFlowBuilderV2<'a> {
alias_registry: &'a TypeAliasRegistry,
symbol_registry: &'a SymbolRegistry,
}
impl<'a> SpecFlowBuilderV2<'a> {
pub fn new(alias_registry: &'a TypeAliasRegistry, symbol_registry: &'a SymbolRegistry) -> Self {
Self {
alias_registry,
symbol_registry,
}
}
pub fn build(self) -> SpecFlowGraphV2 {
let mut graph = SpecFlowGraphV2::new();
for spec_info in self.alias_registry.spec_aliases() {
let alias_name = self
.symbol_registry
.resolve(spec_info.entry.alias_id)
.map(|path| path.name().to_string())
.unwrap_or_default();
let wrapped_type_name = spec_info.entry.resolved.and_then(|wrapped_id| {
self.symbol_registry
.resolve(wrapped_id)
.map(|path| path.to_string())
});
graph.add_spec_alias(
spec_info.entry.alias_id,
alias_name,
&spec_info.group_name,
spec_info.entry.resolved,
wrapped_type_name,
SpecSource::TypeAlias,
);
}
graph
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::symbol::{SymbolPath, SymbolRegistry};
use crate::SymbolKind;
fn create_test_symbol(registry: &mut SymbolRegistry, name: &str) -> SymbolId {
registry
.register(SymbolPath::parse(name).unwrap(), SymbolKind::TypeAlias)
.unwrap()
}
#[test]
fn test_empty_graph() {
let graph = SpecFlowGraphV2::new();
assert!(graph.is_empty());
assert_eq!(graph.node_count(), 0);
}
#[test]
fn test_add_group() {
let mut graph = SpecFlowGraphV2::new();
let g1 = graph.add_group("ConfigGroup", Some("Config types".to_string()));
let g2 = graph.add_group("ConfigGroup", None);
assert_eq!(g1, g2);
assert_eq!(graph.group_count(), 1);
let data = graph.get_group(g1).unwrap();
assert_eq!(data.name, "ConfigGroup");
assert_eq!(data.description, Some("Config types".to_string()));
}
#[test]
fn test_add_spec_alias() {
let mut symbol_registry = SymbolRegistry::new();
let alias_id = create_test_symbol(&mut symbol_registry, "test::DbConfig");
let wrapped_id = create_test_symbol(&mut symbol_registry, "test::DatabaseConfig");
let mut graph = SpecFlowGraphV2::new();
let spec = graph.add_spec_alias(
alias_id,
"DbConfig".to_string(),
"ConfigGroup",
Some(wrapped_id),
Some("test::DatabaseConfig".to_string()),
SpecSource::TypeAlias,
);
assert_eq!(graph.spec_count(), 1);
assert_eq!(graph.group_count(), 1);
let data = graph.get_spec_alias(spec).unwrap();
assert_eq!(data.alias_id, alias_id);
assert_eq!(data.wrapped_type_id, Some(wrapped_id));
assert_eq!(graph.spec_by_symbol(alias_id), Some(spec));
let group = graph.group_by_name("ConfigGroup").unwrap();
let specs: Vec<_> = graph.specs_in_group(group).collect();
assert_eq!(specs.len(), 1);
assert_eq!(specs[0], spec);
}
#[test]
fn test_dependencies() {
let mut symbol_registry = SymbolRegistry::new();
let db_id = create_test_symbol(&mut symbol_registry, "test::DbConfig");
let cache_id = create_test_symbol(&mut symbol_registry, "test::CacheConfig");
let mut graph = SpecFlowGraphV2::new();
let db_spec = graph.add_spec_alias(
db_id,
"DbConfig".to_string(),
"ConfigGroup",
None,
None,
SpecSource::TypeAlias,
);
let cache_spec = graph.add_spec_alias(
cache_id,
"CacheConfig".to_string(),
"ConfigGroup",
None,
None,
SpecSource::TypeAlias,
);
graph.add_dependency(cache_spec, db_spec);
let deps: Vec<_> = graph.dependencies(cache_spec).collect();
assert_eq!(deps.len(), 1);
assert_eq!(deps[0], db_spec);
}
#[test]
fn test_constraints() {
let mut symbol_registry = SymbolRegistry::new();
let id = create_test_symbol(&mut symbol_registry, "test::UserId");
let mut graph = SpecFlowGraphV2::new();
let spec = graph.add_spec_alias(
id,
"UserId".to_string(),
"DomainGroup",
None,
None,
SpecSource::TypeAlias,
);
let constraint = graph.add_constraint(ConstraintKind::Range {
min: Some(1),
max: None,
});
graph.link_constraint(spec, constraint);
let constraints: Vec<_> = graph.constraints(spec).collect();
assert_eq!(constraints.len(), 1);
let data = graph.get_constraint(constraints[0]).unwrap();
assert!(matches!(
data.kind,
ConstraintKind::Range {
min: Some(1),
max: None
}
));
}
}