use std::collections::{HashMap, HashSet};
use crate::core::ir::TypeDef;
use crate::e2e::codegen::call_ir::named_type;
use super::parse::{parse_path, segment_name};
use super::types::IrCollectionMap;
pub(super) fn build_ir_collection_map(type_defs: &[TypeDef]) -> IrCollectionMap {
let struct_names: HashSet<&str> = type_defs.iter().map(|t| t.name.as_str()).collect();
let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
let mut collection_fields: HashMap<String, HashSet<String>> = HashMap::new();
for type_def in type_defs {
for field in &type_def.fields {
if is_vec_type(&field.ty) {
collection_fields
.entry(type_def.name.clone())
.or_default()
.insert(field.name.clone());
}
let Some(named) = named_type(&field.ty) else {
continue;
};
if struct_names.contains(named) {
field_types
.entry(type_def.name.clone())
.or_default()
.insert(field.name.clone(), named.to_string());
}
}
}
IrCollectionMap {
field_types,
collection_fields,
root_type: None,
}
}
fn has_non_string_scalar_elements(ty: &crate::core::ir::TypeRef) -> bool {
match ty {
crate::core::ir::TypeRef::Optional(inner) => has_non_string_scalar_elements(inner),
crate::core::ir::TypeRef::Vec(element) => is_non_string_scalar(element),
_ => false,
}
}
fn is_non_string_scalar(ty: &crate::core::ir::TypeRef) -> bool {
match ty {
crate::core::ir::TypeRef::Optional(inner) => is_non_string_scalar(inner),
crate::core::ir::TypeRef::Primitive(_) | crate::core::ir::TypeRef::Char => true,
_ => false,
}
}
pub(super) fn is_vec_type(ty: &crate::core::ir::TypeRef) -> bool {
match ty {
crate::core::ir::TypeRef::Vec(_) => true,
crate::core::ir::TypeRef::Optional(inner) => is_vec_type(inner),
_ => false,
}
}
pub(super) fn is_collection_path(map: &IrCollectionMap, path: &str) -> bool {
let Some(root) = map.root_type.as_deref() else {
return false;
};
is_collection_path_from(map, root, path)
}
pub(super) fn is_collection_path_from(map: &IrCollectionMap, root: &str, path: &str) -> bool {
let segments = parse_path(path);
let Some((last, prefix)) = segments.split_last() else {
return false;
};
let mut owner = root;
for segment in prefix {
let Some(name) = segment_name(segment) else {
return false;
};
match map.field_types.get(owner).and_then(|fields| fields.get(name)) {
Some(next) => owner = next.as_str(),
None => return false,
}
}
let Some(name) = segment_name(last) else {
return false;
};
map.collection_fields
.get(owner)
.is_some_and(|fields| fields.contains(name))
}
pub(super) fn element_type_at_path(map: &IrCollectionMap, path: &str) -> Option<String> {
let root = map.root_type.as_deref()?;
let segments = parse_path(path);
let mut owner = root;
for segment in &segments {
let name = segment_name(segment)?;
owner = map.field_types.get(owner)?.get(name)?.as_str();
}
Some(owner.to_string())
}
pub(super) fn build_non_string_scalar_element_fields(type_defs: &[TypeDef]) -> HashMap<String, HashSet<String>> {
let mut fields: HashMap<String, HashSet<String>> = HashMap::new();
for type_def in type_defs {
for field in &type_def.fields {
if has_non_string_scalar_elements(&field.ty) {
fields
.entry(type_def.name.clone())
.or_default()
.insert(field.name.clone());
}
}
}
fields
}
pub(super) fn has_non_string_scalar_elements_at_path(
map: &IrCollectionMap,
fields: &HashMap<String, HashSet<String>>,
path: &str,
) -> bool {
let Some(root) = map.root_type.as_deref() else {
return false;
};
let segments = parse_path(path);
let Some((last, prefix)) = segments.split_last() else {
return false;
};
let mut owner = root;
for segment in prefix {
let Some(name) = segment_name(segment) else {
return false;
};
match map.field_types.get(owner).and_then(|fields| fields.get(name)) {
Some(next) => owner = next.as_str(),
None => return false,
}
}
let Some(name) = segment_name(last) else {
return false;
};
fields.get(owner).is_some_and(|fields| fields.contains(name))
}
#[cfg(test)]
mod element_type_at_path_tests {
use super::*;
use crate::core::ir::{FieldDef, TypeDef, TypeRef};
fn field(name: &str, ty: TypeRef) -> FieldDef {
FieldDef {
name: name.to_string(),
ty,
..FieldDef::default()
}
}
fn type_defs() -> Vec<TypeDef> {
vec![
TypeDef {
name: "Container".to_string(),
fields: vec![field("rows", TypeRef::Vec(Box::new(TypeRef::Named("Row".to_string()))))],
..TypeDef::default()
},
TypeDef {
name: "Row".to_string(),
fields: vec![field("values", TypeRef::Vec(Box::new(TypeRef::String)))],
..TypeDef::default()
},
]
}
fn anchored_map() -> IrCollectionMap {
let mut map = build_ir_collection_map(&type_defs());
map.root_type = Some("Container".to_string());
map
}
#[test]
fn a_vec_field_resolves_to_its_element_type() {
assert_eq!(element_type_at_path(&anchored_map(), "rows"), Some("Row".to_string()));
}
#[test]
fn an_indexed_vec_field_resolves_the_same_way() {
assert_eq!(
element_type_at_path(&anchored_map(), "rows[0]"),
Some("Row".to_string())
);
}
#[test]
fn an_unknown_field_resolves_to_nothing() {
assert_eq!(element_type_at_path(&anchored_map(), "not_a_real_field"), None);
}
#[test]
fn no_anchored_root_resolves_to_nothing() {
let map = build_ir_collection_map(&type_defs());
assert_eq!(element_type_at_path(&map, "rows"), None);
}
}
#[cfg(test)]
mod non_string_scalar_element_tests {
use super::*;
use crate::core::ir::{FieldDef, PrimitiveType, TypeDef, TypeRef};
fn field(name: &str, ty: TypeRef) -> FieldDef {
FieldDef {
name: name.to_string(),
ty,
..FieldDef::default()
}
}
fn vec_of(inner: TypeRef) -> TypeRef {
TypeRef::Vec(Box::new(inner))
}
fn type_defs() -> Vec<TypeDef> {
vec![
TypeDef {
name: "Container".to_string(),
fields: vec![
field("codes", vec_of(TypeRef::Primitive(PrimitiveType::U32))),
field("ratios", vec_of(TypeRef::Primitive(PrimitiveType::F64))),
field("flags", vec_of(TypeRef::Primitive(PrimitiveType::Bool))),
field("initials", vec_of(TypeRef::Char)),
field(
"optional_codes",
TypeRef::Optional(Box::new(vec_of(TypeRef::Primitive(PrimitiveType::I64)))),
),
field("warnings", vec_of(TypeRef::String)),
field("rows", vec_of(TypeRef::Named("Row".to_string()))),
field("title", TypeRef::String),
],
..TypeDef::default()
},
TypeDef {
name: "Row".to_string(),
fields: vec![field("scores", vec_of(TypeRef::Primitive(PrimitiveType::U8)))],
..TypeDef::default()
},
]
}
fn anchored_map() -> IrCollectionMap {
let mut map = build_ir_collection_map(&type_defs());
map.root_type = Some("Container".to_string());
map
}
fn element_facts() -> HashMap<String, HashSet<String>> {
build_non_string_scalar_element_fields(&type_defs())
}
#[test]
fn a_numeric_collection_is_recognised_as_a_non_string_scalar_element() {
assert!(has_non_string_scalar_elements_at_path(
&anchored_map(),
&element_facts(),
"codes"
));
assert!(has_non_string_scalar_elements_at_path(
&anchored_map(),
&element_facts(),
"ratios"
));
}
#[test]
fn boolean_and_char_collections_are_recognised_too() {
assert!(has_non_string_scalar_elements_at_path(
&anchored_map(),
&element_facts(),
"flags"
));
assert!(has_non_string_scalar_elements_at_path(
&anchored_map(),
&element_facts(),
"initials"
));
}
#[test]
fn an_optional_numeric_collection_is_seen_through_its_option() {
assert!(has_non_string_scalar_elements_at_path(
&anchored_map(),
&element_facts(),
"optional_codes"
));
}
#[test]
fn a_string_collection_is_not_a_non_string_scalar() {
assert!(!has_non_string_scalar_elements_at_path(
&anchored_map(),
&element_facts(),
"warnings"
));
assert_eq!(element_type_at_path(&anchored_map(), "warnings"), None);
}
#[test]
fn a_struct_collection_is_not_a_non_string_scalar() {
assert!(!has_non_string_scalar_elements_at_path(
&anchored_map(),
&element_facts(),
"rows"
));
}
#[test]
fn a_scalar_field_that_is_not_a_collection_is_not_one_either() {
assert!(!has_non_string_scalar_elements_at_path(
&anchored_map(),
&element_facts(),
"title"
));
}
#[test]
fn a_nested_numeric_collection_is_resolved_through_its_owner() {
assert!(has_non_string_scalar_elements_at_path(
&anchored_map(),
&element_facts(),
"rows.scores"
));
}
#[test]
fn an_unknown_field_and_an_unanchored_map_both_answer_no() {
assert!(!has_non_string_scalar_elements_at_path(
&anchored_map(),
&element_facts(),
"not_a_real_field"
));
let unanchored = build_ir_collection_map(&type_defs());
assert!(!has_non_string_scalar_elements_at_path(
&unanchored,
&element_facts(),
"codes"
));
}
}