use std::sync::Arc;
use ahash::{HashMap, HashSet};
use zen_expression::variable::VariableType;
use crate::policy::ir::DataModelIr;
use crate::policy::queries::dependency::{DependencyGraph, PathPrefix};
use crate::policy::queries::scope::PropertyScope;
use crate::workspace::db::{Db, DictionaryUnitEntry};
use crate::workspace::types::{
Dictionary, DictionaryEntryInfo, Entity, EntityField, FieldOrigin, Global, InputProperty,
OutputProperty, PropertyKind, ScopeRequest,
};
impl Db {
pub fn entities(&self, req: &ScopeRequest) -> Vec<Entity> {
let entity_filter = (!req.goals.is_empty()).then(|| {
self.goal_reachable_entities(&self.unit(&req.policy_path).dep_graph, &req.goals)
});
let mut by_entity: HashMap<Arc<str>, Vec<EntityField>> = HashMap::default();
let fields = self
.walk_schema_fields(req)
.into_iter()
.chain(self.walk_computed_fields(req));
for (entity, field) in fields {
if entity_filter.as_ref().is_some_and(|f| !f.contains(&entity)) {
continue;
}
by_entity.entry(entity).or_default().push(field);
}
let mut result: Vec<Entity> = by_entity
.into_iter()
.map(|(name, mut fields)| {
fields.sort_by(|a, b| a.name.cmp(&b.name));
Entity { name, fields }
})
.collect();
result.sort_by(|a, b| a.name.cmp(&b.name));
result
}
pub fn globals(&self, req: &ScopeRequest) -> Vec<Global> {
let mut out: Vec<Global> = Vec::new();
let unit = self.unit(&req.policy_path);
let visible = &unit.members;
let entities_map = &unit.entities;
let goal_filter =
(!req.goals.is_empty()).then(|| unit.dep_graph.reachable_from(&req.goals));
for vp in self.walk_visible_properties(&req.policy_path) {
if !matches!(vp.scope, PropertyScope::Global) {
continue;
}
if let Some(filter) = goal_filter.as_ref() {
if !filter.contains(&vp.property.name) {
continue;
}
}
let mut visited: HashSet<Arc<str>> = HashSet::default();
let resolved_type = DataModelIr::wire_property_type(
&vp.property,
entities_map,
&unit.dictionaries,
&mut visited,
);
out.push(Global {
name: vp.property.name.clone(),
resolved_type,
origin: FieldOrigin::Schema {
source: vp.policy_path,
kind: vp.property.kind.to_schema_field_kind(vp.property.array),
},
});
}
let mut seen: HashSet<Arc<str>> = out.iter().map(|g| g.name.clone()).collect();
let enriched = self.enriched_of_unit(&unit);
for (path, owner, node) in unit.dep_graph.computed_in(visible) {
if path.contains('.') {
continue;
}
if !seen.insert(path.clone()) {
continue;
}
if let Some(filter) = goal_filter.as_ref() {
if !filter.contains(path) {
continue;
}
}
out.push(Global {
name: path.clone(),
resolved_type: node.resolved_type_in(&enriched.scope, path),
origin: FieldOrigin::Computed {
written_by: owner.clone(),
instance_of: unit.computed_instances.get(path).cloned(),
},
});
}
out.sort_by(|a, b| a.name.cmp(&b.name));
out
}
pub fn dictionaries(&self, req: &ScopeRequest) -> Vec<Dictionary> {
if self.is_graph(&req.policy_path) {
return self
.graph_dictionary_blocks(&self.graph_imports(&req.policy_path))
.iter()
.map(Self::dictionary_of)
.collect();
}
let unit = self.unit(&req.policy_path);
let mut seen: HashSet<Arc<str>> = HashSet::default();
let mut out: Vec<Dictionary> = Vec::new();
for entry in &unit.dictionary_blocks {
if !seen.insert(entry.ir.name.clone()) {
continue;
}
out.push(Self::dictionary_of(entry));
}
out
}
fn dictionary_of(entry: &DictionaryUnitEntry) -> Dictionary {
Dictionary {
name: entry.ir.name.clone(),
source: entry.policy_path.clone(),
entries: entry
.ir
.entries
.iter()
.map(|e| DictionaryEntryInfo {
value: e.value.clone(),
label: e.label.clone(),
})
.collect(),
}
}
pub fn inputs(&self, req: &ScopeRequest) -> Vec<InputProperty> {
if self.is_graph(&req.policy_path) {
return self.graph_inputs(&req.policy_path);
}
let unit = self.unit(&req.policy_path);
let visible = &unit.members;
let entities = &unit.entities;
let (root_entities, ref_targets) = self.classify_root_entities(visible);
let mut result: Vec<InputProperty> = self
.walk_visible_properties(&req.policy_path)
.into_iter()
.filter(|vp| match &vp.scope {
PropertyScope::Entity(entity) => root_entities.contains(entity),
PropertyScope::Global => true,
})
.map(|vp| {
let mut visited: HashSet<Arc<str>> = HashSet::default();
InputProperty {
path: vp.dotted_path(),
resolved_type: DataModelIr::wire_property_type(
&vp.property,
entities,
&unit.dictionaries,
&mut visited,
),
}
})
.collect();
for target in &ref_targets {
if !entities.contains_key(target) {
continue;
}
let mut visited: HashSet<Arc<str>> = HashSet::default();
let entity_type =
DataModelIr::wire_object(target, entities, &unit.dictionaries, &mut visited);
if !matches!(entity_type, VariableType::Any) {
result.push(InputProperty {
path: target.clone(),
resolved_type: entity_type.array(),
});
}
}
if !req.goals.is_empty() {
let reachable = self.goal_reachable_input_paths(&unit.dep_graph, &req.goals, visible);
result.retain(|p| {
reachable.iter().any(|r| {
PathPrefix::extends(p.path.as_ref(), r.as_ref())
|| PathPrefix::extends(r.as_ref(), p.path.as_ref())
})
});
}
result.sort_by(|a, b| a.path.cmp(&b.path));
result
}
pub fn outputs(&self, req: &ScopeRequest) -> Vec<OutputProperty> {
if self.is_graph(&req.policy_path) {
return self.graph_outputs(&req.policy_path);
}
let unit = self.unit(&req.policy_path);
let enriched = self.enriched_of_unit(&unit);
let goal_filter =
(!req.goals.is_empty()).then(|| unit.dep_graph.reachable_from(&req.goals));
let mut result: Vec<OutputProperty> = unit
.dep_graph
.computed_in(&unit.members)
.filter(|(path, _, _)| goal_filter.as_ref().is_none_or(|f| f.contains(*path)))
.map(|(path, owner, node)| OutputProperty {
path: path.clone(),
resolved_type: node.resolved_type_in(&enriched.scope, path),
kind: PropertyKind::Computed,
written_by: Some(owner.clone()),
instance_of: unit.computed_instances.get(path).cloned(),
})
.collect();
result.sort_by(|a, b| a.path.cmp(&b.path));
result
}
fn walk_schema_fields(&self, req: &ScopeRequest) -> Vec<(Arc<str>, EntityField)> {
let unit = self.unit(&req.policy_path);
let entities = &unit.entities;
self.walk_visible_properties(&req.policy_path)
.into_iter()
.filter_map(|vp| {
let PropertyScope::Entity(entity) = vp.scope else {
return None;
};
let mut visited: HashSet<Arc<str>> = HashSet::default();
let resolved_type = DataModelIr::wire_property_type(
&vp.property,
entities,
&unit.dictionaries,
&mut visited,
);
let origin = FieldOrigin::Schema {
source: vp.policy_path,
kind: vp.property.kind.to_schema_field_kind(vp.property.array),
};
Some((
entity,
EntityField {
name: vp.property.name,
resolved_type,
origin,
},
))
})
.collect()
}
fn walk_computed_fields(&self, req: &ScopeRequest) -> Vec<(Arc<str>, EntityField)> {
let unit = self.unit(&req.policy_path);
let enriched = self.enriched_of_unit(&unit);
let mut sorted: Vec<(&Arc<str>, &crate::workspace::types::BlockRef, &_)> =
unit.dep_graph.computed_in(&unit.members).collect();
sorted.sort_by(|a, b| a.0.cmp(b.0));
let mut seen: HashSet<(Arc<str>, Arc<str>)> = HashSet::default();
sorted
.into_iter()
.filter_map(|(path, owner, node)| {
let (entity, name) = path.split_once('.')?;
let entity: Arc<str> = Arc::from(entity);
let name: Arc<str> = Arc::from(name);
if !seen.insert((entity.clone(), name.clone())) {
return None;
}
Some((
entity,
EntityField {
name,
resolved_type: node.resolved_type_in(&enriched.scope, path),
origin: FieldOrigin::Computed {
written_by: owner.clone(),
instance_of: unit.computed_instances.get(path).cloned(),
},
},
))
})
.collect()
}
fn classify_root_entities(
&self,
visible: &HashSet<Arc<str>>,
) -> (HashSet<Arc<str>>, HashSet<Arc<str>>) {
let parsed: Vec<_> = visible.iter().filter_map(|pp| self.parsed(pp)).collect();
let models = parsed
.iter()
.flat_map(|p| p.policy.data_models().map(|(_, dm)| dm));
DataModelIr::classify_roots(models)
}
pub(crate) fn goal_reachable_input_paths(
&self,
graph: &DependencyGraph,
goals: &[Arc<str>],
visible: &HashSet<Arc<str>>,
) -> HashSet<Arc<str>> {
graph
.reachable_from(goals)
.iter()
.filter(|p| {
graph.node_map.get(p.as_ref()).is_some_and(|&idx| {
match &graph.graph[idx].written_by {
None => true,
Some(owner) => !visible.contains(&owner.policy_path),
}
})
})
.cloned()
.collect()
}
fn goal_reachable_entities(
&self,
graph: &DependencyGraph,
goals: &[Arc<str>],
) -> HashSet<Arc<str>> {
graph
.reachable_from(goals)
.iter()
.filter_map(|p| p.split('.').next().map(Arc::<str>::from))
.collect()
}
}