use alef_codegen::naming::to_go_name;
use heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase};
use std::collections::{HashMap, HashSet};
pub struct FieldResolver {
aliases: HashMap<String, String>,
optional_fields: HashSet<String>,
result_fields: HashSet<String>,
array_fields: HashSet<String>,
method_calls: HashSet<String>,
error_field_aliases: HashMap<String, String>,
php_getter_map: PhpGetterMap,
}
#[derive(Debug, Clone, Default)]
pub struct PhpGetterMap {
pub getters: HashMap<String, HashSet<String>>,
pub field_types: HashMap<String, HashMap<String, String>>,
pub root_type: Option<String>,
pub all_fields: HashMap<String, HashSet<String>>,
}
impl PhpGetterMap {
pub fn needs_getter(&self, owner_type: Option<&str>, field_name: &str) -> bool {
if let Some(t) = owner_type {
let owner_has_field = self.all_fields.get(t).is_some_and(|s| s.contains(field_name));
if owner_has_field {
if let Some(fields) = self.getters.get(t) {
return fields.contains(field_name);
}
}
}
self.getters.values().any(|set| set.contains(field_name))
}
pub fn advance(&self, owner_type: Option<&str>, field_name: &str) -> Option<String> {
let owner = owner_type?;
self.field_types.get(owner).and_then(|m| m.get(field_name).cloned())
}
pub fn is_empty(&self) -> bool {
self.getters.is_empty()
}
}
#[derive(Debug, Clone)]
enum PathSegment {
Field(String),
ArrayField { name: String, index: usize },
MapAccess { field: String, key: String },
Length,
}
impl FieldResolver {
pub fn new(
fields: &HashMap<String, String>,
optional: &HashSet<String>,
result_fields: &HashSet<String>,
array_fields: &HashSet<String>,
method_calls: &HashSet<String>,
) -> Self {
Self {
aliases: fields.clone(),
optional_fields: optional.clone(),
result_fields: result_fields.clone(),
array_fields: array_fields.clone(),
method_calls: method_calls.clone(),
error_field_aliases: HashMap::new(),
php_getter_map: PhpGetterMap::default(),
}
}
pub fn new_with_error_aliases(
fields: &HashMap<String, String>,
optional: &HashSet<String>,
result_fields: &HashSet<String>,
array_fields: &HashSet<String>,
method_calls: &HashSet<String>,
error_field_aliases: &HashMap<String, String>,
) -> Self {
Self {
aliases: fields.clone(),
optional_fields: optional.clone(),
result_fields: result_fields.clone(),
array_fields: array_fields.clone(),
method_calls: method_calls.clone(),
error_field_aliases: error_field_aliases.clone(),
php_getter_map: PhpGetterMap::default(),
}
}
pub fn new_with_php_getters(
fields: &HashMap<String, String>,
optional: &HashSet<String>,
result_fields: &HashSet<String>,
array_fields: &HashSet<String>,
method_calls: &HashSet<String>,
error_field_aliases: &HashMap<String, String>,
php_getter_map: PhpGetterMap,
) -> Self {
Self {
aliases: fields.clone(),
optional_fields: optional.clone(),
result_fields: result_fields.clone(),
array_fields: array_fields.clone(),
method_calls: method_calls.clone(),
error_field_aliases: error_field_aliases.clone(),
php_getter_map,
}
}
pub fn resolve<'a>(&'a self, fixture_field: &'a str) -> &'a str {
self.aliases
.get(fixture_field)
.map(String::as_str)
.unwrap_or(fixture_field)
}
pub fn is_optional(&self, field: &str) -> bool {
if self.optional_fields.contains(field) {
return true;
}
let index_normalized = normalize_numeric_indices(field);
if index_normalized != field && self.optional_fields.contains(index_normalized.as_str()) {
return true;
}
let de_indexed = strip_numeric_indices(field);
if de_indexed != field && self.optional_fields.contains(de_indexed.as_str()) {
return true;
}
let normalized = field.replace("[].", ".");
if normalized != field && self.optional_fields.contains(normalized.as_str()) {
return true;
}
for af in &self.array_fields {
if let Some(rest) = field.strip_prefix(af.as_str()) {
if let Some(rest) = rest.strip_prefix('.') {
let with_bracket = format!("{af}[].{rest}");
if self.optional_fields.contains(with_bracket.as_str()) {
return true;
}
}
}
}
false
}
pub fn has_alias(&self, fixture_field: &str) -> bool {
self.aliases.contains_key(fixture_field)
}
pub fn has_explicit_field(&self, field_name: &str) -> bool {
if self.result_fields.is_empty() {
return false;
}
self.result_fields.contains(field_name)
}
pub fn is_valid_for_result(&self, fixture_field: &str) -> bool {
if self.result_fields.is_empty() {
return true;
}
let resolved = self.resolve(fixture_field);
let first_segment = resolved.split('.').next().unwrap_or(resolved);
let first_segment = first_segment.split('[').next().unwrap_or(first_segment);
self.result_fields.contains(first_segment)
}
pub fn is_array(&self, field: &str) -> bool {
self.array_fields.contains(field)
}
pub fn is_collection_root(&self, field: &str) -> bool {
let prefix = format!("{field}[");
self.array_fields.iter().any(|af| af.starts_with(&prefix))
|| self.optional_fields.iter().any(|of| of.starts_with(&prefix))
}
pub fn tagged_union_split(&self, fixture_field: &str) -> Option<(String, String, String)> {
let resolved = self.resolve(fixture_field);
let segments: Vec<&str> = resolved.split('.').collect();
let mut path_so_far = String::new();
for (i, seg) in segments.iter().enumerate() {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(seg);
if self.method_calls.contains(&path_so_far) {
let prefix = segments[..i].join(".");
let variant = (*seg).to_string();
let suffix = segments[i + 1..].join(".");
return Some((prefix, variant, suffix));
}
}
None
}
pub fn has_map_access(&self, fixture_field: &str) -> bool {
let resolved = self.resolve(fixture_field);
let segments = parse_path(resolved);
segments.iter().any(|s| {
if let PathSegment::MapAccess { key, .. } = s {
!key.chars().all(|c| c.is_ascii_digit())
} else {
false
}
})
}
pub fn accessor(&self, fixture_field: &str, language: &str, result_var: &str) -> String {
let resolved = self.resolve(fixture_field);
let segments = parse_path(resolved);
let segments = self.inject_array_indexing(segments);
match language {
"java" => render_java_with_optionals(&segments, result_var, &self.optional_fields),
"kotlin" => render_kotlin_with_optionals(&segments, result_var, &self.optional_fields),
"kotlin_android" => render_kotlin_android_with_optionals(&segments, result_var, &self.optional_fields),
"rust" => render_rust_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
"csharp" => render_csharp_with_optionals(&segments, result_var, &self.optional_fields),
"zig" => render_zig_with_optionals(&segments, result_var, &self.optional_fields, &self.method_calls),
"swift" => render_swift_with_optionals(&segments, result_var, &self.optional_fields),
"dart" => render_dart_with_optionals(&segments, result_var, &self.optional_fields),
"php" if !self.php_getter_map.is_empty() => {
render_php_with_getters(&segments, result_var, &self.php_getter_map)
}
_ => render_accessor(&segments, language, result_var),
}
}
pub fn accessor_for_error(&self, sub_field: &str, language: &str, err_var: &str) -> String {
let resolved = self
.error_field_aliases
.get(sub_field)
.map(String::as_str)
.unwrap_or(sub_field);
let segments = parse_path(resolved);
match language {
"rust" => render_rust_with_optionals(&segments, err_var, &self.optional_fields, &self.method_calls),
_ => render_accessor(&segments, language, err_var),
}
}
pub fn has_error_aliases(&self) -> bool {
!self.error_field_aliases.is_empty()
}
fn inject_array_indexing(&self, segments: Vec<PathSegment>) -> Vec<PathSegment> {
if self.array_fields.is_empty() {
return segments;
}
let len = segments.len();
let mut result = Vec::with_capacity(len);
let mut path_so_far = String::new();
for i in 0..len {
let seg = &segments[i];
match seg {
PathSegment::Field(f) => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(f);
let next_is_length = i + 1 < len && matches!(segments[i + 1], PathSegment::Length);
if i + 1 < len && self.array_fields.contains(&path_so_far) && !next_is_length {
result.push(PathSegment::ArrayField {
name: f.clone(),
index: 0,
});
} else {
result.push(seg.clone());
}
}
PathSegment::ArrayField { .. } => {
result.push(seg.clone());
}
PathSegment::MapAccess { field, key } => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(field);
let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
if is_numeric && self.array_fields.contains(&path_so_far) {
let index: usize = key.parse().unwrap_or(0);
result.push(PathSegment::ArrayField {
name: field.clone(),
index,
});
} else {
result.push(seg.clone());
}
}
_ => {
result.push(seg.clone());
}
}
}
result
}
pub fn rust_unwrap_binding(&self, fixture_field: &str, result_var: &str) -> Option<(String, String)> {
let resolved = self.resolve(fixture_field);
if !self.is_optional(resolved) {
return None;
}
let segments = parse_path(resolved);
let segments = self.inject_array_indexing(segments);
let local_var = {
let raw = resolved.replace(['.', '['], "_").replace(']', "");
let mut collapsed = String::with_capacity(raw.len());
let mut prev_underscore = false;
for ch in raw.chars() {
if ch == '_' {
if !prev_underscore {
collapsed.push('_');
}
prev_underscore = true;
} else {
collapsed.push(ch);
prev_underscore = false;
}
}
collapsed.trim_matches('_').to_string()
};
let accessor = render_accessor(&segments, "rust", result_var);
let has_map_access = segments.iter().any(|s| {
if let PathSegment::MapAccess { key, .. } = s {
!key.chars().all(|c| c.is_ascii_digit())
} else {
false
}
});
let is_array = self.is_array(resolved);
let binding = if has_map_access {
format!("let {local_var} = {accessor}.unwrap_or(\"\");")
} else if is_array {
format!("let {local_var} = {accessor}.as_deref().unwrap_or(&[]);")
} else {
format!("let {local_var} = {accessor}.as_ref().map(|v| v.to_string()).unwrap_or_default();")
};
Some((binding, local_var))
}
}
fn strip_numeric_indices(path: &str) -> String {
let mut result = String::with_capacity(path.len());
let mut chars = path.chars().peekable();
while let Some(c) = chars.next() {
if c == '[' {
let mut key = String::new();
let mut closed = false;
for inner in chars.by_ref() {
if inner == ']' {
closed = true;
break;
}
key.push(inner);
}
if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
} else {
result.push('[');
result.push_str(&key);
if closed {
result.push(']');
}
}
} else {
result.push(c);
}
}
while result.contains("..") {
result = result.replace("..", ".");
}
if result.starts_with('.') {
result.remove(0);
}
result
}
fn normalize_numeric_indices(path: &str) -> String {
let mut result = String::with_capacity(path.len());
let mut chars = path.chars().peekable();
while let Some(c) = chars.next() {
if c == '[' {
let mut key = String::new();
let mut closed = false;
for inner in chars.by_ref() {
if inner == ']' {
closed = true;
break;
}
key.push(inner);
}
if closed && !key.is_empty() && key.chars().all(|k| k.is_ascii_digit()) {
result.push_str("[0]");
} else {
result.push('[');
result.push_str(&key);
if closed {
result.push(']');
}
}
} else {
result.push(c);
}
}
result
}
fn parse_path(path: &str) -> Vec<PathSegment> {
let mut segments = Vec::new();
for part in path.split('.') {
if part == "length" || part == "count" || part == "size" {
segments.push(PathSegment::Length);
} else if let Some(bracket_pos) = part.find('[') {
let name = part[..bracket_pos].to_string();
let key = part[bracket_pos + 1..].trim_end_matches(']').to_string();
if key.is_empty() {
segments.push(PathSegment::ArrayField { name, index: 0 });
} else if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
let index: usize = key.parse().unwrap_or(0);
segments.push(PathSegment::ArrayField { name, index });
} else {
segments.push(PathSegment::MapAccess { field: name, key });
}
} else {
segments.push(PathSegment::Field(part.to_string()));
}
}
segments
}
fn render_accessor(segments: &[PathSegment], language: &str, result_var: &str) -> String {
match language {
"rust" => render_rust(segments, result_var),
"python" => render_dot_access(segments, result_var, "python"),
"typescript" | "node" => render_typescript(segments, result_var),
"wasm" => render_wasm(segments, result_var),
"go" => render_go(segments, result_var),
"java" => render_java(segments, result_var),
"kotlin" => render_kotlin(segments, result_var),
"kotlin_android" => render_kotlin_android(segments, result_var),
"csharp" => render_pascal_dot(segments, result_var),
"ruby" => render_dot_access(segments, result_var, "ruby"),
"php" => render_php(segments, result_var),
"elixir" => render_dot_access(segments, result_var, "elixir"),
"r" => render_r(segments, result_var),
"c" => render_c(segments, result_var),
"swift" => render_swift(segments, result_var),
"dart" => render_dart(segments, result_var),
_ => render_dot_access(segments, result_var, language),
}
}
fn render_swift(segments: &[PathSegment], result_var: &str) -> String {
let mut out = result_var.to_string();
for seg in segments {
match seg {
PathSegment::Field(f) => {
out.push('.');
out.push_str(f);
}
PathSegment::ArrayField { name, index } => {
out.push('.');
out.push_str(name);
out.push_str(&format!("[{index}]"));
}
PathSegment::MapAccess { field, key } => {
out.push('.');
out.push_str(field);
if key.chars().all(|c| c.is_ascii_digit()) {
out.push_str(&format!("[{key}]"));
} else {
out.push_str(&format!("[\"{key}\"]"));
}
}
PathSegment::Length => {
out.push_str(".count");
}
}
}
out
}
fn render_swift_with_optionals(
segments: &[PathSegment],
result_var: &str,
optional_fields: &HashSet<String>,
) -> String {
let mut out = result_var.to_string();
let mut path_so_far = String::new();
let total = segments.len();
for (i, seg) in segments.iter().enumerate() {
let is_leaf = i == total - 1;
match seg {
PathSegment::Field(f) => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(f);
out.push('.');
out.push_str(f);
if !is_leaf && optional_fields.contains(&path_so_far) {
out.push('?');
}
}
PathSegment::ArrayField { name, index } => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(name);
let is_optional = optional_fields.contains(&path_so_far);
out.push('.');
out.push_str(name);
if is_optional {
out.push_str(&format!("?[{index}]"));
} else {
out.push_str(&format!("[{index}]"));
}
path_so_far.push_str("[0]");
let _ = is_leaf;
}
PathSegment::MapAccess { field, key } => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(field);
out.push('.');
out.push_str(field);
if key.chars().all(|c| c.is_ascii_digit()) {
out.push_str(&format!("[{key}]"));
} else {
out.push_str(&format!("[\"{key}\"]"));
}
}
PathSegment::Length => {
out.push_str(".count");
}
}
}
out
}
fn render_rust(segments: &[PathSegment], result_var: &str) -> String {
let mut out = result_var.to_string();
for seg in segments {
match seg {
PathSegment::Field(f) => {
out.push('.');
out.push_str(&f.to_snake_case());
}
PathSegment::ArrayField { name, index } => {
out.push('.');
out.push_str(&name.to_snake_case());
out.push_str(&format!("[{index}]"));
}
PathSegment::MapAccess { field, key } => {
out.push('.');
out.push_str(&field.to_snake_case());
if key.chars().all(|c| c.is_ascii_digit()) {
out.push_str(&format!("[{key}]"));
} else {
out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
}
}
PathSegment::Length => {
out.push_str(".len()");
}
}
}
out
}
fn render_dot_access(segments: &[PathSegment], result_var: &str, language: &str) -> String {
let mut out = result_var.to_string();
for seg in segments {
match seg {
PathSegment::Field(f) => {
out.push('.');
out.push_str(f);
}
PathSegment::ArrayField { name, index } => {
if language == "elixir" {
let current = std::mem::take(&mut out);
out = format!("Enum.at({current}.{name}, {index})");
} else {
out.push('.');
out.push_str(name);
out.push_str(&format!("[{index}]"));
}
}
PathSegment::MapAccess { field, key } => {
let is_numeric = key.chars().all(|c| c.is_ascii_digit());
if is_numeric && language == "elixir" {
let current = std::mem::take(&mut out);
out = format!("Enum.at({current}.{field}, {key})");
} else {
out.push('.');
out.push_str(field);
if is_numeric {
let idx: usize = key.parse().unwrap_or(0);
out.push_str(&format!("[{idx}]"));
} else if language == "elixir" || language == "ruby" {
out.push_str(&format!("[\"{key}\"]"));
} else {
out.push_str(&format!(".get(\"{key}\")"));
}
}
}
PathSegment::Length => match language {
"ruby" => out.push_str(".length"),
"elixir" => {
let current = std::mem::take(&mut out);
out = format!("length({current})");
}
"gleam" => {
let current = std::mem::take(&mut out);
out = format!("list.length({current})");
}
_ => {
let current = std::mem::take(&mut out);
out = format!("len({current})");
}
},
}
}
out
}
fn render_typescript(segments: &[PathSegment], result_var: &str) -> String {
let mut out = result_var.to_string();
for seg in segments {
match seg {
PathSegment::Field(f) => {
out.push('.');
out.push_str(&f.to_lower_camel_case());
}
PathSegment::ArrayField { name, index } => {
out.push('.');
out.push_str(&name.to_lower_camel_case());
out.push_str(&format!("[{index}]"));
}
PathSegment::MapAccess { field, key } => {
out.push('.');
out.push_str(&field.to_lower_camel_case());
if !key.is_empty() && key.chars().all(|c| c.is_ascii_digit()) {
out.push_str(&format!("[{key}]"));
} else {
out.push_str(&format!("[\"{key}\"]"));
}
}
PathSegment::Length => {
out.push_str(".length");
}
}
}
out
}
fn render_wasm(segments: &[PathSegment], result_var: &str) -> String {
let mut out = result_var.to_string();
for seg in segments {
match seg {
PathSegment::Field(f) => {
out.push('.');
out.push_str(&f.to_lower_camel_case());
}
PathSegment::ArrayField { name, index } => {
out.push('.');
out.push_str(&name.to_lower_camel_case());
out.push_str(&format!("[{index}]"));
}
PathSegment::MapAccess { field, key } => {
out.push('.');
out.push_str(&field.to_lower_camel_case());
out.push_str(&format!(".get(\"{key}\")"));
}
PathSegment::Length => {
out.push_str(".length");
}
}
}
out
}
fn render_go(segments: &[PathSegment], result_var: &str) -> String {
let mut out = result_var.to_string();
for seg in segments {
match seg {
PathSegment::Field(f) => {
out.push('.');
out.push_str(&to_go_name(f));
}
PathSegment::ArrayField { name, index } => {
out.push('.');
out.push_str(&to_go_name(name));
out.push_str(&format!("[{index}]"));
}
PathSegment::MapAccess { field, key } => {
out.push('.');
out.push_str(&to_go_name(field));
if key.chars().all(|c| c.is_ascii_digit()) {
out.push_str(&format!("[{key}]"));
} else {
out.push_str(&format!("[\"{key}\"]"));
}
}
PathSegment::Length => {
let current = std::mem::take(&mut out);
out = format!("len({current})");
}
}
}
out
}
fn render_java(segments: &[PathSegment], result_var: &str) -> String {
let mut out = result_var.to_string();
for seg in segments {
match seg {
PathSegment::Field(f) => {
out.push('.');
out.push_str(&f.to_lower_camel_case());
out.push_str("()");
}
PathSegment::ArrayField { name, index } => {
out.push('.');
out.push_str(&name.to_lower_camel_case());
out.push_str(&format!("().get({index})"));
}
PathSegment::MapAccess { field, key } => {
out.push('.');
out.push_str(&field.to_lower_camel_case());
let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
if is_numeric {
out.push_str(&format!("().get({key})"));
} else {
out.push_str(&format!("().get(\"{key}\")"));
}
}
PathSegment::Length => {
out.push_str(".size()");
}
}
}
out
}
fn kotlin_getter(name: &str) -> String {
let camel = name.to_lower_camel_case();
match camel.as_str() {
"as" | "break" | "class" | "continue" | "do" | "else" | "false" | "for" | "fun" | "if" | "in" | "interface"
| "is" | "null" | "object" | "package" | "return" | "super" | "this" | "throw" | "true" | "try"
| "typealias" | "typeof" | "val" | "var" | "when" | "while" => format!("`{camel}`"),
_ => camel,
}
}
fn render_kotlin(segments: &[PathSegment], result_var: &str) -> String {
let mut out = result_var.to_string();
for seg in segments {
match seg {
PathSegment::Field(f) => {
out.push('.');
out.push_str(&kotlin_getter(f));
out.push_str("()");
}
PathSegment::ArrayField { name, index } => {
out.push('.');
out.push_str(&kotlin_getter(name));
if *index == 0 {
out.push_str("().first()");
} else {
out.push_str(&format!("().get({index})"));
}
}
PathSegment::MapAccess { field, key } => {
out.push('.');
out.push_str(&kotlin_getter(field));
let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
if is_numeric {
out.push_str(&format!("().get({key})"));
} else {
out.push_str(&format!("().get(\"{key}\")"));
}
}
PathSegment::Length => {
out.push_str(".size");
}
}
}
out
}
fn render_java_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
let mut out = result_var.to_string();
let mut path_so_far = String::new();
for (i, seg) in segments.iter().enumerate() {
let is_leaf = i == segments.len() - 1;
match seg {
PathSegment::Field(f) => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(f);
out.push('.');
out.push_str(&f.to_lower_camel_case());
out.push_str("()");
let _ = is_leaf;
let _ = optional_fields;
}
PathSegment::ArrayField { name, index } => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(name);
out.push('.');
out.push_str(&name.to_lower_camel_case());
out.push_str(&format!("().get({index})"));
}
PathSegment::MapAccess { field, key } => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(field);
out.push('.');
out.push_str(&field.to_lower_camel_case());
let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
if is_numeric {
out.push_str(&format!("().get({key})"));
} else {
out.push_str(&format!("().get(\"{key}\")"));
}
}
PathSegment::Length => {
out.push_str(".size()");
}
}
}
out
}
fn render_kotlin_with_optionals(
segments: &[PathSegment],
result_var: &str,
optional_fields: &HashSet<String>,
) -> String {
let mut out = result_var.to_string();
let mut path_so_far = String::new();
let mut prev_was_nullable = false;
for seg in segments {
let nav = if prev_was_nullable { "?." } else { "." };
match seg {
PathSegment::Field(f) => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(f);
let is_optional = optional_fields.contains(&path_so_far);
out.push_str(nav);
out.push_str(&kotlin_getter(f));
out.push_str("()");
prev_was_nullable = prev_was_nullable || is_optional;
}
PathSegment::ArrayField { name, index } => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(name);
let is_optional = optional_fields.contains(&path_so_far);
out.push_str(nav);
out.push_str(&kotlin_getter(name));
let safe = if prev_was_nullable || is_optional { "?" } else { "" };
if *index == 0 {
out.push_str(&format!("(){safe}.first()"));
} else {
out.push_str(&format!("(){safe}.get({index})"));
}
path_so_far.push_str("[0]");
prev_was_nullable = prev_was_nullable || is_optional;
}
PathSegment::MapAccess { field, key } => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(field);
let is_optional = optional_fields.contains(&path_so_far);
out.push_str(nav);
out.push_str(&kotlin_getter(field));
let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
if is_numeric {
if prev_was_nullable || is_optional {
out.push_str(&format!("()?.get({key})"));
} else {
out.push_str(&format!("().get({key})"));
}
} else if prev_was_nullable || is_optional {
out.push_str(&format!("()?.get(\"{key}\")"));
} else {
out.push_str(&format!("().get(\"{key}\")"));
}
prev_was_nullable = prev_was_nullable || is_optional;
}
PathSegment::Length => {
let size_nav = if prev_was_nullable { "?" } else { "" };
out.push_str(&format!("{size_nav}.size"));
prev_was_nullable = false;
}
}
}
out
}
fn render_kotlin_android_with_optionals(
segments: &[PathSegment],
result_var: &str,
optional_fields: &HashSet<String>,
) -> String {
let mut out = result_var.to_string();
let mut path_so_far = String::new();
let mut prev_was_nullable = false;
for seg in segments {
let nav = if prev_was_nullable { "?." } else { "." };
match seg {
PathSegment::Field(f) => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(f);
let is_optional = optional_fields.contains(&path_so_far);
out.push_str(nav);
out.push_str(&kotlin_getter(f));
prev_was_nullable = prev_was_nullable || is_optional;
}
PathSegment::ArrayField { name, index } => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(name);
let is_optional = optional_fields.contains(&path_so_far);
out.push_str(nav);
out.push_str(&kotlin_getter(name));
let safe = if prev_was_nullable || is_optional { "?" } else { "" };
if *index == 0 {
out.push_str(&format!("{safe}.first()"));
} else {
out.push_str(&format!("{safe}.get({index})"));
}
path_so_far.push_str("[0]");
prev_was_nullable = prev_was_nullable || is_optional;
}
PathSegment::MapAccess { field, key } => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(field);
let is_optional = optional_fields.contains(&path_so_far);
out.push_str(nav);
out.push_str(&kotlin_getter(field));
let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
if is_numeric {
if prev_was_nullable || is_optional {
out.push_str(&format!("?.get({key})"));
} else {
out.push_str(&format!(".get({key})"));
}
} else if prev_was_nullable || is_optional {
out.push_str(&format!("?.get(\"{key}\")"));
} else {
out.push_str(&format!(".get(\"{key}\")"));
}
prev_was_nullable = prev_was_nullable || is_optional;
}
PathSegment::Length => {
let size_nav = if prev_was_nullable { "?" } else { "" };
out.push_str(&format!("{size_nav}.size"));
prev_was_nullable = false;
}
}
}
out
}
fn render_kotlin_android(segments: &[PathSegment], result_var: &str) -> String {
let mut out = result_var.to_string();
for seg in segments {
match seg {
PathSegment::Field(f) => {
out.push('.');
out.push_str(&kotlin_getter(f));
}
PathSegment::ArrayField { name, index } => {
out.push('.');
out.push_str(&kotlin_getter(name));
if *index == 0 {
out.push_str(".first()");
} else {
out.push_str(&format!(".get({index})"));
}
}
PathSegment::MapAccess { field, key } => {
out.push('.');
out.push_str(&kotlin_getter(field));
let is_numeric = !key.is_empty() && key.chars().all(|c| c.is_ascii_digit());
if is_numeric {
out.push_str(&format!(".get({key})"));
} else {
out.push_str(&format!(".get(\"{key}\")"));
}
}
PathSegment::Length => {
out.push_str(".size");
}
}
}
out
}
fn render_rust_with_optionals(
segments: &[PathSegment],
result_var: &str,
optional_fields: &HashSet<String>,
method_calls: &HashSet<String>,
) -> String {
let mut out = result_var.to_string();
let mut path_so_far = String::new();
for (i, seg) in segments.iter().enumerate() {
let is_leaf = i == segments.len() - 1;
match seg {
PathSegment::Field(f) => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(f);
out.push('.');
out.push_str(&f.to_snake_case());
let is_method = method_calls.contains(&path_so_far);
if is_method {
out.push_str("()");
if !is_leaf && optional_fields.contains(&path_so_far) {
out.push_str(".as_ref().unwrap()");
}
} else if !is_leaf && optional_fields.contains(&path_so_far) {
out.push_str(".as_ref().unwrap()");
}
}
PathSegment::ArrayField { name, index } => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(name);
out.push('.');
out.push_str(&name.to_snake_case());
let path_with_idx = format!("{path_so_far}[0]");
let is_opt = optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
if is_opt {
out.push_str(&format!(".as_ref().unwrap()[{index}]"));
} else {
out.push_str(&format!("[{index}]"));
}
path_so_far.push_str("[0]");
}
PathSegment::MapAccess { field, key } => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(field);
out.push('.');
out.push_str(&field.to_snake_case());
if key.chars().all(|c| c.is_ascii_digit()) {
let path_with_idx = format!("{path_so_far}[0]");
let is_opt =
optional_fields.contains(&path_so_far) || optional_fields.contains(path_with_idx.as_str());
if is_opt {
out.push_str(&format!(".as_ref().unwrap()[{key}]"));
} else {
out.push_str(&format!("[{key}]"));
}
path_so_far.push_str("[0]");
} else {
out.push_str(&format!(".get(\"{key}\").map(|s| s.as_str())"));
}
}
PathSegment::Length => {
out.push_str(".len()");
}
}
}
out
}
fn render_zig_with_optionals(
segments: &[PathSegment],
result_var: &str,
optional_fields: &HashSet<String>,
method_calls: &HashSet<String>,
) -> String {
let mut out = result_var.to_string();
let mut path_so_far = String::new();
for seg in segments {
match seg {
PathSegment::Field(f) => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(f);
out.push('.');
out.push_str(f);
if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
out.push_str(".?");
}
}
PathSegment::ArrayField { name, index } => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(name);
out.push('.');
out.push_str(name);
if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
out.push_str(".?");
}
out.push_str(&format!("[{index}]"));
}
PathSegment::MapAccess { field, key } => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(field);
out.push('.');
out.push_str(field);
if !method_calls.contains(&path_so_far) && optional_fields.contains(&path_so_far) {
out.push_str(".?");
}
if key.chars().all(|c| c.is_ascii_digit()) {
out.push_str(&format!("[{key}]"));
} else {
out.push_str(&format!(".get(\"{key}\")"));
}
}
PathSegment::Length => {
out.push_str(".len");
}
}
}
out
}
fn render_pascal_dot(segments: &[PathSegment], result_var: &str) -> String {
let mut out = result_var.to_string();
for seg in segments {
match seg {
PathSegment::Field(f) => {
out.push('.');
out.push_str(&f.to_pascal_case());
}
PathSegment::ArrayField { name, index } => {
out.push('.');
out.push_str(&name.to_pascal_case());
out.push_str(&format!("[{index}]"));
}
PathSegment::MapAccess { field, key } => {
out.push('.');
out.push_str(&field.to_pascal_case());
if key.chars().all(|c| c.is_ascii_digit()) {
out.push_str(&format!("[{key}]"));
} else {
out.push_str(&format!("[\"{key}\"]"));
}
}
PathSegment::Length => {
out.push_str(".Count");
}
}
}
out
}
fn render_csharp_with_optionals(
segments: &[PathSegment],
result_var: &str,
optional_fields: &HashSet<String>,
) -> String {
let mut out = result_var.to_string();
let mut path_so_far = String::new();
for (i, seg) in segments.iter().enumerate() {
let is_leaf = i == segments.len() - 1;
match seg {
PathSegment::Field(f) => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(f);
out.push('.');
out.push_str(&f.to_pascal_case());
if !is_leaf && optional_fields.contains(&path_so_far) {
out.push('!');
}
}
PathSegment::ArrayField { name, index } => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(name);
out.push('.');
out.push_str(&name.to_pascal_case());
out.push_str(&format!("[{index}]"));
}
PathSegment::MapAccess { field, key } => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(field);
out.push('.');
out.push_str(&field.to_pascal_case());
if key.chars().all(|c| c.is_ascii_digit()) {
out.push_str(&format!("[{key}]"));
} else {
out.push_str(&format!("[\"{key}\"]"));
}
}
PathSegment::Length => {
out.push_str(".Count");
}
}
}
out
}
fn render_php(segments: &[PathSegment], result_var: &str) -> String {
let mut out = result_var.to_string();
for seg in segments {
match seg {
PathSegment::Field(f) => {
out.push_str("->");
out.push_str(&f.to_lower_camel_case());
}
PathSegment::ArrayField { name, index } => {
out.push_str("->");
out.push_str(&name.to_lower_camel_case());
out.push_str(&format!("[{index}]"));
}
PathSegment::MapAccess { field, key } => {
out.push_str("->");
out.push_str(&field.to_lower_camel_case());
out.push_str(&format!("[\"{key}\"]"));
}
PathSegment::Length => {
let current = std::mem::take(&mut out);
out = format!("count({current})");
}
}
}
out
}
fn render_php_with_getters(segments: &[PathSegment], result_var: &str, getter_map: &PhpGetterMap) -> String {
let mut out = result_var.to_string();
let mut current_type: Option<String> = getter_map.root_type.clone();
for seg in segments {
match seg {
PathSegment::Field(f) => {
let camel = f.to_lower_camel_case();
if getter_map.needs_getter(current_type.as_deref(), f.as_str()) {
let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
out.push_str("->");
out.push_str(&getter);
out.push_str("()");
} else {
out.push_str("->");
out.push_str(&camel);
}
current_type = getter_map.advance(current_type.as_deref(), f.as_str());
}
PathSegment::ArrayField { name, index } => {
let camel = name.to_lower_camel_case();
if getter_map.needs_getter(current_type.as_deref(), name.as_str()) {
let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
out.push_str("->");
out.push_str(&getter);
out.push_str("()");
} else {
out.push_str("->");
out.push_str(&camel);
}
out.push_str(&format!("[{index}]"));
current_type = getter_map.advance(current_type.as_deref(), name.as_str());
}
PathSegment::MapAccess { field, key } => {
let camel = field.to_lower_camel_case();
if getter_map.needs_getter(current_type.as_deref(), field.as_str()) {
let getter = format!("get{}", camel.as_str()[..1].to_uppercase() + &camel[1..]);
out.push_str("->");
out.push_str(&getter);
out.push_str("()");
} else {
out.push_str("->");
out.push_str(&camel);
}
out.push_str(&format!("[\"{key}\"]"));
current_type = getter_map.advance(current_type.as_deref(), field.as_str());
}
PathSegment::Length => {
let current = std::mem::take(&mut out);
out = format!("count({current})");
}
}
}
out
}
fn render_r(segments: &[PathSegment], result_var: &str) -> String {
let mut out = result_var.to_string();
for seg in segments {
match seg {
PathSegment::Field(f) => {
out.push('$');
out.push_str(f);
}
PathSegment::ArrayField { name, index } => {
out.push('$');
out.push_str(name);
out.push_str(&format!("[[{}]]", index + 1));
}
PathSegment::MapAccess { field, key } => {
out.push('$');
out.push_str(field);
out.push_str(&format!("[[\"{key}\"]]"));
}
PathSegment::Length => {
let current = std::mem::take(&mut out);
out = format!("length({current})");
}
}
}
out
}
fn render_c(segments: &[PathSegment], result_var: &str) -> String {
let mut parts = Vec::new();
let mut trailing_length = false;
for seg in segments {
match seg {
PathSegment::Field(f) => parts.push(f.to_snake_case()),
PathSegment::ArrayField { name, .. } => parts.push(name.to_snake_case()),
PathSegment::MapAccess { field, key } => {
parts.push(field.to_snake_case());
parts.push(key.clone());
}
PathSegment::Length => {
trailing_length = true;
}
}
}
let suffix = parts.join("_");
if trailing_length {
format!("result_{suffix}_count({result_var})")
} else {
format!("result_{suffix}({result_var})")
}
}
fn render_dart(segments: &[PathSegment], result_var: &str) -> String {
let mut out = result_var.to_string();
for seg in segments {
match seg {
PathSegment::Field(f) => {
out.push('.');
out.push_str(&f.to_lower_camel_case());
}
PathSegment::ArrayField { name, index } => {
out.push('.');
out.push_str(&name.to_lower_camel_case());
out.push_str(&format!("[{index}]"));
}
PathSegment::MapAccess { field, key } => {
out.push('.');
out.push_str(&field.to_lower_camel_case());
if key.chars().all(|c| c.is_ascii_digit()) {
out.push_str(&format!("[{key}]"));
} else {
out.push_str(&format!("[\"{key}\"]"));
}
}
PathSegment::Length => {
out.push_str(".length");
}
}
}
out
}
fn render_dart_with_optionals(segments: &[PathSegment], result_var: &str, optional_fields: &HashSet<String>) -> String {
let mut out = result_var.to_string();
let mut path_so_far = String::new();
let mut prev_was_nullable = false;
for seg in segments {
let nav = if prev_was_nullable { "?." } else { "." };
match seg {
PathSegment::Field(f) => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(f);
let is_optional = optional_fields.contains(&path_so_far);
out.push_str(nav);
out.push_str(&f.to_lower_camel_case());
prev_was_nullable = is_optional;
}
PathSegment::ArrayField { name, index } => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(name);
let is_optional = optional_fields.contains(&path_so_far);
out.push_str(nav);
out.push_str(&name.to_lower_camel_case());
if is_optional {
out.push('!');
}
out.push_str(&format!("[{index}]"));
prev_was_nullable = false;
}
PathSegment::MapAccess { field, key } => {
if !path_so_far.is_empty() {
path_so_far.push('.');
}
path_so_far.push_str(field);
let is_optional = optional_fields.contains(&path_so_far);
out.push_str(nav);
out.push_str(&field.to_lower_camel_case());
if key.chars().all(|c| c.is_ascii_digit()) {
out.push_str(&format!("[{key}]"));
} else {
out.push_str(&format!("[\"{key}\"]"));
}
prev_was_nullable = is_optional;
}
PathSegment::Length => {
out.push_str(nav);
out.push_str("length");
prev_was_nullable = false;
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn make_resolver() -> FieldResolver {
let mut fields = HashMap::new();
fields.insert("title".to_string(), "metadata.document.title".to_string());
fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
fields.insert("og".to_string(), "metadata.document.open_graph".to_string());
fields.insert("twitter".to_string(), "metadata.document.twitter_card".to_string());
fields.insert("canonical".to_string(), "metadata.document.canonical_url".to_string());
fields.insert("og_tag".to_string(), "metadata.open_graph_tags[og_title]".to_string());
let mut optional = HashSet::new();
optional.insert("metadata.document.title".to_string());
FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
}
fn make_resolver_with_doc_optional() -> FieldResolver {
let mut fields = HashMap::new();
fields.insert("title".to_string(), "metadata.document.title".to_string());
fields.insert("tags".to_string(), "metadata.tags[name]".to_string());
let mut optional = HashSet::new();
optional.insert("document".to_string());
optional.insert("metadata.document.title".to_string());
optional.insert("metadata.document".to_string());
FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &HashSet::new())
}
#[test]
fn test_resolve_alias() {
let r = make_resolver();
assert_eq!(r.resolve("title"), "metadata.document.title");
}
#[test]
fn test_resolve_passthrough() {
let r = make_resolver();
assert_eq!(r.resolve("content"), "content");
}
#[test]
fn test_is_optional() {
let r = make_resolver();
assert!(r.is_optional("metadata.document.title"));
assert!(!r.is_optional("content"));
}
#[test]
fn test_accessor_rust_struct() {
let r = make_resolver();
assert_eq!(r.accessor("title", "rust", "result"), "result.metadata.document.title");
}
#[test]
fn test_accessor_rust_map() {
let r = make_resolver();
assert_eq!(
r.accessor("tags", "rust", "result"),
"result.metadata.tags.get(\"name\").map(|s| s.as_str())"
);
}
#[test]
fn test_accessor_python() {
let r = make_resolver();
assert_eq!(
r.accessor("title", "python", "result"),
"result.metadata.document.title"
);
}
#[test]
fn test_accessor_go() {
let r = make_resolver();
assert_eq!(r.accessor("title", "go", "result"), "result.Metadata.Document.Title");
}
#[test]
fn test_accessor_go_initialism_fields() {
let mut fields = std::collections::HashMap::new();
fields.insert("content".to_string(), "html".to_string());
fields.insert("link_url".to_string(), "links.url".to_string());
let r = FieldResolver::new(
&fields,
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
);
assert_eq!(r.accessor("content", "go", "result"), "result.HTML");
assert_eq!(r.accessor("link_url", "go", "result"), "result.Links.URL");
assert_eq!(r.accessor("html", "go", "result"), "result.HTML");
assert_eq!(r.accessor("url", "go", "result"), "result.URL");
assert_eq!(r.accessor("id", "go", "result"), "result.ID");
assert_eq!(r.accessor("user_id", "go", "result"), "result.UserID");
assert_eq!(r.accessor("request_url", "go", "result"), "result.RequestURL");
assert_eq!(r.accessor("links", "go", "result"), "result.Links");
}
#[test]
fn test_accessor_typescript() {
let r = make_resolver();
assert_eq!(
r.accessor("title", "typescript", "result"),
"result.metadata.document.title"
);
}
#[test]
fn test_accessor_typescript_snake_to_camel() {
let r = make_resolver();
assert_eq!(
r.accessor("og", "typescript", "result"),
"result.metadata.document.openGraph"
);
assert_eq!(
r.accessor("twitter", "typescript", "result"),
"result.metadata.document.twitterCard"
);
assert_eq!(
r.accessor("canonical", "typescript", "result"),
"result.metadata.document.canonicalUrl"
);
}
#[test]
fn test_accessor_typescript_map_snake_to_camel() {
let r = make_resolver();
assert_eq!(
r.accessor("og_tag", "typescript", "result"),
"result.metadata.openGraphTags[\"og_title\"]"
);
}
#[test]
fn test_accessor_typescript_numeric_index_is_unquoted() {
let mut fields = HashMap::new();
fields.insert("first_score".to_string(), "results[0].relevance_score".to_string());
let r = FieldResolver::new(
&fields,
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
);
assert_eq!(
r.accessor("first_score", "typescript", "result"),
"result.results[0].relevanceScore"
);
}
#[test]
fn test_accessor_node_alias() {
let r = make_resolver();
assert_eq!(r.accessor("og", "node", "result"), "result.metadata.document.openGraph");
}
#[test]
fn test_accessor_wasm_camel_case() {
let r = make_resolver();
assert_eq!(r.accessor("og", "wasm", "result"), "result.metadata.document.openGraph");
assert_eq!(
r.accessor("twitter", "wasm", "result"),
"result.metadata.document.twitterCard"
);
assert_eq!(
r.accessor("canonical", "wasm", "result"),
"result.metadata.document.canonicalUrl"
);
}
#[test]
fn test_accessor_wasm_map_access() {
let r = make_resolver();
assert_eq!(
r.accessor("og_tag", "wasm", "result"),
"result.metadata.openGraphTags.get(\"og_title\")"
);
}
#[test]
fn test_accessor_java() {
let r = make_resolver();
assert_eq!(
r.accessor("title", "java", "result"),
"result.metadata().document().title()"
);
}
#[test]
fn test_accessor_kotlin_uses_kotlin_collection_idioms() {
let mut fields = HashMap::new();
fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
fields.insert("node_count".to_string(), "nodes.length".to_string());
let mut arrays = HashSet::new();
arrays.insert("nodes".to_string());
let r = FieldResolver::new(&fields, &HashSet::new(), &HashSet::new(), &arrays, &HashSet::new());
assert_eq!(
r.accessor("first_node_name", "kotlin", "result"),
"result.nodes().first().name()"
);
assert_eq!(r.accessor("node_count", "kotlin", "result"), "result.nodes().size");
}
#[test]
fn test_accessor_kotlin_uses_safe_calls_for_optional_prefixes() {
let r = make_resolver_with_doc_optional();
assert_eq!(
r.accessor("title", "kotlin", "result"),
"result.metadata().document()?.title()"
);
}
#[test]
fn test_accessor_kotlin_uses_safe_calls_for_optional_arrays_and_maps() {
let mut fields = HashMap::new();
fields.insert("first_node_name".to_string(), "nodes[0].name".to_string());
fields.insert("tag".to_string(), "tags[name]".to_string());
let mut optional = HashSet::new();
optional.insert("nodes".to_string());
optional.insert("tags".to_string());
let mut arrays = HashSet::new();
arrays.insert("nodes".to_string());
let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
assert_eq!(
r.accessor("first_node_name", "kotlin", "result"),
"result.nodes()?.first()?.name()"
);
assert_eq!(r.accessor("tag", "kotlin", "result"), "result.tags()?.get(\"name\")");
}
#[test]
fn test_accessor_kotlin_optional_field_after_indexed_array() {
let mut fields = HashMap::new();
fields.insert(
"tool_call_name".to_string(),
"choices[0].message.tool_calls[0].function.name".to_string(),
);
let mut optional = HashSet::new();
optional.insert("choices[0].message.tool_calls".to_string());
let mut arrays = HashSet::new();
arrays.insert("choices".to_string());
arrays.insert("choices[0].message.tool_calls".to_string());
let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &arrays, &HashSet::new());
let expr = r.accessor("tool_call_name", "kotlin", "result");
assert!(
expr.contains("toolCalls()?.first()"),
"expected toolCalls()?.first() for optional list, got: {expr}"
);
}
#[test]
fn test_accessor_csharp() {
let r = make_resolver();
assert_eq!(
r.accessor("title", "csharp", "result"),
"result.Metadata.Document.Title"
);
}
#[test]
fn test_accessor_php() {
let r = make_resolver();
assert_eq!(
r.accessor("title", "php", "$result"),
"$result->metadata->document->title"
);
}
#[test]
fn test_accessor_r() {
let r = make_resolver();
assert_eq!(r.accessor("title", "r", "result"), "result$metadata$document$title");
}
#[test]
fn test_accessor_c() {
let r = make_resolver();
assert_eq!(
r.accessor("title", "c", "result"),
"result_metadata_document_title(result)"
);
}
#[test]
fn test_rust_unwrap_binding() {
let r = make_resolver();
let (binding, var) = r.rust_unwrap_binding("title", "result").unwrap();
assert_eq!(var, "metadata_document_title");
assert!(binding.contains("as_ref().map(|v| v.to_string()).unwrap_or_default()"));
}
#[test]
fn test_rust_unwrap_binding_non_optional() {
let r = make_resolver();
assert!(r.rust_unwrap_binding("content", "result").is_none());
}
#[test]
fn test_rust_unwrap_binding_collapses_double_underscore() {
let mut aliases = HashMap::new();
aliases.insert("json_ld.name".to_string(), "json_ld[].name".to_string());
let mut optional = HashSet::new();
optional.insert("json_ld[].name".to_string());
let mut array = HashSet::new();
array.insert("json_ld".to_string());
let result_fields = HashSet::new();
let method_calls = HashSet::new();
let r = FieldResolver::new(&aliases, &optional, &result_fields, &array, &method_calls);
let (_binding, var) = r.rust_unwrap_binding("json_ld.name", "result").unwrap();
assert_eq!(var, "json_ld_name");
}
#[test]
fn test_direct_field_no_alias() {
let r = make_resolver();
assert_eq!(r.accessor("content", "rust", "result"), "result.content");
assert_eq!(r.accessor("content", "go", "result"), "result.Content");
}
#[test]
fn test_accessor_rust_with_optionals() {
let r = make_resolver_with_doc_optional();
assert_eq!(
r.accessor("title", "rust", "result"),
"result.metadata.document.as_ref().unwrap().title"
);
}
#[test]
fn test_accessor_csharp_with_optionals() {
let r = make_resolver_with_doc_optional();
assert_eq!(
r.accessor("title", "csharp", "result"),
"result.Metadata.Document!.Title"
);
}
#[test]
fn test_accessor_rust_non_optional_field() {
let r = make_resolver();
assert_eq!(r.accessor("content", "rust", "result"), "result.content");
}
#[test]
fn test_accessor_csharp_non_optional_field() {
let r = make_resolver();
assert_eq!(r.accessor("content", "csharp", "result"), "result.Content");
}
#[test]
fn test_accessor_rust_method_call() {
let mut fields = HashMap::new();
fields.insert(
"excel_sheet_count".to_string(),
"metadata.format.excel.sheet_count".to_string(),
);
let mut optional = HashSet::new();
optional.insert("metadata.format".to_string());
optional.insert("metadata.format.excel".to_string());
let mut method_calls = HashSet::new();
method_calls.insert("metadata.format.excel".to_string());
let r = FieldResolver::new(&fields, &optional, &HashSet::new(), &HashSet::new(), &method_calls);
assert_eq!(
r.accessor("excel_sheet_count", "rust", "result"),
"result.metadata.format.as_ref().unwrap().excel().as_ref().unwrap().sheet_count"
);
}
fn make_php_getter_resolver() -> FieldResolver {
let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
getters.insert(
"Root".to_string(),
["metadata".to_string(), "links".to_string()].into_iter().collect(),
);
let map = PhpGetterMap {
getters,
field_types: HashMap::new(),
root_type: Some("Root".to_string()),
all_fields: HashMap::new(),
};
FieldResolver::new_with_php_getters(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashMap::new(),
map,
)
}
#[test]
fn render_php_uses_getter_method_for_non_scalar_field() {
let r = make_php_getter_resolver();
assert_eq!(r.accessor("metadata", "php", "$result"), "$result->getMetadata()");
}
#[test]
fn render_php_uses_property_for_scalar_field() {
let r = make_php_getter_resolver();
assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
}
#[test]
fn render_php_nested_non_scalar_uses_getter_then_property() {
let mut fields = HashMap::new();
fields.insert("title".to_string(), "metadata.title".to_string());
let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
getters.insert("Root".to_string(), ["metadata".to_string()].into_iter().collect());
getters.insert("Metadata".to_string(), HashSet::new());
let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
field_types.insert(
"Root".to_string(),
[("metadata".to_string(), "Metadata".to_string())].into_iter().collect(),
);
let map = PhpGetterMap {
getters,
field_types,
root_type: Some("Root".to_string()),
all_fields: HashMap::new(),
};
let r = FieldResolver::new_with_php_getters(
&fields,
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashMap::new(),
map,
);
assert_eq!(r.accessor("title", "php", "$result"), "$result->getMetadata()->title");
}
#[test]
fn render_php_array_field_uses_getter_when_non_scalar() {
let mut fields = HashMap::new();
fields.insert("first_link".to_string(), "links[0]".to_string());
let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
getters.insert("Root".to_string(), ["links".to_string()].into_iter().collect());
let map = PhpGetterMap {
getters,
field_types: HashMap::new(),
root_type: Some("Root".to_string()),
all_fields: HashMap::new(),
};
let r = FieldResolver::new_with_php_getters(
&fields,
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashMap::new(),
map,
);
assert_eq!(r.accessor("first_link", "php", "$result"), "$result->getLinks()[0]");
}
#[test]
fn render_php_falls_back_to_property_when_getter_fields_empty() {
let r = FieldResolver::new(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
);
assert_eq!(r.accessor("status_code", "php", "$result"), "$result->statusCode");
assert_eq!(r.accessor("metadata", "php", "$result"), "$result->metadata");
}
#[test]
fn render_php_with_getters_distinguishes_same_field_name_on_different_types() {
let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
getters.insert("A".to_string(), ["content".to_string()].into_iter().collect());
getters.insert("B".to_string(), HashSet::new());
let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
all_fields.insert("A".to_string(), ["content".to_string()].into_iter().collect());
all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
let map_a = PhpGetterMap {
getters: getters.clone(),
field_types: HashMap::new(),
root_type: Some("A".to_string()),
all_fields: all_fields.clone(),
};
let map_b = PhpGetterMap {
getters,
field_types: HashMap::new(),
root_type: Some("B".to_string()),
all_fields,
};
let r_a = FieldResolver::new_with_php_getters(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashMap::new(),
map_a,
);
let r_b = FieldResolver::new_with_php_getters(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashMap::new(),
map_b,
);
assert_eq!(r_a.accessor("content", "php", "$a"), "$a->getContent()");
assert_eq!(r_b.accessor("content", "php", "$b"), "$b->content");
}
#[test]
fn render_php_with_getters_chains_through_correct_type() {
let mut fields = HashMap::new();
fields.insert("nested_content".to_string(), "inner.content".to_string());
let mut getters: HashMap<String, HashSet<String>> = HashMap::new();
getters.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
getters.insert("B".to_string(), HashSet::new());
getters.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
field_types.insert(
"Outer".to_string(),
[("inner".to_string(), "B".to_string())].into_iter().collect(),
);
let mut all_fields: HashMap<String, HashSet<String>> = HashMap::new();
all_fields.insert("Outer".to_string(), ["inner".to_string()].into_iter().collect());
all_fields.insert("B".to_string(), ["content".to_string()].into_iter().collect());
all_fields.insert("Decoy".to_string(), ["content".to_string()].into_iter().collect());
let map = PhpGetterMap {
getters,
field_types,
root_type: Some("Outer".to_string()),
all_fields,
};
let r = FieldResolver::new_with_php_getters(
&fields,
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashMap::new(),
map,
);
assert_eq!(
r.accessor("nested_content", "php", "$result"),
"$result->getInner()->content"
);
}
}