use toasty::schema::app;
use toasty_core::stmt::{Path, PathRoot};
use super::{AppSchema, ModelId, app_embedded, fields, lookup::app_field_at};
use crate::toasty_compat::UntypedInclude;
pub(crate) fn included_models<M: toasty::schema::Model>(
includes: &[UntypedInclude],
) -> Option<Vec<(ModelId, String)>> {
let schema = AppSchema::of_model::<M>().ok()?;
let schema = schema.inner();
let mut models = Vec::new();
for include in includes {
let (fields, steps) = start(schema, &include.path)?;
walk(schema, fields, steps, &mut models)?;
}
Some(models)
}
fn start<'a>(
schema: &'a toasty_core::Schema,
path: &'a Path,
) -> Option<(&'a [app::Field], &'a [usize])> {
match &path.root {
PathRoot::Model(model) => {
let root = schema.app.get_model(*model)?.as_root()?;
Some((&root.fields, path.projection.as_slice()))
}
PathRoot::Variant { parent, variant_id } => {
let (fields, steps) = start(schema, parent)?;
let field = app_field_at(schema, fields, steps)?;
let app::Model::EmbeddedEnum(enum_) = app_embedded(schema, field)? else {
return None;
};
enum_.variants.get(variant_id.index)?;
Some((
enum_.variant_fields(variant_id.index),
path.projection.as_slice(),
))
}
}
}
fn walk(
schema: &toasty_core::Schema,
fields: &[app::Field],
steps: &[usize],
models: &mut Vec<(ModelId, String)>,
) -> Option<()> {
let Some((first, rest)) = steps.split_first() else {
return Some(());
};
let field = fields.get(*first)?;
match &field.ty {
app::FieldTy::BelongsTo(relation) => reach(schema, relation.target, rest, models),
app::FieldTy::Has(relation) => reach(schema, relation.target, rest, models),
app::FieldTy::Via(relation) => reach(schema, relation.target, rest, models),
app::FieldTy::Primitive(_) => rest.is_empty().then_some(()),
app::FieldTy::Embedded(_) => match app_embedded(schema, field)? {
app::Model::EmbeddedStruct(embedded) => walk(schema, &embedded.fields, rest, models),
app::Model::EmbeddedEnum(enum_) => {
let Some((variant, tail)) = rest.split_first() else {
return Some(());
};
enum_.variants.get(*variant)?;
walk(schema, enum_.variant_fields(*variant), tail, models)
}
app::Model::Root(_) => None,
},
}
}
fn reach(
schema: &toasty_core::Schema,
target: ModelId,
steps: &[usize],
models: &mut Vec<(ModelId, String)>,
) -> Option<()> {
let model = schema.app.get_model(target)?;
models.push((target, model.name().upper_camel_case()));
match model.as_root() {
Some(root) => walk(schema, &root.fields, steps, models),
None => steps.is_empty().then_some(()),
}
}
pub(crate) fn read_relations<M: toasty::schema::Model>(
names: &[String],
) -> Option<Vec<(ModelId, String)>> {
let schema = AppSchema::of_model::<M>().ok()?;
let schema = schema.inner();
let root = schema.app.get_model(M::id())?.as_root()?;
let mut models = Vec::new();
for name in names {
let info = fields::<M>().into_iter().find(|info| &info.name == name)?;
reach_field(schema, root.fields.get(info.index)?, &mut models)?;
}
Some(models)
}
fn reach_field(
schema: &toasty_core::Schema,
field: &app::Field,
models: &mut Vec<(ModelId, String)>,
) -> Option<()> {
match &field.ty {
app::FieldTy::BelongsTo(relation) => reach(schema, relation.target, &[], models),
app::FieldTy::Has(relation) => reach(schema, relation.target, &[], models),
app::FieldTy::Via(relation) => reach(schema, relation.target, &[], models),
app::FieldTy::Primitive(_) => Some(()),
app::FieldTy::Embedded(_) => match app_embedded(schema, field)? {
app::Model::EmbeddedStruct(embedded) => embedded
.fields
.iter()
.try_for_each(|field| reach_field(schema, field, models)),
app::Model::EmbeddedEnum(enum_) => enum_
.fields
.iter()
.try_for_each(|field| reach_field(schema, field, models)),
app::Model::Root(_) => None,
},
}
}