use std::collections::{HashMap, HashSet};
use crate::core::config::e2e::CallConfig;
use crate::core::ir::{EnumDef, EnumVariant, FieldDef, TypeDef, TypeRef};
use crate::e2e::fixture::Fixture;
#[derive(Clone, Copy)]
enum NamedDef<'a> {
Struct(&'a TypeDef),
Enum(&'a EnumDef),
}
type NamedIndex<'a> = HashMap<&'a str, NamedDef<'a>>;
fn build_named_index<'a>(type_defs: &'a [TypeDef], enums: &'a [EnumDef]) -> NamedIndex<'a> {
let mut index = HashMap::with_capacity(type_defs.len() + enums.len());
for definition in type_defs {
index.insert(definition.name.as_str(), NamedDef::Struct(definition));
}
for definition in enums {
index.insert(definition.name.as_str(), NamedDef::Enum(definition));
}
index
}
pub(super) struct FileInputScan<'a> {
index: NamedIndex<'a>,
}
impl<'a> FileInputScan<'a> {
pub(super) fn new(type_defs: &'a [TypeDef], enums: &'a [EnumDef]) -> Self {
Self {
index: build_named_index(type_defs, enums),
}
}
pub(super) fn fixture_uses_test_documents(&self, fixture: &Fixture, call: &CallConfig) -> bool {
scan_fixture(&self.index, fixture, call).0
}
}
#[cfg(test)]
pub(super) fn fixture_uses_test_documents(
fixture: &Fixture,
call: &CallConfig,
type_defs: &[TypeDef],
enums: &[EnumDef],
) -> bool {
FileInputScan::new(type_defs, enums).fixture_uses_test_documents(fixture, call)
}
fn scan_fixture(index: &NamedIndex<'_>, fixture: &Fixture, call: &CallConfig) -> (bool, usize) {
let mut walk = DocumentWalk::new(index);
let found = fixture.resolved_args(call).iter().any(|argument| {
if !fixture.docs_files_for_arg(&argument.field).is_empty() || argument.arg_type == "file_path" {
return true;
}
let value = super::resolve_field(&fixture.input, &argument.field);
if argument.arg_type == "bytes" {
return value.as_str().is_some_and(is_relative_document_path);
}
if argument.arg_type != "json_object" {
return false;
}
let Some(element_type) = argument.element_type.as_deref() else {
return false;
};
match value.as_array() {
Some(values) => values.iter().any(|element| walk.resolve_named(element, element_type)),
None => walk.resolve_named(value, element_type),
}
});
(found, walk.named_resolutions)
}
fn value_identity(value: &serde_json::Value) -> usize {
std::ptr::from_ref(value).addr()
}
struct DocumentWalk<'defs, 'index> {
index: &'index NamedIndex<'defs>,
active: HashSet<&'defs str>,
memo: HashMap<(usize, &'defs str), bool>,
cycle_cut: bool,
named_resolutions: usize,
}
impl<'defs, 'index> DocumentWalk<'defs, 'index> {
fn new(index: &'index NamedIndex<'defs>) -> Self {
Self {
index,
active: HashSet::new(),
memo: HashMap::new(),
cycle_cut: false,
named_resolutions: 0,
}
}
fn typed_value(&mut self, value: &serde_json::Value, ty: &TypeRef) -> bool {
match ty {
TypeRef::Bytes => value.as_str().is_some_and(is_relative_document_path),
TypeRef::Optional(inner) => self.typed_value(value, inner),
TypeRef::Vec(inner) => value
.as_array()
.is_some_and(|values| values.iter().any(|value| self.typed_value(value, inner))),
TypeRef::Map(_, value_type) => value
.as_object()
.is_some_and(|values| values.values().any(|value| self.typed_value(value, value_type))),
TypeRef::Named(name) => self.resolve_named(value, name),
_ => false,
}
}
fn resolve_named(&mut self, value: &serde_json::Value, name: &str) -> bool {
self.named_resolutions += 1;
let index = self.index;
let Some((&definition_name, definition)) = index.get_key_value(name) else {
return false;
};
let key = (value_identity(value), definition_name);
if let Some(&cached) = self.memo.get(&key) {
return cached;
}
if !self.active.insert(definition_name) {
self.cycle_cut = true;
return false;
}
let outer_cut = std::mem::replace(&mut self.cycle_cut, false);
let found = match *definition {
NamedDef::Struct(struct_def) => {
self.fields(value, &struct_def.fields, struct_def.serde_rename_all.as_deref())
}
NamedDef::Enum(enum_def) => self.enum_value(value, enum_def),
};
let inner_cut = self.cycle_cut;
self.cycle_cut = outer_cut || inner_cut;
self.active.remove(definition_name);
if !inner_cut {
self.memo.insert(key, found);
}
found
}
fn fields(&mut self, value: &serde_json::Value, fields: &[FieldDef], rename_all: Option<&str>) -> bool {
let Some(object) = value.as_object() else {
return false;
};
fields.iter().any(|field| {
if field.serde_flatten {
return self.typed_value(value, &field.ty);
}
let wire_name =
crate::codegen::naming::wire_field_name(&field.name, field.serde_rename.as_deref(), rename_all);
let Some(field_value) = object.get(&field.name).or_else(|| object.get(&wire_name)) else {
return false;
};
self.typed_value(field_value, &field.ty)
})
}
fn enum_value(&mut self, value: &serde_json::Value, definition: &EnumDef) -> bool {
definition
.variants
.iter()
.any(|variant| self.variant(value, definition, variant))
}
fn variant(&mut self, value: &serde_json::Value, definition: &EnumDef, variant: &EnumVariant) -> bool {
let Some(candidate) = variant_payload(value, definition, variant) else {
return false;
};
if variant.is_tuple {
return self.tuple_variant(candidate, &variant.fields);
}
self.fields(candidate, &variant.fields, None)
}
fn tuple_variant(&mut self, candidate: &serde_json::Value, fields: &[FieldDef]) -> bool {
if let [only] = fields {
return self.typed_value(candidate, &only.ty);
}
let Some(values) = candidate.as_array() else {
return false;
};
fields
.iter()
.zip(values.iter())
.any(|(field, value)| self.typed_value(value, &field.ty))
}
}
fn variant_payload<'a>(
value: &'a serde_json::Value,
definition: &EnumDef,
variant: &EnumVariant,
) -> Option<&'a serde_json::Value> {
if definition.serde_untagged {
return Some(value);
}
let Some(tag_key) = &definition.serde_tag else {
let wire_name = crate::codegen::naming::wire_variant_value(
&variant.name,
variant.serde_rename.as_deref(),
definition.serde_rename_all.as_deref(),
);
return value.get(&wire_name);
};
if !tag_matches_variant(value, tag_key, definition, variant) {
return None;
}
match &definition.serde_content {
Some(content_key) => value.get(content_key),
None => Some(value),
}
}
fn tag_matches_variant(value: &serde_json::Value, tag_key: &str, definition: &EnumDef, variant: &EnumVariant) -> bool {
let wire_name = crate::codegen::naming::wire_variant_value(
&variant.name,
variant.serde_rename.as_deref(),
definition.serde_rename_all.as_deref(),
);
value.get(tag_key).and_then(serde_json::Value::as_str) == Some(wire_name.as_str())
}
fn is_relative_document_path(value: &str) -> bool {
if value.starts_with('<') || value.starts_with('{') || value.starts_with('[') || value.contains(' ') {
return false;
}
let first = value.chars().next().unwrap_or('\0');
if !first.is_ascii_alphanumeric() && first != '_' {
return false;
}
value
.find('/')
.map(|slash| &value[slash + 1..])
.is_some_and(|suffix| !suffix.is_empty() && suffix.contains('.'))
}
#[cfg(test)]
mod cycle_guard_tests;
#[cfg(test)]
mod cycle_memo_taint_tests;
#[cfg(test)]
mod tag_and_shape_tests;
#[cfg(test)]
mod tag_value_discrimination_tests;
#[cfg(test)]
mod variant_field_rename_all_tests;
#[cfg(test)]
mod wide_dag_memo_tests;
#[cfg(test)]
mod tests {
use crate::core::config::e2e::{ArgMapping, CallConfig};
use crate::core::ir::{EnumDef, EnumVariant, FieldDef, TypeDef, TypeRef};
use crate::e2e::fixture::Fixture;
fn object_arg() -> ArgMapping {
ArgMapping {
name: "request".into(),
field: "input".into(),
arg_type: "json_object".into(),
optional: false,
owned: true,
element_type: Some("SampleRequest".into()),
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
}
}
fn request_type() -> TypeDef {
TypeDef {
name: "SampleRequest".into(),
fields: vec![FieldDef {
name: "content".into(),
ty: TypeRef::Bytes,
..Default::default()
}],
..Default::default()
}
}
#[test]
fn nested_bytes_file_path_requires_test_document_working_directory() {
let fixture = Fixture {
input: serde_json::json!({"content": "documents/sample.bin"}),
..Default::default()
};
let call = CallConfig {
args: vec![object_arg()],
..Default::default()
};
assert!(super::fixture_uses_test_documents(
&fixture,
&call,
&[request_type()],
&[]
));
}
#[test]
fn nested_inline_bytes_do_not_require_test_document_working_directory() {
let fixture = Fixture {
input: serde_json::json!({"content": "inline text"}),
..Default::default()
};
let call = CallConfig {
args: vec![object_arg()],
..Default::default()
};
assert!(!super::fixture_uses_test_documents(
&fixture,
&call,
&[request_type()],
&[]
));
}
#[test]
fn batch_nested_bytes_file_path_requires_test_document_working_directory() {
let mut argument = object_arg();
argument.field = "input.requests".into();
let fixture = Fixture {
input: serde_json::json!({
"requests": [
{"content": "inline text"},
{"content": "documents/sample.bin"}
]
}),
..Default::default()
};
let call = CallConfig {
args: vec![argument],
..Default::default()
};
assert!(super::fixture_uses_test_documents(
&fixture,
&call,
&[request_type()],
&[]
));
}
fn event_enum() -> EnumDef {
EnumDef {
name: "SampleEvent".into(),
variants: vec![EnumVariant {
name: "Uploaded".into(),
fields: vec![FieldDef {
name: "file".into(),
ty: TypeRef::Bytes,
..Default::default()
}],
..Default::default()
}],
..Default::default()
}
}
fn request_with_event_type() -> TypeDef {
TypeDef {
name: "SampleRequest".into(),
fields: vec![FieldDef {
name: "event".into(),
ty: TypeRef::Named("SampleEvent".into()),
..Default::default()
}],
..Default::default()
}
}
#[test]
fn enum_variant_payload_bytes_file_path_requires_test_document_working_directory() {
let fixture = Fixture {
input: serde_json::json!({"event": {"Uploaded": {"file": "documents/sample.bin"}}}),
..Default::default()
};
let call = CallConfig {
args: vec![object_arg()],
..Default::default()
};
assert!(super::fixture_uses_test_documents(
&fixture,
&call,
&[request_with_event_type()],
&[event_enum()]
));
}
#[test]
fn enum_variant_payload_inline_bytes_do_not_require_test_document_working_directory() {
let fixture = Fixture {
input: serde_json::json!({"event": {"Uploaded": {"file": "inline text"}}}),
..Default::default()
};
let call = CallConfig {
args: vec![object_arg()],
..Default::default()
};
assert!(!super::fixture_uses_test_documents(
&fixture,
&call,
&[request_with_event_type()],
&[event_enum()]
));
}
#[test]
fn enum_variant_mismatched_tag_key_does_not_require_test_document_working_directory() {
let fixture = Fixture {
input: serde_json::json!({"event": {"SomethingElse": {"file": "documents/sample.bin"}}}),
..Default::default()
};
let call = CallConfig {
args: vec![object_arg()],
..Default::default()
};
assert!(!super::fixture_uses_test_documents(
&fixture,
&call,
&[request_with_event_type()],
&[event_enum()]
));
}
fn flattened_details_type() -> TypeDef {
TypeDef {
name: "SampleDetails".into(),
fields: vec![FieldDef {
name: "attachment".into(),
ty: TypeRef::Bytes,
..Default::default()
}],
..Default::default()
}
}
fn request_with_flattened_details() -> TypeDef {
TypeDef {
name: "SampleRequest".into(),
fields: vec![FieldDef {
name: "details".into(),
ty: TypeRef::Named("SampleDetails".into()),
serde_flatten: true,
..Default::default()
}],
..Default::default()
}
}
#[test]
fn flattened_named_field_bytes_file_path_requires_test_document_working_directory() {
let fixture = Fixture {
input: serde_json::json!({"attachment": "documents/sample.bin"}),
..Default::default()
};
let call = CallConfig {
args: vec![object_arg()],
..Default::default()
};
assert!(super::fixture_uses_test_documents(
&fixture,
&call,
&[request_with_flattened_details(), flattened_details_type()],
&[]
));
}
}
#[cfg(test)]
mod docs_presentation_array_scan_tests {
use crate::core::config::e2e::{ArgMapping, CallConfig};
use crate::core::ir::{FieldDef, TypeDef, TypeRef};
use crate::e2e::fixture::Fixture;
fn batch_arg() -> ArgMapping {
ArgMapping {
name: "inputs".into(),
field: "input.inputs".into(),
arg_type: "json_object".into(),
optional: false,
owned: true,
element_type: Some("ExtractInput".into()),
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
}
}
fn extract_input_type() -> TypeDef {
TypeDef {
name: "ExtractInput".into(),
fields: vec![FieldDef {
name: "bytes".into(),
ty: TypeRef::Bytes,
..Default::default()
}],
..Default::default()
}
}
#[test]
fn docs_presentation_file_nested_in_a_batch_array_element_requires_test_document_working_directory() {
let fixture_json = serde_json::json!({
"id": "extract_batch_bytes_happy",
"input": {
"inputs": [
{"kind": "bytes", "bytes": [72, 101], "mime_type": "text/plain"},
{"kind": "bytes", "bytes": [60, 104, 116], "mime_type": "text/html"}
]
},
"docs": {
"topic": "batch",
"presentation": {
"files": [{"field": "/inputs/1/bytes", "path": "html/html.html"}]
}
}
});
let fixture: Fixture = serde_json::from_value(fixture_json).expect("parse fixture");
assert_eq!(
fixture.docs_files_for_arg("input.inputs"),
vec![crate::e2e::fixture::FixtureDocsFileInput {
field: "/1/bytes".to_string(),
path: "html/html.html".to_string(),
}],
"the nested doc file entry must resolve relative to the argument's own field prefix"
);
let call = CallConfig {
args: vec![batch_arg()],
..Default::default()
};
assert!(
super::fixture_uses_test_documents(&fixture, &call, &[extract_input_type()], &[]),
"a docs.presentation.files entry nested under an array element must still require \
the test-documents working directory"
);
}
#[test]
fn inline_bytes_with_no_docs_presentation_file_does_not_require_test_document_working_directory() {
let fixture_json = serde_json::json!({
"id": "extract_batch_bytes_inline_only",
"input": {
"inputs": [{"kind": "bytes", "bytes": [72, 101], "mime_type": "text/plain"}]
}
});
let fixture: Fixture = serde_json::from_value(fixture_json).expect("parse fixture");
let call = CallConfig {
args: vec![batch_arg()],
..Default::default()
};
assert!(!super::fixture_uses_test_documents(
&fixture,
&call,
&[extract_input_type()],
&[]
));
}
}