use crate::e2e::codegen::field_skip::FieldSkip;
use crate::e2e::field_access::FieldResolver;
const COUNT_SUFFIXES: [&str; 3] = ["length", "count", "size"];
pub(super) fn json_bridged_traversal_skip(field_resolver: &FieldResolver, field: Option<&str>) -> Option<String> {
let field = field.filter(|f| !f.is_empty())?;
let bridged = field_resolver.swift_json_bridged_traversal_prefix(field)?;
Some(skip_line(FieldSkip::CountOnJsonBridgedLeafInSwift, &bridged))
}
pub(super) fn non_countable_leaf_count_skip(field_resolver: &FieldResolver, field: Option<&str>) -> Option<String> {
let field = field?;
let collection = COUNT_SUFFIXES
.iter()
.find_map(|suffix| field.strip_suffix(&format!(".{suffix}")))?;
if collection.is_empty() || field_resolver.leaf_is_vec_via_swift_map(field_resolver.resolve(collection)) {
return None;
}
Some(skip_line(FieldSkip::CountOnJsonBridgedLeafInSwift, field))
}
pub(super) fn swift_json_bridged_count_expr(
field_resolver: &FieldResolver,
field: Option<&str>,
field_expr: &str,
) -> Option<String> {
let field = field.filter(|f| !f.is_empty())?;
let resolved = field_resolver.resolve(field);
let is_collection = field_resolver.is_array(field)
|| field_resolver.is_array(resolved)
|| field_resolver.is_collection_root(field)
|| field_resolver.is_collection_root(resolved);
if !is_collection || !field_resolver.leaf_is_json_bridged_via_swift_map(resolved) {
return None;
}
let json_text_expr = if field_expr.contains("?.") {
format!("({field_expr}?.toString() ?? \"null\")")
} else {
format!("{field_expr}.toString()")
};
Some(format!(
"((try? JSONSerialization.jsonObject(with: Data({json_text_expr}.utf8))) as? [Any])?.count ?? 0"
))
}
#[cfg(test)]
mod json_bridged_count_tests {
use super::swift_json_bridged_count_expr;
use crate::e2e::field_access::{FieldResolver, SwiftFirstClassMap};
use std::collections::{HashMap, HashSet};
fn resolver_with_json_bridged_array(field_name: &str) -> FieldResolver {
let swift_first_class_map = SwiftFirstClassMap {
json_bridged_field_names: HashSet::from([field_name.to_string()]),
..SwiftFirstClassMap::default()
};
FieldResolver::new_with_swift_first_class(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::from([field_name.to_string()]),
&HashSet::new(),
&HashMap::new(),
swift_first_class_map,
)
}
#[test]
fn bare_json_bridged_array_leaf_yields_a_real_decode_and_count_expression() {
let resolver = resolver_with_json_bridged_array("chunks");
let expr = swift_json_bridged_count_expr(&resolver, Some("chunks"), "result.results()[0].chunks()");
let Some(expr) = expr else {
panic!("a JSON-bridged array leaf must yield a real count expression");
};
assert_eq!(
expr,
"((try? JSONSerialization.jsonObject(with: Data(result.results()[0].chunks().toString().utf8))) \
as? [Any])?.count ?? 0"
);
}
#[test]
fn optional_ancestor_chain_coalesces_to_null_json_text() {
let resolver = resolver_with_json_bridged_array("chunks");
let expr = swift_json_bridged_count_expr(&resolver, Some("chunks"), "result.document()?.chunks()");
let Some(expr) = expr else {
panic!("a JSON-bridged array leaf behind an optional ancestor must still yield a count expression");
};
assert!(
expr.contains("(result.document()?.chunks()?.toString() ?? \"null\")"),
"got: {expr}"
);
}
#[test]
fn non_collection_field_yields_no_count_expression() {
let empty = HashSet::new();
let resolver = FieldResolver::new(&HashMap::new(), &empty, &empty, &empty, &empty);
let expr = swift_json_bridged_count_expr(&resolver, Some("title"), "result.title()");
assert!(expr.is_none(), "got: {expr:?}");
}
#[test]
fn no_field_yields_no_count_expression() {
let resolver = resolver_with_json_bridged_array("chunks");
let expr = swift_json_bridged_count_expr(&resolver, None, "result");
assert!(expr.is_none(), "got: {expr:?}");
}
}
pub(super) fn unspellable_collection_emptiness_skip(
field_resolver: &FieldResolver,
field: Option<&str>,
) -> Option<String> {
let field = field.filter(|f| !f.is_empty())?;
let resolved = field_resolver.resolve(field);
let is_collection = field_resolver.is_array(field)
|| field_resolver.is_array(resolved)
|| field_resolver.is_collection_root(field)
|| field_resolver.is_collection_root(resolved);
if !is_collection || !field_resolver.leaf_is_json_bridged_via_swift_map(resolved) {
return None;
}
Some(skip_line(FieldSkip::CountOnJsonBridgedLeafInSwift, field))
}
pub(super) fn non_countable_leaf_skip_line(field: Option<&str>) -> String {
skip_line(
FieldSkip::CountOnJsonBridgedLeafInSwift,
field
.filter(|f| !f.is_empty())
.unwrap_or(super::assertions::BARE_RESULT_TOKEN),
)
}
pub(super) fn leaf_getter_is_optional(field_resolver: &FieldResolver, field: Option<&str>) -> bool {
field
.filter(|f| !f.is_empty())
.and_then(|f| field_resolver.swift_leaf_getter_is_optional(f))
.unwrap_or(false)
}
fn skip_line(kind: FieldSkip, field: &str) -> String {
format!(" // skipped: {}\n", kind.message(field))
}
pub(super) fn mixed_map_then_vec_traversal_skip(field: &str) -> String {
skip_line(FieldSkip::MixedMapThenVecTraversalInSwift, field)
}
#[cfg(test)]
mod mixed_map_then_vec_tests {
use super::json_bridged_traversal_skip;
use crate::e2e::field_access::{FieldResolver, SwiftFirstClassMap};
use std::collections::{HashMap, HashSet};
fn ir_backed_resolver() -> FieldResolver {
let swift_first_class_map = SwiftFirstClassMap {
json_bridged_field_names: HashSet::from(["labels".to_string()]),
..SwiftFirstClassMap::default()
};
FieldResolver::new_with_swift_first_class(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashMap::new(),
swift_first_class_map,
)
}
#[test]
fn ir_backed_json_bridged_map_field_is_refused_before_an_accessor_is_built() {
let resolver = ir_backed_resolver();
let skip = json_bridged_traversal_skip(&resolver, Some("labels[key].items[0]"));
let Some(skip) = skip else {
panic!("IR-backed json-bridged map field must be refused before accessor-building");
};
assert!(skip.contains("'labels'"), "got: {skip}");
}
#[test]
fn opaque_resolver_does_not_refuse_here_the_gap_is_closed_downstream() {
let empty = HashSet::new();
let resolver = FieldResolver::new(&HashMap::new(), &empty, &empty, &empty, &empty);
let skip = json_bridged_traversal_skip(&resolver, Some("labels[key].items[0]"));
assert!(
skip.is_none(),
"an IR-less resolver has no positive fact to refuse on; got: {skip:?}"
);
}
}