use crate::codegen::keywords::swift_ident;
use crate::e2e::codegen::field_skip::nested_wildcard_skip_line;
use crate::e2e::field_access::FieldResolver;
use heck::ToLowerCamelCase;
use std::collections::HashMap;
pub(super) fn materialise_vec_temporaries(expr: &str, name_suffix: &str) -> Option<(Vec<String>, String, bool)> {
let mut setups = Vec::new();
let mut current = expr.to_string();
let mut is_string_key = false;
let mut hoist_count = 0usize;
let mut carried_optional = false;
while let Some((idx, marker_optional)) = find_next_subscript_marker(¤t) {
let is_optional = marker_optional || carried_optional;
hoist_count += 1;
match hoist_one_subscript(¤t, idx, marker_optional, is_optional, name_suffix, hoist_count) {
HoistOutcome::Refuse => return None,
HoistOutcome::Hoisted {
setup,
next,
is_string_key: key,
} => {
setups.push(setup);
current = next;
is_string_key = key;
carried_optional = carried_optional || marker_optional;
}
}
}
Some((setups, current, is_string_key))
}
enum HoistOutcome {
Hoisted {
setup: String,
next: String,
is_string_key: bool,
},
Refuse,
}
fn hoist_one_subscript(
current: &str,
idx: usize,
marker_optional: bool,
is_optional: bool,
name_suffix: &str,
hoist_count: usize,
) -> HoistOutcome {
let bracket_start = if marker_optional { idx + 3 } else { idx + 2 }; let after_open = bracket_start + 1; let Some(close_rel) = find_subscript_close(¤t[after_open..]) else {
return HoistOutcome::Refuse;
};
let subscript_end = after_open + close_rel; let prefix = ¤t[..idx + 2]; let subscript = ¤t[bracket_start..=subscript_end]; let tail = ¤t[subscript_end + 1..]; let method_dot = current[..idx].rfind('.').unwrap_or(0);
let method = ¤t[method_dot + 1..idx];
let local = format!("_vec_{method}_{name_suffix}_{hoist_count}");
let inner = subscript.trim_start_matches('[').trim_end_matches(']');
let is_string_key = inner.starts_with('"') && inner.ends_with('"');
if is_string_key && (tail.contains("()[") || tail.contains("()?[")) {
return HoistOutcome::Refuse;
}
let setup = build_hoist_setup(&local, prefix, is_string_key, is_optional);
let next = if !is_string_key && is_optional {
format!("{local}?{subscript}{tail}")
} else {
format!("{local}{subscript}{tail}")
};
HoistOutcome::Hoisted {
setup,
next,
is_string_key,
}
}
fn build_hoist_setup(local: &str, prefix: &str, is_string_key: bool, is_optional: bool) -> String {
if !is_string_key {
return format!("let {local} = {prefix}");
}
let to_string_call = if is_optional {
format!("{prefix}?.toString()")
} else {
format!("{prefix}.toString()")
};
format!(
"let {local} = (try? JSONSerialization.jsonObject(with: ({to_string_call} ?? \"{{}}\").data(using: .utf8)!) as? [String: String]) ?? [:]"
)
}
fn find_next_subscript_marker(s: &str) -> Option<(usize, bool)> {
let bytes = s.as_bytes();
let mut i = 0usize;
let mut in_quotes = false;
while i < bytes.len() {
if in_quotes {
match bytes[i] {
b'\\' => i += 2,
b'"' => {
in_quotes = false;
i += 1;
}
_ => i += 1,
}
continue;
}
if bytes[i] == b'"' {
in_quotes = true;
i += 1;
} else if bytes[i..].starts_with(b"()?[") {
return Some((i, true));
} else if bytes[i..].starts_with(b"()[") {
return Some((i, false));
} else {
i += 1;
}
}
None
}
fn find_subscript_close(content: &str) -> Option<usize> {
let bytes = content.as_bytes();
if bytes.first() != Some(&b'"') {
return content.find(']');
}
let mut i = 1usize;
while i < bytes.len() {
match bytes[i] {
b'\\' => i += 2,
b'"' => return content[i + 1..].find(']').map(|rel| i + 1 + rel),
_ => i += 1,
}
}
None
}
pub(super) use super::accessor_walk::swift_build_accessor;
pub(super) struct SwiftTraversalContains<'a> {
pub(super) array_part: &'a str,
pub(super) element_part: &'a str,
pub(super) full_field: &'a str,
pub(super) value_expression: &'a str,
pub(super) result_variable: &'a str,
pub(super) negate: bool,
pub(super) message: &'a str,
pub(super) field_resolver: &'a FieldResolver,
}
pub(super) fn swift_traversal_contains_assert(context: SwiftTraversalContains<'_>) -> String {
let array_accessor = context
.field_resolver
.accessor(context.array_part, "swift", context.result_variable);
let resolved_full = context.field_resolver.resolve(context.full_field);
let resolved_elem_part = resolved_full
.find("[].")
.map(|d| &resolved_full[d + 3..])
.unwrap_or(context.element_part);
if let Some(line) = nested_wildcard_skip_line(" ", "//", context.full_field, resolved_elem_part) {
return line;
}
let elem_accessor = context
.field_resolver
.element_accessor(resolved_elem_part, "swift", "$0");
let elem_is_enum = context.field_resolver.is_enum(context.full_field);
let elem_is_optional = context.field_resolver.is_optional(resolved_elem_part)
|| context
.field_resolver
.is_optional(context.field_resolver.resolve(resolved_elem_part));
let elem_str = if elem_is_enum {
format!("{elem_accessor}.toString()")
} else if elem_is_optional {
format!("({elem_accessor}?.toString() ?? \"\")")
} else {
format!("{elem_accessor}.toString()")
};
let assert_fn = if context.negate {
"XCTAssertFalse"
} else {
"XCTAssertTrue"
};
format!(
" {assert_fn}({array_accessor}.contains(where: {{ {elem_str}.contains({}) }}), \"{}\")",
context.value_expression, context.message
)
}
pub(super) fn swift_array_contains_expr(
field: Option<&str>,
result_var: &str,
field_resolver: &FieldResolver,
result_field_accessor: &HashMap<String, String>,
materialized_expr: Option<&str>,
) -> (String, bool) {
let Some(f) = field else {
return (format!("{result_var}.map {{ $0.asStr().toString() }}"), false);
};
let resolved_field = field_resolver.resolve(f);
let elem_accessor_name = result_field_accessor
.get(f)
.or_else(|| result_field_accessor.get(resolved_field))
.cloned()
.unwrap_or_else(|| "as_str".to_string());
let elem_call = swift_ident(&elem_accessor_name.to_lower_camel_case());
let (accessor, has_optional) = if let Some(expr) = materialized_expr {
(expr.to_string(), swift_build_accessor(f, result_var, field_resolver).1)
} else {
swift_build_accessor(f, result_var, field_resolver)
};
let field_is_optional =
has_optional || field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f));
if field_is_optional {
(format!("{accessor}?.map {{ $0.{elem_call}().toString() }}"), true)
} else {
(format!("{accessor}.map {{ $0.{elem_call}().toString() }}"), false)
}
}
pub(super) fn swift_stringy_aggregator_contains_assert(
field: Option<&str>,
result_var: &str,
field_resolver: &FieldResolver,
swift_val: &str,
) -> Option<String> {
let field = field?;
let resolved = field_resolver.resolve(field);
if resolved.contains('.') || resolved.contains('[') {
return None;
}
let root_type = field_resolver.swift_root_type()?.clone();
let elem_type = field_resolver.swift_advance(Some(&root_type), resolved)?;
let stringy = field_resolver.swift_stringy_fields(&elem_type)?;
if stringy.len() < 2 {
return None;
}
let array_accessor = field_resolver.accessor(field, "swift", result_var);
let mut texts_lines: Vec<String> = Vec::new();
for sf in stringy {
texts_lines.push(stringy_field_text_line(sf));
}
let texts_block = texts_lines.join("\n");
Some(format!(
" XCTAssertTrue({array_accessor}.contains(where: {{ item in\n var texts = [String]()\n{texts_block}\n return texts.contains(where: {{ $0.contains({swift_val}) }})\n }}), \"expected to contain: \\({swift_val})\")"
))
}
fn stringy_field_text_line(sf: &crate::e2e::field_access::StringyField) -> String {
use crate::e2e::field_access::StringyFieldKind;
let call = swift_ident(&sf.name.to_lower_camel_case());
match sf.kind {
StringyFieldKind::Plain => {
format!(" texts.append(item.{call}().toString())")
}
StringyFieldKind::Optional => {
format!(" if let v = item.{call}() {{ texts.append(v.toString()) }}")
}
StringyFieldKind::Vec => {
format!(" texts.append(contentsOf: item.{call}().map {{ $0.as_str().toString() }})")
}
}
}
pub(super) fn swift_array_count_expr(
field: Option<&str>,
result_var: &str,
field_resolver: &FieldResolver,
materialized_expr: Option<&str>,
) -> Option<String> {
let Some(f) = field else {
return Some(format!("{result_var}.count"));
};
let accessor = if let Some(expr) = materialized_expr {
expr.to_string()
} else {
swift_build_accessor(f, result_var, field_resolver).0
};
let mut has_optional = swift_build_accessor(f, result_var, field_resolver).1;
if field_resolver.is_optional(f) {
has_optional = true;
}
let count_target = swift_count_target(&accessor, field_resolver, Some(f))?;
let target_is_to_string = count_target.ends_with(".toString()");
Some(if count_target.contains("?.") {
format!("({count_target}.count ?? 0)")
} else if has_optional && !target_is_to_string {
format!("({count_target}?.count ?? 0)")
} else {
format!("{count_target}.count")
})
}
pub(super) fn swift_count_target(
field_expr: &str,
field_resolver: &FieldResolver,
field: Option<&str>,
) -> Option<String> {
let is_method_call = field_expr.trim_end().ends_with(')');
if !is_method_call {
return Some(field_expr.to_string());
}
if let Some(f) = field {
let resolved = field_resolver.resolve(f);
if field_resolver.leaf_is_json_bridged_via_swift_map(resolved) {
return None;
}
if field_resolver.leaf_is_vec_via_swift_map(resolved)
|| field_resolver.is_array(resolved)
|| field_resolver.is_collection_root(resolved)
{
return Some(field_expr.to_string());
}
}
Some(format!("{field_expr}.toString()"))
}
pub(super) fn swift_array_is_empty_expr(field_expr: &str, accessor_is_optional: bool) -> String {
if accessor_is_optional {
format!("({field_expr}.isEmpty ?? true)")
} else {
format!("{field_expr}.isEmpty")
}
}
pub(super) fn swift_array_not_empty_predicate(field_expr: &str, accessor_is_optional: bool) -> String {
if accessor_is_optional {
format!("{field_expr}.isEmpty == false")
} else {
format!("!{field_expr}.isEmpty")
}
}
#[cfg(test)]
mod materialise_vec_temporaries_tests {
use super::materialise_vec_temporaries;
#[test]
fn nested_indexed_rust_vec_hoists_every_temporary() {
let (setup, rewritten, is_map_subscript) =
materialise_vec_temporaries("result.items()[0].nested()[1].value()", "count_min_ab12").unwrap();
assert_eq!(
setup,
vec![
"let _vec_items_count_min_ab12_1 = result.items()".to_string(),
"let _vec_nested_count_min_ab12_2 = _vec_items_count_min_ab12_1[0].nested()".to_string(),
]
);
assert_eq!(rewritten, "_vec_nested_count_min_ab12_2[1].value()");
assert!(!is_map_subscript);
}
#[test]
fn single_indexed_rust_vec_hoists_one_temporary() {
let (setup, rewritten, is_map_subscript) =
materialise_vec_temporaries("result.items()[0].value()", "count_min_ab12").unwrap();
assert_eq!(
setup,
vec!["let _vec_items_count_min_ab12_1 = result.items()".to_string()]
);
assert_eq!(rewritten, "_vec_items_count_min_ab12_1[0].value()");
assert!(!is_map_subscript);
}
#[test]
fn non_indexed_chain_is_unchanged() {
let (setup, rewritten, is_map_subscript) =
materialise_vec_temporaries("result.items().value()", "count_min_ab12").unwrap();
assert!(setup.is_empty());
assert_eq!(rewritten, "result.items().value()");
assert!(!is_map_subscript);
}
#[test]
fn terminal_map_key_containing_a_bracket_is_recognised_as_a_string_key() {
let (setup, rewritten, is_map_subscript) =
materialise_vec_temporaries("result.labels()[\"a]b\"]", "equals_ff01").unwrap();
assert_eq!(
setup,
vec![
"let _vec_labels_equals_ff01_1 = (try? JSONSerialization.jsonObject(with: \
(result.labels().toString() ?? \"{}\").data(using: .utf8)!) as? [String: String]) ?? [:]"
.to_string(),
]
);
assert_eq!(rewritten, "_vec_labels_equals_ff01_1[\"a]b\"]");
assert!(is_map_subscript);
}
#[test]
fn vec_then_terminal_map_subscript_proves_last_subscript_wins() {
let (setup, rewritten, is_map_subscript) =
materialise_vec_temporaries("result.items()[0].labels()[\"a\"]", "count_min_9f01").unwrap();
assert_eq!(
setup,
vec![
"let _vec_items_count_min_9f01_1 = result.items()".to_string(),
"let _vec_labels_count_min_9f01_2 = (try? JSONSerialization.jsonObject(with: \
(_vec_items_count_min_9f01_1[0].labels().toString() ?? \"{}\").data(using: .utf8)!) as? \
[String: String]) ?? [:]"
.to_string(),
]
);
assert_eq!(rewritten, "_vec_labels_count_min_9f01_2[\"a\"]");
assert!(is_map_subscript);
}
#[test]
fn mixed_map_then_vec_subscript_is_refused() {
let result = materialise_vec_temporaries("result.labels()[\"a\"].items()[0]", "not_empty_77aa");
assert!(result.is_none(), "got: {result:?}");
}
}
#[cfg(test)]
mod nested_wildcard_tests {
use super::{SwiftTraversalContains, swift_traversal_contains_assert};
use crate::e2e::field_access::FieldResolver;
use std::collections::{HashMap, HashSet};
fn array_resolver(field: &str) -> FieldResolver {
let names: HashSet<String> = [field.to_string()].into_iter().collect();
FieldResolver::new(&HashMap::new(), &HashSet::new(), &names, &names, &HashSet::new())
}
fn render(full_field: &str, array_part: &str, element_part: &str, resolver: &FieldResolver) -> String {
swift_traversal_contains_assert(SwiftTraversalContains {
array_part,
element_part,
full_field,
value_expression: "\"example.test\"",
result_variable: "result",
negate: false,
message: "expected to contain",
field_resolver: resolver,
})
}
#[test]
fn single_wildcard_still_builds_a_contains_where_closure() {
let line = render("links[].url", "links", "url", &array_resolver("links"));
assert!(line.contains(".contains(where: {"), "got: {line}");
assert!(!line.contains("skipped:"), "got: {line}");
}
#[test]
fn nested_wildcard_should_return_a_visible_skip_rather_than_an_index_zero_check() {
let line = render("pages[].links[].url", "pages", "links[].url", &array_resolver("pages"));
assert_eq!(
line, " // skipped: nested array-wildcard field 'pages[].links[].url' not supported",
"got: {line}"
);
}
}