use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{FieldDef, FunctionDef, PrimitiveType, TypeDef, TypeRef};
use crate::e2e::config::{CallConfig, E2eConfig};
use crate::e2e::fixture::{Assertion, Fixture};
fn field(name: &str, ty: TypeRef, optional: bool) -> FieldDef {
FieldDef {
name: name.to_string(),
ty,
optional,
..FieldDef::default()
}
}
fn metadata_ir() -> (Vec<TypeDef>, Vec<FunctionDef>) {
let type_defs = vec![
TypeDef {
name: "HeadingInfo".to_string(),
fields: vec![
field("level", TypeRef::Primitive(PrimitiveType::U32), false),
field("text", TypeRef::String, false),
],
has_serde: true,
..TypeDef::default()
},
TypeDef {
name: "PageMetadata".to_string(),
fields: vec![
field("title", TypeRef::String, false),
field(
"headings",
TypeRef::Vec(Box::new(TypeRef::Named("HeadingInfo".to_string()))),
true,
),
field("og_locale_alternates", TypeRef::Vec(Box::new(TypeRef::String)), true),
field(
"favicons",
TypeRef::Vec(Box::new(TypeRef::Named("HeadingInfo".to_string()))),
false,
),
],
has_serde: true,
..TypeDef::default()
},
TypeDef {
name: "ArticleInfo".to_string(),
fields: vec![
field("published_time", TypeRef::String, true),
field("byline", TypeRef::String, false),
],
is_opaque: true,
..TypeDef::default()
},
TypeDef {
name: "ProcessResult".to_string(),
fields: vec![
field("metadata", TypeRef::Named("PageMetadata".to_string()), false),
field("article", TypeRef::Named("ArticleInfo".to_string()), true),
],
has_serde: true,
..TypeDef::default()
},
];
let functions = vec![FunctionDef {
name: "process".to_string(),
return_type: TypeRef::Named("ProcessResult".to_string()),
..FunctionDef::default()
}];
(type_defs, functions)
}
fn render_assertion_on(assertion: Assertion) -> String {
render_assertion_with_result_fields(assertion, &[])
}
fn render_assertion_with_result_fields(assertion: Assertion, result_fields: &[&str]) -> String {
let (type_defs, functions) = metadata_ir();
let call_config = CallConfig {
function: "process".to_string(),
result_var: "result".to_string(),
..CallConfig::default()
};
let mut e2e_config = E2eConfig::default();
e2e_config.calls.insert("process".to_string(), call_config.clone());
e2e_config
.result_fields
.extend(result_fields.iter().map(|f| (*f).to_string()));
let fixture = Fixture {
id: "json_bridged_traversal".to_string(),
description: "JSON-bridged traversal".to_string(),
call: Some("process".to_string()),
assertions: vec![assertion],
..Fixture::default()
};
let map = super::values::build_swift_first_class_map(&type_defs, &[], &e2e_config, &call_config);
let config = ResolvedCrateConfig {
name: "sample".into(),
..ResolvedCrateConfig::default()
};
let mut out = String::new();
super::test_method::render_test_method(
&mut out,
&fixture,
&e2e_config,
"process",
"result",
&[],
false,
None,
&map,
"Sample",
&config,
&type_defs,
&[],
&functions,
&[],
);
out
}
fn assertion(assertion_type: &str, field_path: &str, value: Option<serde_json::Value>) -> Assertion {
Assertion {
assertion_type: assertion_type.to_string(),
field: Some(field_path.to_string()),
value,
..Assertion::default()
}
}
const JSON_BRIDGE_SKIP: &str = "swift-bridge JSON-bridges it to RustString";
#[test]
fn should_refuse_an_indexed_step_into_a_json_bridged_leaf() {
let out = render_assertion_on(assertion(
"equals",
"metadata.headings[0].level",
Some(serde_json::json!(1)),
));
assert!(
out.contains(JSON_BRIDGE_SKIP),
"indexing a JSON-bridged leaf must render the JSON-bridge skip, got:\n{out}"
);
assert!(
!out.contains("headings()[0]") && !out.contains("headings()?[0]"),
"must not emit a subscript against a RustString leaf, got:\n{out}"
);
}
#[test]
fn should_refuse_a_wildcard_step_into_a_json_bridged_leaf() {
let out = render_assertion_on(assertion(
"contains",
"metadata.headings[].text",
Some(serde_json::json!("Intro")),
));
assert!(
out.contains(JSON_BRIDGE_SKIP),
"a wildcard over a JSON-bridged leaf must be refused for the JSON-bridge reason, got:\n{out}"
);
assert!(
!out.contains("headings()[0]") && !out.contains("headings()?[0]"),
"a wildcard over a JSON-bridged leaf must not emit an index-0 accessor, got:\n{out}"
);
}
#[test]
fn should_refuse_a_count_suffix_on_an_optional_vec_of_string() {
let out = render_assertion_on(assertion(
"equals",
"metadata.og_locale_alternates.length",
Some(serde_json::json!(2)),
));
assert!(
out.contains(JSON_BRIDGE_SKIP),
"a count on Option<Vec<String>> must render the JSON-bridge skip, got:\n{out}"
);
assert!(
!out.contains("ogLocaleAlternates()?.count") && !out.contains("ogLocaleAlternates().count"),
"must not emit .count against a RustString leaf, got:\n{out}"
);
}
#[test]
fn should_still_count_a_non_optional_vec_whose_getter_is_a_real_rust_vec() {
let out = render_assertion_on(assertion(
"equals",
"metadata.favicons.length",
Some(serde_json::json!(3)),
));
assert!(
!out.contains(JSON_BRIDGE_SKIP),
"a real RustVec getter must not be refused as JSON-bridged, got:\n{out}"
);
assert!(
out.contains("favicons()") && out.contains("count"),
"a countable leaf must still render its count assertion, got:\n{out}"
);
}
#[test]
fn should_keep_an_assertion_that_stops_at_the_json_bridged_leaf() {
let out = render_assertion_on(assertion(
"contains",
"metadata.headings",
Some(serde_json::json!("Intro")),
));
assert!(
!out.contains(JSON_BRIDGE_SKIP),
"an assertion that does not step past the bridged leaf must not be refused, got:\n{out}"
);
assert!(
out.contains("headings()"),
"the bridged leaf itself must still be read, got:\n{out}"
);
}
#[test]
fn should_chain_optionally_at_an_optional_leaf_behind_an_optional_ancestor() {
let out = render_assertion_on(assertion(
"equals",
"article.published_time",
Some(serde_json::json!("2024-01-01")),
));
assert!(
out.contains("article()?.publishedTime()?.toString()"),
"an optional leaf must be unwrapped before toString(), got:\n{out}"
);
assert!(
!out.contains("publishedTime().toString()"),
"must not apply toString() directly to an Optional<RustString> leaf, got:\n{out}"
);
}
#[test]
fn should_not_chain_optionally_at_a_non_optional_leaf_behind_an_optional_ancestor() {
let out = render_assertion_on(assertion("equals", "article.byline", Some(serde_json::json!("Ada"))));
assert!(
out.contains("article()?.byline().toString()"),
"a non-optional leaf must keep a plain toString(), got:\n{out}"
);
assert!(
!out.contains("byline()?.toString()"),
"must not apply optional chaining to a non-optional RustString leaf, got:\n{out}"
);
}
#[test]
fn should_not_strip_a_real_struct_segment_that_result_fields_omits() {
let out = render_assertion_with_result_fields(
assertion("equals", "metadata.favicons.length", Some(serde_json::json!(3))),
&["favicons", "title"],
);
assert!(
out.contains("metadata()") || out.contains("metadata."),
"the real `metadata` segment must survive, got:\n{out}"
);
assert!(
!out.contains("result.favicons()"),
"must not build the accessor on the result when `metadata` is a real field, got:\n{out}"
);
}
#[test]
fn should_still_strip_a_virtual_namespace_segment_the_ir_does_not_declare() {
let out = render_assertion_with_result_fields(
assertion("equals", "browser.title", Some(serde_json::json!("Hello"))),
&["title"],
);
assert!(
out.contains("result.title()"),
"a virtual namespace prefix must still be stripped down to the real field, got:\n{out}"
);
assert!(
!out.contains("browser()"),
"a virtual namespace prefix must still be stripped, got:\n{out}"
);
}
#[test]
fn should_keep_an_assertion_on_a_plain_string_field() {
let out = render_assertion_on(assertion("equals", "metadata.title", Some(serde_json::json!("Hello"))));
assert!(
!out.contains(JSON_BRIDGE_SKIP),
"a plain String field must not be refused as JSON-bridged, got:\n{out}"
);
assert!(
out.contains("title()"),
"a plain String field must still render its assertion, got:\n{out}"
);
}