use crate::e2e::config::E2eConfig;
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::{Fixture, FixtureDocsOperation};
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub(crate) struct PresentationOperation {
pub(crate) kind: &'static str,
pub(crate) expression: String,
pub(crate) item: String,
pub(crate) fields: Vec<String>,
pub(crate) optional: bool,
pub(crate) display: bool,
pub(crate) destructure_source: String,
pub(crate) destructure_item: String,
pub(crate) shown_optional: bool,
pub(crate) field_optionals: Vec<bool>,
}
fn path_yields_optional(resolver: &FieldResolver, path: &str) -> bool {
let segments: Vec<&str> = path.split('.').collect();
(1..=segments.len()).any(|length| resolver.is_optional(&segments[..length].join(".")))
}
pub(crate) fn resolve(
fixture: &Fixture,
e2e_config: &E2eConfig,
language: &str,
type_defs: &[crate::core::ir::TypeDef],
functions: &[crate::core::ir::FunctionDef],
) -> Vec<PresentationOperation> {
if fixture.docs.is_none() {
return Vec::new();
}
let call = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
let resolver = build_resolver(e2e_config, call, language, type_defs, functions);
resolve_with(fixture, e2e_config, language, &resolver, type_defs, functions)
}
fn build_resolver(
e2e_config: &E2eConfig,
call: &crate::core::config::e2e::CallConfig,
language: &str,
type_defs: &[crate::core::ir::TypeDef],
functions: &[crate::core::ir::FunctionDef],
) -> FieldResolver {
let (ir_reachable_fields, ir_known_excluded_fields, ir_optional_fields) = FieldResolver::ir_field_sets(type_defs);
anchor_to_declared_result_type(
FieldResolver::new(
e2e_config.effective_fields(call),
e2e_config.effective_fields_optional(call),
e2e_config.effective_result_fields(call),
e2e_config.effective_fields_array(call),
e2e_config.effective_fields_method_calls(call),
)
.with_ir_fields(ir_reachable_fields, ir_known_excluded_fields, ir_optional_fields),
call,
language,
type_defs,
functions,
)
}
fn anchor_to_declared_result_type(
resolver: FieldResolver,
call: &crate::core::config::e2e::CallConfig,
language: &str,
type_defs: &[crate::core::ir::TypeDef],
functions: &[crate::core::ir::FunctionDef],
) -> FieldResolver {
let root_type = crate::e2e::codegen::call_ir::resolve_declared_result_type(
call,
language,
crate::e2e::codegen::call_ir::CallIr { functions, type_defs },
);
resolver.with_ir_result_fields(FieldResolver::ir_result_field_facts(type_defs, language), root_type)
}
pub(crate) fn apply_derived_shows(
fixture: &mut Fixture,
e2e_config: &E2eConfig,
language: &str,
type_defs: &[crate::core::ir::TypeDef],
functions: &[crate::core::ir::FunctionDef],
) {
if fixture.docs.is_none() || fixture.has_docs_presentation() {
return;
}
let call = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
let resolver = build_resolver(e2e_config, call, language, type_defs, functions);
let paths: Vec<String> = default_operations_from_assertions(fixture, call, language, &resolver)
.into_iter()
.filter_map(|operation| match operation {
FixtureDocsOperation::Show { path, .. } => Some(path),
FixtureDocsOperation::Iterate { .. } => None,
})
.collect();
if paths.is_empty() {
return;
}
if let Some(docs) = fixture.docs.as_mut() {
docs.shows = paths;
}
}
pub(crate) fn resolve_with(
fixture: &Fixture,
e2e_config: &E2eConfig,
language: &str,
resolver: &FieldResolver,
type_defs: &[crate::core::ir::TypeDef],
functions: &[crate::core::ir::FunctionDef],
) -> Vec<PresentationOperation> {
let Some(docs) = fixture.docs.as_ref() else {
return Vec::new();
};
let call = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
let resolver = &anchor_to_declared_result_type(resolver.clone(), call, language, type_defs, functions);
let result_var = call.effective_result_var();
let result_root = root_variable(language, result_var);
let operations = docs
.shows
.iter()
.cloned()
.map(|path| FixtureDocsOperation::Show { path, display: false })
.chain(
docs.presentation
.iter()
.flat_map(|presentation| presentation.operations.iter().cloned()),
)
.collect::<Vec<_>>();
let operations = if operations.is_empty() {
default_operations_from_assertions(fixture, call, language, resolver)
} else {
operations
};
let resolver = &resolver
.clone()
.with_anchored_optional_paths(operations.iter().map(|operation| match operation {
FixtureDocsOperation::Show { path, .. } | FixtureDocsOperation::Iterate { path, .. } => path.as_str(),
}));
operations
.iter()
.map(|operation| match operation {
FixtureDocsOperation::Show { path, display } => PresentationOperation {
kind: "show",
expression: resolver.accessor(path, language, &result_root),
item: String::new(),
fields: Vec::new(),
optional: false,
display: *display,
destructure_source: String::new(),
destructure_item: String::new(),
shown_optional: path_yields_optional(resolver, path),
field_optionals: Vec::new(),
},
FixtureDocsOperation::Iterate {
path,
item,
fields,
display,
optional,
} => {
let (destructure_source, destructure_item, expression) =
typescript_first_item(path, language, resolver, &result_root);
let item_root = root_variable(language, item);
PresentationOperation {
kind: "iterate",
expression,
item: item.clone(),
fields: fields
.iter()
.map(|field| resolver.accessor(field, language, &item_root))
.collect(),
optional: *optional || resolver.is_optional(path),
display: *display,
destructure_source,
destructure_item,
shown_optional: false,
field_optionals: fields
.iter()
.map(|field| path_yields_optional(resolver, field))
.collect(),
}
}
})
.collect()
}
fn default_operations_from_assertions(
fixture: &Fixture,
call: &crate::core::config::e2e::CallConfig,
language: &str,
resolver: &FieldResolver,
) -> Vec<FixtureDocsOperation> {
if call.returns_void
|| call.effective_result_is_simple(language)
|| call.effective_result_is_bytes(language)
|| fixture.assertions.iter().any(|a| a.assertion_type == "error")
{
return Vec::new();
}
let mut seen_fields = Vec::new();
fixture
.assertions
.iter()
.filter_map(|assertion| assertion.field.as_deref())
.filter(|field| shows_on_result(field, resolver))
.filter(|field| {
let is_new = !seen_fields.contains(field);
if is_new {
seen_fields.push(*field);
}
is_new
})
.map(|field| FixtureDocsOperation::Show {
path: field.to_string(),
display: false,
})
.collect()
}
fn shows_on_result(field: &str, resolver: &FieldResolver) -> bool {
!field.is_empty()
&& !crate::e2e::codegen::streaming_assertions::is_streaming_virtual_field(field)
&& resolver.is_valid_for_result(field)
&& resolver.result_field_oracle_knows(field) != Some(false)
}
fn root_variable(language: &str, name: &str) -> String {
if language == "php" {
format!("${name}")
} else {
name.to_string()
}
}
fn typescript_first_item(
path: &str,
language: &str,
resolver: &FieldResolver,
result_var: &str,
) -> (String, String, String) {
if matches!(language, "node" | "wasm")
&& let Some((source, tail)) = path.split_once("[0].")
{
let source = resolver.accessor(source, language, result_var);
return (format!("{source} ?? []"), "first".into(), format!("first?.{tail}"));
}
(
String::new(),
String::new(),
resolver.accessor(path, language, result_var),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::e2e::config::{ArgMapping, CallConfig};
use crate::e2e::fixture::{FixtureDocs, FixtureDocsPresentation, SideEffectClass};
use std::collections::BTreeMap;
fn fixture() -> Fixture {
Fixture {
id: "present_items".into(),
description: "Present returned items".into(),
input: serde_json::json!({"old_source": "test.txt"}),
docs: Some(FixtureDocs {
topic: "configuration".into(),
stem: None,
paths: BTreeMap::new(),
title: None,
description: None,
input: None,
shows: Vec::new(),
error: None,
presentation: Some(FixtureDocsPresentation {
call: None,
input: Some(serde_json::json!({"source": "guide.txt"})),
args: Some(vec![ArgMapping {
name: "source".into(),
field: "source".into(),
arg_type: "string".into(),
optional: false,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
}]),
files: Vec::new(),
operations: vec![FixtureDocsOperation::Iterate {
path: "items".into(),
item: "item".into(),
fields: vec!["text".into(), "metadata.heading".into()],
display: true,
optional: true,
}],
}),
client: None,
side_effects: SideEffectClass::Safe,
coverage_exceptions: BTreeMap::new(),
}),
..Fixture::default()
}
}
fn config() -> E2eConfig {
E2eConfig {
call: CallConfig {
function: "process".into(),
result_var: "result".into(),
..CallConfig::default()
},
fields_optional: ["items".to_string()].into_iter().collect(),
..E2eConfig::default()
}
}
#[test]
fn docs_call_overrides_reuse_typed_fixture_arguments() {
let fixture = fixture().docs_call_fixture();
assert_eq!(fixture.input, serde_json::json!({"source": "guide.txt"}));
assert_eq!(fixture.args[0].arg_type, "string");
assert_eq!(fixture.args[0].field, "source");
}
#[test]
fn docs_call_fixture_removes_mock_harness_and_uses_an_illustrative_url() {
let mut fixture = fixture();
fixture
.docs
.as_mut()
.and_then(|docs| docs.presentation.as_mut())
.expect("presentation")
.input = None;
fixture.input = serde_json::json!({
"mock_responses": [{"path": "/guide.txt", "status_code": 200}],
"extract_input": {"kind": "uri", "uri": "$mock_url/guide.txt"}
});
fixture.mock_response = Some(crate::e2e::fixture::MockResponse {
status: 200,
body: None,
stream_chunks: None,
headers: Default::default(),
});
let docs_fixture = fixture.docs_call_fixture();
assert!(docs_fixture.mock_response.is_none());
assert!(docs_fixture.input.get("mock_responses").is_none());
assert_eq!(
docs_fixture
.input
.pointer("/extract_input/uri")
.and_then(serde_json::Value::as_str),
Some("https://example.com/guide.txt")
);
assert!(!docs_fixture.needs_mock_server());
}
#[test]
fn show_display_flag_selects_the_human_readable_rust_formatter() {
let mut display_fixture = fixture();
display_fixture
.docs
.as_mut()
.and_then(|docs| docs.presentation.as_mut())
.expect("presentation")
.operations = vec![FixtureDocsOperation::Show {
path: "text".into(),
display: true,
}];
let mut debug_fixture = fixture();
debug_fixture
.docs
.as_mut()
.and_then(|docs| docs.presentation.as_mut())
.expect("presentation")
.operations = vec![FixtureDocsOperation::Show {
path: "text".into(),
display: false,
}];
let config = config();
let render = |operations| {
crate::e2e::template_env::render(
"rust/snippet_body.rs.jinja",
minijinja::context! { imports => Vec::<String>::new(), body => vec!["let result = process();"],
is_async => false, presentation => operations },
)
};
let displayed = render(resolve(&display_fixture, &config, "rust", &[], &[]));
let debugged = render(resolve(&debug_fixture, &config, "rust", &[], &[]));
assert!(displayed.contains("println!(\"{}\", result.text);"), "{displayed}");
assert!(debugged.contains("println!(\"{:?}\", result.text);"), "{debugged}");
}
#[test]
fn presentation_templates_emit_idiomatic_python_rust_and_typescript() {
let fixture = fixture();
let config = config();
let python = resolve(&fixture, &config, "python", &[], &[]);
let rust = resolve(&fixture, &config, "rust", &[], &[]);
let mut typescript_fixture = fixture.clone();
typescript_fixture
.docs
.as_mut()
.and_then(|docs| docs.presentation.as_mut())
.expect("presentation")
.operations = vec![FixtureDocsOperation::Iterate {
path: "results[0].chunks".into(),
item: "chunk".into(),
fields: vec!["content".into()],
display: true,
optional: true,
}];
let typescript = resolve(&typescript_fixture, &config, "node", &[], &[]);
let python_output = crate::e2e::template_env::render(
"python/snippet_body.py.jinja",
minijinja::context! { imports => Vec::<String>::new(), body => vec!["result = process()"],
is_async => false, presentation => python },
);
let rust_output = crate::e2e::template_env::render(
"rust/snippet_body.rs.jinja",
minijinja::context! { imports => Vec::<String>::new(), body => vec!["let result = process();"],
is_async => false, presentation => rust },
);
let typescript_output = crate::e2e::template_env::render(
"typescript/snippet_body.jinja",
minijinja::context! { imports => vec!["process"], module => "@example/library",
setup_lines => Vec::<String>::new(), client_setup => "", call_expr => "process()",
result_var => "result", is_async => false, expects_error => false,
presentation => typescript },
);
assert!(
python_output.contains("for item in result.items or []:"),
"{python_output}"
);
assert!(
python_output.contains("print(item.metadata.heading)"),
"{python_output}"
);
assert!(
rust_output.contains("for item in result.items.iter().flatten()"),
"{rust_output}"
);
assert!(
rust_output.contains("println!(\"{}\", item.metadata.heading);"),
"{rust_output}"
);
assert!(
typescript_output.contains("const [first] = result.results ?? [];"),
"{typescript_output}"
);
assert!(
typescript_output.contains("for (const chunk of first?.chunks ?? [])"),
"{typescript_output}"
);
assert!(
typescript_output.contains("console.log(chunk.content);"),
"{typescript_output}"
);
}
#[test]
fn resolve_iterate_treats_path_optional_when_fixture_flag_is_stale() {
let mut stale_fixture = fixture();
stale_fixture
.docs
.as_mut()
.and_then(|docs| docs.presentation.as_mut())
.expect("presentation")
.operations = vec![FixtureDocsOperation::Iterate {
path: "results[0].elements".into(),
item: "element".into(),
fields: vec!["element_type".into()],
display: true,
optional: false,
}];
let mut stale_config = config();
stale_config.fields_optional = ["results[0].elements".to_string()].into_iter().collect();
let operations = resolve(&stale_fixture, &stale_config, "node", &[], &[]);
let iterate = operations.first().expect("one iterate operation");
assert!(
iterate.optional,
"resolver-known optionality for 'results[0].elements' must win over the fixture's stale `optional: false`"
);
let typescript_output = crate::e2e::template_env::render(
"typescript/snippet_body.jinja",
minijinja::context! { imports => vec!["process"], module => "@example/library",
setup_lines => Vec::<String>::new(), client_setup => "", call_expr => "process()",
result_var => "result", is_async => false, expects_error => false,
presentation => operations },
);
assert!(
typescript_output.contains("for (const element of first?.elements ?? [])"),
"{typescript_output}"
);
}
#[test]
fn resolve_show_unwraps_ir_only_optional_field_in_non_leaf_position() {
use crate::core::ir::{FieldDef, TypeDef};
let mut show_fixture = fixture();
show_fixture
.docs
.as_mut()
.and_then(|docs| docs.presentation.as_mut())
.expect("presentation")
.operations = vec![FixtureDocsOperation::Show {
path: "data.kind".into(),
display: false,
}];
let config = config();
assert!(!config.fields_optional.contains("data"));
let process_result = TypeDef {
name: "ProcessResult".to_string(),
fields: vec![FieldDef {
name: "data".to_string(),
optional: true,
..FieldDef::default()
}],
..TypeDef::default()
};
let without_ir = resolve(&show_fixture, &config, "rust", &[], &[]);
let with_ir = resolve(&show_fixture, &config, "rust", &[process_result], &[]);
assert_eq!(
without_ir[0].expression, "result.data.kind",
"with no IR in scope, resolve falls back to the pre-fix (non-compiling) accessor"
);
assert_eq!(
with_ir[0].expression, "result.data.as_ref().unwrap().kind",
"with IR in scope, resolve must unwrap the Option before the nested field access"
);
}
#[test]
fn resolve_derives_show_operations_from_assertion_fields_when_docs_names_none() {
let fixture: Fixture = serde_json::from_value(serde_json::json!({
"id": "smoke_simple_paragraph",
"description": "Simple paragraph converts correctly",
"input": {"html": "<p>Hello World</p>"},
"assertions": [
{"type": "equals", "field": "content", "value": "Hello World\n"},
{"type": "not_empty", "field": "content"}
],
"docs": {"topic": "smoke", "stem": "smoke_simple_paragraph"}
}))
.expect("fixture must parse");
let config = E2eConfig {
call: CallConfig {
function: "convert".into(),
result_var: "result".into(),
..CallConfig::default()
},
..E2eConfig::default()
};
let python = resolve(&fixture, &config, "python", &[], &[]);
assert_eq!(python.len(), 1, "the duplicate 'content' field must not be shown twice");
assert_eq!(python[0].kind, "show");
assert_eq!(python[0].expression, "result.content");
let rust = resolve(&fixture, &config, "rust", &[], &[]);
assert_eq!(rust[0].expression, "result.content");
}
#[test]
fn resolve_ignores_assertions_with_no_field_when_deriving_show_operations() {
let fixture: Fixture = serde_json::from_value(serde_json::json!({
"id": "auth_error",
"description": "Authentication failure",
"input": {"token": "bad"},
"assertions": [{"type": "error"}],
"docs": {"topic": "errors", "stem": "auth_error"}
}))
.expect("fixture must parse");
let config = config();
assert!(resolve(&fixture, &config, "python", &[], &[]).is_empty());
}
#[test]
fn resolve_derives_no_show_operations_for_a_void_returning_call() {
let fixture: Fixture = serde_json::from_value(serde_json::json!({
"id": "configure_logging",
"description": "Configure logging",
"input": {"level": "debug"},
"assertions": [{"type": "equals", "field": "level", "value": "debug"}],
"docs": {"topic": "configuration", "stem": "configure_logging"}
}))
.expect("fixture must parse");
let mut config = config();
config.call.returns_void = true;
assert!(resolve(&fixture, &config, "python", &[], &[]).is_empty());
}
}
#[cfg(test)]
#[path = "presentation/derived_show_resolution_tests.rs"]
mod derived_show_resolution_tests;
#[cfg(test)]
#[path = "presentation/anchored_result_facts_tests.rs"]
mod anchored_result_facts_tests;