use std::collections::{HashMap, HashSet};
use crate::model::{Field, TransformationContract};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldLocation {
pub interface_id: String,
pub field_name: String,
pub type_name: String,
pub nullable: bool,
pub is_input: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TargetResolution<'a> {
Field(&'a FieldLocation),
Interface {
id: String,
is_input: bool,
},
Ambiguous(Vec<FieldLocation>),
NotFound,
}
#[derive(Debug, Default)]
pub struct FieldIndex {
qualified: HashMap<String, FieldLocation>,
by_name: HashMap<String, Vec<FieldLocation>>,
input_ids: HashSet<String>,
output_ids: HashSet<String>,
}
impl FieldIndex {
#[must_use]
pub fn from_contract(contract: &TransformationContract) -> Self {
let mut index = Self::default();
for input in &contract.inputs {
index.input_ids.insert(input.id.clone());
if let Some(schema) = &input.schema {
index.insert_fields(&input.id, schema.fields.iter(), true);
}
}
for output in &contract.outputs {
index.output_ids.insert(output.id.clone());
if let Some(schema) = &output.schema {
index.insert_fields(&output.id, schema.fields.iter(), false);
}
}
index
}
fn insert_fields<'a>(
&mut self,
interface_id: &str,
fields: impl Iterator<Item = &'a Field>,
is_input: bool,
) {
for field in fields {
let location = FieldLocation {
interface_id: interface_id.to_string(),
field_name: field.name.clone(),
type_name: field.type_name.clone(),
nullable: field.nullable,
is_input,
};
let qualified = format!("{interface_id}.{}", field.name);
self.qualified.insert(qualified, location.clone());
self.by_name
.entry(field.name.clone())
.or_default()
.push(location);
}
}
#[allow(dead_code)]
#[must_use]
pub fn input_ids(&self) -> &HashSet<String> {
&self.input_ids
}
#[allow(dead_code)]
#[must_use]
pub fn output_ids(&self) -> &HashSet<String> {
&self.output_ids
}
#[allow(dead_code)]
#[must_use]
pub fn interface_ids(&self) -> HashSet<String> {
self.input_ids
.iter()
.chain(self.output_ids.iter())
.cloned()
.collect()
}
#[must_use]
pub fn resolve<'a>(&'a self, target: &str) -> TargetResolution<'a> {
let target = target.trim();
if target.is_empty() {
return TargetResolution::NotFound;
}
if let Some(location) = self.qualified.get(target) {
return TargetResolution::Field(location);
}
if let Some((interface_id, field_name)) = target.split_once('.') {
let qualified = format!("{interface_id}.{field_name}");
if let Some(location) = self.qualified.get(&qualified) {
return TargetResolution::Field(location);
}
return TargetResolution::NotFound;
}
if let Some(matches) = self.by_name.get(target) {
return match matches.len() {
0 => TargetResolution::NotFound,
1 => TargetResolution::Field(&matches[0]),
_ => TargetResolution::Ambiguous(matches.clone()),
};
}
if self.input_ids.contains(target) {
return TargetResolution::Interface {
id: target.to_string(),
is_input: true,
};
}
if self.output_ids.contains(target) {
return TargetResolution::Interface {
id: target.to_string(),
is_input: false,
};
}
TargetResolution::NotFound
}
#[must_use]
pub fn ambiguous_field_names(&self) -> Vec<String> {
self.by_name
.iter()
.filter(|(_, locations)| locations.len() > 1)
.map(|(name, _)| name.clone())
.collect()
}
}