use std::collections::{HashMap, HashSet};
use crate::core::ir::{FieldDef, TypeDef, TypeRef};
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::Assertion;
pub(crate) const ROOT_TYPE: &str = "Report";
pub(crate) const ENVELOPE_ROOT_TYPE: &str = "Envelope";
pub(crate) const WILDCARD_FIELD: &str = "records[].kind";
pub(crate) const WILDCARD_NAME_FIELD: &str = "records[].name";
fn report_entry_type_defs() -> Vec<TypeDef> {
vec![
TypeDef {
name: ROOT_TYPE.to_string(),
fields: vec![FieldDef {
name: "records".to_string(),
ty: TypeRef::Vec(Box::new(TypeRef::Named("Entry".to_string()))),
..FieldDef::default()
}],
..TypeDef::default()
},
TypeDef {
name: "Entry".to_string(),
fields: vec![
FieldDef {
name: "kind".to_string(),
ty: TypeRef::String,
..FieldDef::default()
},
FieldDef {
name: "name".to_string(),
ty: TypeRef::String,
..FieldDef::default()
},
],
..TypeDef::default()
},
]
}
pub(crate) fn report_resolver(language: &str) -> FieldResolver {
let type_defs = report_entry_type_defs();
let result_field_map = FieldResolver::ir_result_field_facts(&type_defs, language);
let collection_map = FieldResolver::ir_collection_fields(&type_defs);
let (reachable, excluded, optional) = FieldResolver::ir_field_sets(&type_defs);
let result_fields = HashSet::from(["records".to_string()]);
FieldResolver::new(
&HashMap::new(),
&HashSet::new(),
&result_fields,
&HashSet::new(),
&HashSet::new(),
)
.with_ir_result_fields(result_field_map, Some(ROOT_TYPE.to_string()))
.with_ir_collection_map(collection_map, Some(ROOT_TYPE.to_string()))
.with_ir_fields(reachable, excluded, optional)
}
fn envelope_report_entry_type_defs() -> Vec<TypeDef> {
let mut type_defs = vec![TypeDef {
name: ENVELOPE_ROOT_TYPE.to_string(),
fields: vec![FieldDef {
name: "results".to_string(),
ty: TypeRef::Vec(Box::new(TypeRef::Named(ROOT_TYPE.to_string()))),
..FieldDef::default()
}],
..TypeDef::default()
}];
type_defs.extend(report_entry_type_defs());
type_defs
}
pub(crate) fn envelope_resolver(language: &str) -> FieldResolver {
let type_defs = envelope_report_entry_type_defs();
let result_field_map = FieldResolver::ir_result_field_facts(&type_defs, language);
let collection_map = FieldResolver::ir_collection_fields(&type_defs);
let (reachable, excluded, optional) = FieldResolver::ir_field_sets(&type_defs);
let result_fields = HashSet::from(["results".to_string()]);
FieldResolver::new(
&HashMap::new(),
&HashSet::new(),
&result_fields,
&HashSet::new(),
&HashSet::new(),
)
.with_ir_result_fields(result_field_map, Some(ENVELOPE_ROOT_TYPE.to_string()))
.with_ir_collection_map(collection_map, Some(ENVELOPE_ROOT_TYPE.to_string()))
.with_ir_fields(reachable, excluded, optional)
}
pub(crate) fn assert_container_accessor_appears_once(rendered: &str, container_accessor: &str) {
let occurrences = rendered.matches(container_accessor).count();
assert_eq!(
occurrences, 1,
"`{container_accessor}` must appear once (the iteration expression) and not a second time \
inside the closure body, got {occurrences} in: {rendered}"
);
}
pub(crate) fn contains_assertion(field: &str) -> Assertion {
Assertion {
assertion_type: "contains".to_string(),
field: Some(field.to_string()),
value: Some(serde_json::json!("Heading")),
..Default::default()
}
}
pub(crate) fn not_empty_assertion(field: &str) -> Assertion {
Assertion {
assertion_type: "not_empty".to_string(),
field: Some(field.to_string()),
..Default::default()
}
}
pub(crate) fn assert_element_relative(rendered: &str, element_body: &str, container_reapplied: &str) {
assert!(
!rendered.contains(container_reapplied),
"closure body must not re-apply the container path to the element binding \
(`{container_reapplied}` addresses a member the element type does not declare), got: {rendered}"
);
assert!(
rendered.contains(element_body),
"closure body must address the element binding's own field (`{element_body}`), got: {rendered}"
);
}
pub(crate) fn optional_name_resolver(language: &str) -> FieldResolver {
let mut types = report_entry_type_defs();
types[0].fields.push(FieldDef {
name: "name".into(),
ty: TypeRef::String,
..FieldDef::default()
});
types[1].fields[1].optional = true;
let (reachable, excluded, optional) = FieldResolver::ir_field_sets(&types);
let resolver = FieldResolver::new(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
)
.with_ir_result_fields(
FieldResolver::ir_result_field_facts(&types, language),
Some(ROOT_TYPE.into()),
)
.with_ir_collection_map(FieldResolver::ir_collection_fields(&types), Some(ROOT_TYPE.into()))
.with_ir_fields(reachable, excluded, optional);
assert!(!resolver.is_optional("name"));
assert!(resolver.is_optional(WILDCARD_NAME_FIELD));
if language == "swift" {
resolver.with_swift_first_class_map(crate::e2e::field_access::SwiftFirstClassMap {
first_class_types: HashSet::from([ROOT_TYPE.into(), "Entry".into()]),
field_types: HashMap::from([(ROOT_TYPE.into(), HashMap::from([("records".into(), "Entry".into())]))]),
root_type: Some(ROOT_TYPE.into()),
..Default::default()
})
} else {
resolver
}
}
pub(crate) fn compile_and_run_wildcard_program(language: &str, source: &str) {
let directory = tempfile::tempdir().expect("create compiler test directory");
let path = directory.path().join(format!("wildcard.{language}"));
std::fs::write(&path, source).expect("write generated assertion harness");
let mut command = std::process::Command::new(language);
if language == "go" {
command.arg("run");
}
let output = command.arg(&path).output().expect("start language toolchain");
assert!(
output.status.success(),
"{}\n{}\n{source}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
pub(crate) fn wildcard_runtime_cases(language: &str) -> Vec<(Assertion, Option<&'static str>, bool)> {
let mut cases = Vec::new();
for name in [None, Some("t"), Some("other"), Some("")] {
for needle in ["", "t", "missing"] {
for kind in [
"contains",
"not_contains",
"not_contains_multi",
"contains_all",
"not_empty",
] {
let matches =
name.is_some_and(|name| name.contains(needle) && (language == "go" || !needle.is_empty()));
let passes = match kind {
"not_contains" => !matches,
"not_contains_multi" => !matches && !name.is_some_and(|name| name.contains("t")),
"contains_all" => matches && name.is_some_and(|name| name.contains("t")),
"not_empty" => name.is_some_and(|name| !name.is_empty()),
_ => matches,
};
cases.push((
Assertion {
assertion_type: if kind == "not_contains_multi" {
"not_contains"
} else {
kind
}
.into(),
field: Some(WILDCARD_NAME_FIELD.into()),
value: (!["contains_all", "not_contains_multi"].contains(&kind))
.then(|| serde_json::json!(needle)),
values: ["contains_all", "not_contains_multi"]
.contains(&kind)
.then(|| vec![serde_json::json!(needle), serde_json::json!("t")]),
..Default::default()
},
name,
passes,
));
}
}
}
assert_eq!(cases.len(), 60);
cases
}