use crate::codegen::keywords::swift_ident;
use crate::e2e::field_access::FieldResolver;
use heck::ToLowerCamelCase;
use std::collections::HashMap;
pub(super) fn materialise_vec_temporaries(expr: &str, name_suffix: &str) -> (Vec<String>, String, bool) {
let Some(idx) = expr.find("()[") else {
return (Vec::new(), expr.to_string(), false);
};
let after_open = idx + 3; let Some(close_rel) = expr[after_open..].find(']') else {
return (Vec::new(), expr.to_string(), false);
};
let subscript_end = after_open + close_rel; let prefix = &expr[..idx + 2]; let subscript = &expr[idx + 2..=subscript_end]; let tail = &expr[subscript_end + 1..]; let method_dot = expr[..idx].rfind('.').unwrap_or(0);
let method = &expr[method_dot + 1..idx];
let local = format!("_vec_{}_{}", method, name_suffix);
let inner = subscript.trim_start_matches('[').trim_end_matches(']');
let is_string_key = inner.starts_with('"') && inner.ends_with('"');
let setup = if is_string_key {
format!(
"let {local} = (try? JSONSerialization.jsonObject(with: ({prefix}.toString() ?? \"{{}}\").data(using: .utf8)!) as? [String: String]) ?? [:]"
)
} else {
format!("let {local} = {prefix}")
};
let rewritten = format!("{local}{subscript}{tail}");
(vec![setup], rewritten, is_string_key)
}
pub(super) fn swift_build_accessor(field: &str, result_var: &str, field_resolver: &FieldResolver) -> (String, bool) {
let resolved = field_resolver.resolve(field);
let parts: Vec<&str> = resolved.split('.').collect();
let mut current_type: Option<String> = field_resolver.swift_root_type().cloned();
let mut via_rust_vec = false;
let mut via_opaque = false;
let mut out = result_var.to_string();
let mut has_optional = false;
let mut path_so_far = String::new();
let total = parts.len();
for (i, part) in parts.iter().enumerate() {
let is_leaf = i == total - 1;
let (field_name, subscript): (&str, Option<&str>) = if let Some(bracket_pos) = part.find('[') {
(&part[..bracket_pos], Some(&part[bracket_pos..]))
} else {
(part, None)
};
if !path_so_far.is_empty() {
path_so_far.push('.');
}
let base_path = {
let mut p = path_so_far.clone();
p.push_str(field_name);
p
};
path_so_far.push_str(part);
let is_first_class = current_type
.as_ref()
.is_some_and(|t| field_resolver.swift_is_first_class(Some(t)));
let property_syntax = !via_rust_vec && !via_opaque && is_first_class;
if !property_syntax {
via_opaque = true;
}
out.push('.');
out.push_str(&field_name.to_lower_camel_case());
if let Some(sub) = subscript {
let field_is_optional = field_resolver.is_optional(&base_path);
let access = if property_syntax { "" } else { "()" };
if field_is_optional {
out.push_str(&format!("{access}?"));
has_optional = true;
} else {
out.push_str(access);
}
out.push_str(sub);
current_type = field_resolver.swift_advance(current_type.as_deref(), field_name);
if !property_syntax {
via_rust_vec = true;
}
} else {
if !property_syntax {
out.push_str("()");
}
if !is_leaf && field_resolver.is_optional(&base_path) {
out.push('?');
has_optional = true;
}
current_type = field_resolver.swift_advance(current_type.as_deref(), field_name);
}
}
(out, has_optional)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn swift_traversal_contains_assert(
array_part: &str,
element_part: &str,
full_field: &str,
val_expr: &str,
result_var: &str,
negate: bool,
msg: &str,
enum_fields: &std::collections::HashSet<String>,
field_resolver: &FieldResolver,
) -> String {
let array_accessor = field_resolver.accessor(array_part, "swift", result_var);
let resolved_full = field_resolver.resolve(full_field);
let resolved_elem_part = resolved_full
.find("[].")
.map(|d| &resolved_full[d + 3..])
.unwrap_or(element_part);
let elem_accessor = field_resolver.accessor(resolved_elem_part, "swift", "$0");
let elem_is_enum = enum_fields.contains(full_field) || enum_fields.contains(resolved_full);
let elem_is_optional = field_resolver.is_optional(resolved_elem_part)
|| field_resolver.is_optional(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 negate { "XCTAssertFalse" } else { "XCTAssertTrue" };
format!(" {assert_fn}({array_accessor}.contains(where: {{ {elem_str}.contains({val_expr}) }}), \"{msg}\")")
}
pub(super) fn swift_array_contains_expr(
field: Option<&str>,
result_var: &str,
field_resolver: &FieldResolver,
result_field_accessor: &HashMap<String, String>,
) -> (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) = 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> {
use crate::e2e::field_access::StringyFieldKind;
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 {
let call = swift_ident(&sf.name.to_lower_camel_case());
match sf.kind {
StringyFieldKind::Plain => {
texts_lines.push(format!(" texts.append(item.{call}().toString())"));
}
StringyFieldKind::Optional => {
texts_lines.push(format!(
" if let v = item.{call}() {{ texts.append(v.toString()) }}"
));
}
StringyFieldKind::Vec => {
texts_lines.push(format!(
" texts.append(contentsOf: item.{call}().map {{ $0.as_str().toString() }})"
));
}
}
}
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})\")"
))
}
pub(super) fn swift_array_count_expr(field: Option<&str>, result_var: &str, field_resolver: &FieldResolver) -> String {
let Some(f) = field else {
return format!("{result_var}.count");
};
let (accessor, mut has_optional) = swift_build_accessor(f, result_var, field_resolver);
if field_resolver.is_optional(f) {
has_optional = true;
}
if has_optional {
if accessor.contains("?.") {
format!("{accessor}.count ?? 0")
} else {
format!("({accessor}?.count ?? 0)")
}
} else {
format!("{accessor}.count")
}
}
pub(super) fn swift_count_target(field_expr: &str, field_resolver: &FieldResolver, field: Option<&str>) -> String {
let is_method_call = field_expr.trim_end().ends_with(')');
if !is_method_call {
return field_expr.to_string();
}
if let Some(f) = field
&& field_resolver.leaf_is_vec_via_swift_map(field_resolver.resolve(f))
{
return field_expr.to_string();
}
format!("{field_expr}.toString()")
}