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, Clone, PartialEq, Eq)]
pub struct QualifiedFieldCollision {
pub qualified: String,
pub first_interface: String,
pub second_interface: String,
}
#[derive(Debug, Default)]
pub struct FieldIndex {
qualified: HashMap<String, FieldLocation>,
by_name: HashMap<String, Vec<FieldLocation>>,
input_ids: HashSet<String>,
output_ids: HashSet<String>,
qualified_collisions: Vec<QualifiedFieldCollision>,
}
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 qualified = format!("{interface_id}.{}", field.name);
if let Some(existing) = self.qualified.get(&qualified) {
self.qualified_collisions.push(QualifiedFieldCollision {
qualified: qualified.clone(),
first_interface: existing.interface_id.clone(),
second_interface: interface_id.to_string(),
});
continue;
}
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,
};
self.qualified.insert(qualified, location.clone());
self.by_name
.entry(field.name.clone())
.or_default()
.push(location);
}
}
#[must_use]
pub fn qualified_collisions(&self) -> &[QualifiedFieldCollision] {
&self.qualified_collisions
}
#[must_use]
pub fn interface_ids(&self) -> Vec<&str> {
self.input_ids
.iter()
.chain(self.output_ids.iter())
.map(String::as_str)
.collect()
}
#[must_use]
pub fn has_io_id_collision(&self) -> bool {
self.input_ids
.intersection(&self.output_ids)
.next()
.is_some()
}
#[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(qualified) =
resolve_qualified_path(target, self.interface_ids().iter().copied())
{
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()
}
}
#[must_use]
pub fn resolve_qualified_path<'a>(
target: &str,
interface_ids: impl IntoIterator<Item = &'a str>,
) -> Option<String> {
let target = target.trim();
if !target.contains('.') {
return None;
}
let mut best: Option<(&str, usize)> = None;
for id in interface_ids {
let prefix = format!("{id}.");
if target.starts_with(&prefix) {
let len = id.len();
if best.map_or(true, |(_, bl)| len > bl) {
best = Some((id, len));
}
}
}
best.map(|_| target.to_string())
}