use std::sync::Arc;
use toasty::{
schema::{app, mapping},
stmt::{IntoExpr, IntoInsert, Path},
};
use toasty_core::stmt::PathRoot;
use topcoat::context::Cx;
use crate::{DeclarationErrorKind, naming::capitalize};
pub(crate) type ModelId = app::ModelId;
#[derive(Clone)]
pub(crate) struct AppSchema(Arc<toasty_core::Schema>);
impl AppSchema {
pub(crate) fn of_db(db: &toasty::Db) -> Self {
Self(db.schema().clone())
}
pub(crate) fn of(cx: &Cx) -> Option<Self> {
topcoat::context::try_app_context::<toasty::Db>(cx).map(Self::of_db)
}
pub(crate) fn registers(&self, name: &str) -> bool {
self.0
.app
.models()
.any(|model| model.name().upper_camel_case() == name)
}
}
#[derive(Debug, Clone)]
pub(crate) struct ModelPath(toasty_core::stmt::Path);
impl ModelPath {
pub(crate) fn of<M, T>(path: &Path<M, T>) -> Self {
Self(path.clone().into())
}
pub(crate) fn steps(&self) -> &[usize] {
self.0.projection.as_slice()
}
pub(crate) fn is_embedded(&self) -> bool {
self.steps().len() > 1 || matches!(self.0.root, PathRoot::Variant { .. })
}
pub(crate) fn eq<T: IntoExpr<T>>(&self, value: T) -> toasty::stmt::Expr<bool> {
let rhs: toasty_core::stmt::Expr = value.into_expr().into();
toasty::stmt::Expr::from_untyped(toasty_core::stmt::Expr::eq(
self.0.clone().into_stmt(),
rhs,
))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct FieldInfo {
pub(crate) index: usize,
pub(crate) name: String,
pub(crate) label: String,
pub(crate) nullable: bool,
pub(crate) unique: bool,
pub(crate) relation: bool,
}
pub(crate) fn fields<M: toasty::schema::Model>() -> Vec<FieldInfo> {
let model = M::schema();
let root = model.as_root_unwrap();
root.fields
.iter()
.filter_map(|field| field_info(field, root))
.collect()
}
pub(crate) fn field<M: toasty::schema::Model>(
path: &ModelPath,
) -> Result<FieldInfo, DeclarationErrorKind> {
let model = M::schema();
let root = model.as_root_unwrap();
let index = single_segment(path)?;
let field = root.fields.get(index).unwrap_or_else(|| {
panic!(
"field index {index} out of bounds for {}",
std::any::type_name::<M>()
)
});
Ok(field_info(field, root).expect("a model root's fields are named"))
}
fn field_info(field: &app::Field, root: &app::ModelRoot) -> Option<FieldInfo> {
let name = field.name.app.as_deref()?;
Some(FieldInfo {
index: field.id.index,
name: name.to_string(),
label: capitalize(name),
nullable: field.nullable(),
unique: root.indices.iter().any(|index| {
index.unique && !index.primary_key && index.fields.iter().any(|f| f.field == field.id)
}),
relation: field.is_relation(),
})
}
pub(crate) fn single_segment(path: &ModelPath) -> Result<usize, DeclarationErrorKind> {
match path.steps() {
[index] => Ok(*index),
steps => Err(DeclarationErrorKind::TraversalLens { steps: steps.len() }),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct BelongsTo {
pub(crate) name: String,
pub(crate) target: ModelId,
pub(crate) foreign_keys: Vec<String>,
}
pub(crate) fn belongs_to<M: toasty::schema::Model>(index: usize) -> Option<BelongsTo> {
let model = M::schema();
let root = model.as_root()?;
let field = root.fields.get(index)?;
let app::FieldTy::BelongsTo(relation) = &field.ty else {
return None;
};
let foreign_keys = relation
.foreign_key
.fields
.iter()
.filter_map(|key| root.fields.get(key.source.index))
.map(|field| field.name.app_unwrap().to_string())
.collect::<Vec<_>>();
(!foreign_keys.is_empty()).then(|| BelongsTo {
name: field.name.app_unwrap().to_string(),
target: relation.target,
foreign_keys,
})
}
pub(crate) fn prefilled_fields<M: toasty::schema::Model>() -> Vec<bool> {
let insert = <M::Create as Default>::default().into_insert();
let toasty_core::stmt::Expr::Stmt(statement) = toasty_core::stmt::Expr::from(insert) else {
return Vec::new();
};
statement
.stmt
.as_insert()
.and_then(|insert| insert.source.body.as_values())
.and_then(|values| values.rows.last())
.and_then(|row| row.as_record())
.map(|record| {
record
.fields
.iter()
.map(|expr| !expr.is_value_null())
.collect()
})
.unwrap_or_default()
}
#[derive(Debug, Clone)]
pub(crate) struct LeafField {
pub(crate) name: String,
pub(crate) label: String,
pub(crate) nullable: bool,
pub(crate) unique: bool,
}
impl From<FieldInfo> for LeafField {
fn from(field: FieldInfo) -> Self {
Self {
name: field.name,
label: field.label,
nullable: field.nullable,
unique: field.unique,
}
}
}
pub(crate) fn single_field<M: toasty::schema::Model>(
path: &ModelPath,
) -> Result<LeafField, DeclarationErrorKind> {
field::<M>(path).map(LeafField::from)
}
pub(crate) fn embedded_leaf(
schema: Option<&AppSchema>,
path: &ModelPath,
model: &'static str,
) -> Result<LeafField, DeclarationErrorKind> {
let steps = path.steps();
let Some(AppSchema(schema)) = schema else {
return Err(DeclarationErrorKind::TraversalLens { steps: steps.len() });
};
walk_embedded(schema, &path.0).ok_or_else(|| DeclarationErrorKind::UnresolvedLens {
model,
steps: steps.to_vec(),
})
}
fn walk_embedded(
schema: &toasty_core::Schema,
path: &toasty_core::stmt::Path,
) -> Option<LeafField> {
match &path.root {
PathRoot::Model(id) => {
let root = schema.app.get_model(*id)?.as_root()?;
let model = schema.mapping.models.get(&root.id)?;
descend(
schema,
&root.fields,
&model.fields,
path.projection.as_slice(),
)
}
PathRoot::Variant { parent, variant_id } => {
let root = schema
.app
.get_model(parent.root.as_model_unwrap())?
.as_root()?;
let model = schema.mapping.models.get(&root.id)?;
let parent_steps = parent.projection.as_slice();
let app_field = app_field_at(schema, &root.fields, parent_steps)?;
let Some(app::Model::EmbeddedEnum(e)) = app_embedded(schema, app_field) else {
return None;
};
let MappingField::Enum(me) = mapping_field_at(&model.fields, parent_steps)? else {
return None;
};
let variant = me.variants.get(variant_id.index)?;
let payloads: Vec<_> = e.variant_fields(variant_id.index).iter().collect();
descend(
schema,
&payloads,
&variant.fields,
path.projection.as_slice(),
)
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct EnumShape {
pub(crate) discriminant: String,
pub(crate) variants: Vec<(String, String)>,
}
pub(crate) fn embedded_enum(schema: &AppSchema, path: &ModelPath) -> Option<EnumShape> {
let AppSchema(schema) = schema;
let PathRoot::Model(id) = path.0.root else {
return None;
};
let root = schema.app.get_model(id)?.as_root()?;
let mapping = schema.mapping.models.get(&root.id)?;
let steps = path.steps();
let app_field = app_field_at(schema, &root.fields, steps)?;
let (app::Model::EmbeddedEnum(e), MappingField::Enum(me)) = (
app_embedded(schema, app_field)?,
mapping_field_at(&mapping.fields, steps)?,
) else {
return None;
};
let discriminant = column_of(schema, &MappingField::Primitive(me.discriminant.clone()))?.name;
let variants = e
.variants
.iter()
.map(|v| {
super::value_text(&v.discriminant)
.map(|value| (value, capitalize(&v.name.snake_case())))
})
.collect::<Option<Vec<_>>>()?;
Some(EnumShape {
discriminant,
variants,
})
}
type MappingField = mapping::Field;
fn descend<F>(
schema: &toasty_core::Schema,
app_fields: &[F],
mapping_fields: &[MappingField],
steps: &[usize],
) -> Option<LeafField>
where
F: std::borrow::Borrow<app::Field>,
{
let (first, rest) = steps.split_first()?;
let app_field: &app::Field = app_fields.get(*first)?.borrow();
let mapping_field = mapping_fields.get(*first)?;
if rest.is_empty() || is_document(app_field) {
return column_of(schema, mapping_field);
}
let app::FieldTy::Embedded(embedded) = &app_field.ty else {
return None;
};
match schema.app.get_model(embedded.target)? {
app::Model::EmbeddedStruct(e) => {
let MappingField::Struct(ms) = mapping_field else {
return None;
};
descend(schema, &e.fields, &ms.fields, rest)
}
app::Model::EmbeddedEnum(e) => {
let MappingField::Enum(me) = mapping_field else {
return None;
};
let (variant_index, tail) = rest.split_first()?;
let variant = me.variants.get(*variant_index)?;
let payloads: Vec<_> = e.variant_fields(*variant_index).iter().collect();
descend(schema, &payloads, &variant.fields, tail)
}
_ => None,
}
}
fn column_of(schema: &toasty_core::Schema, field: &MappingField) -> Option<LeafField> {
let MappingField::Primitive(p) = field else {
return None;
};
let column = &schema.db.tables[p.column.table.0].columns[p.column.index];
Some(LeafField {
name: column.name.clone(),
label: capitalize(&column.name.replace('_', " ")),
nullable: true,
unique: false,
})
}
fn is_document(field: &app::Field) -> bool {
matches!(
&field.ty,
app::FieldTy::Primitive(p) if matches!(p.ty, toasty::stmt::Type::Model(_))
)
}
fn app_embedded<'a>(schema: &'a toasty_core::Schema, field: &app::Field) -> Option<&'a app::Model> {
let app::FieldTy::Embedded(embedded) = &field.ty else {
return None;
};
schema.app.get_model(embedded.target)
}
fn app_field_at<'a>(
schema: &'a toasty_core::Schema,
fields: &'a [app::Field],
steps: &[usize],
) -> Option<&'a app::Field> {
let (first, rest) = steps.split_first()?;
let field = fields.get(*first)?;
if rest.is_empty() {
return Some(field);
}
match app_embedded(schema, field)? {
app::Model::EmbeddedStruct(e) => app_field_at(schema, &e.fields, rest),
app::Model::EmbeddedEnum(e) => {
let (variant, tail) = rest.split_first()?;
e.variants.get(*variant)?;
let field = e.variant_fields(*variant).get(*tail.first()?)?;
if tail.len() == 1 {
Some(field)
} else {
app_field_at(schema, std::slice::from_ref(field), &tail[1..])
}
}
app::Model::Root(_) => None,
}
}
fn mapping_field_at<'a>(fields: &'a [MappingField], steps: &[usize]) -> Option<&'a MappingField> {
let (first, rest) = steps.split_first()?;
let field = fields.get(*first)?;
if rest.is_empty() {
return Some(field);
}
match field {
MappingField::Struct(s) => mapping_field_at(&s.fields, rest),
MappingField::Enum(e) => {
let (variant, tail) = rest.split_first()?;
mapping_field_at(&e.variants.get(*variant)?.fields, tail)
}
_ => None,
}
}
#[cfg(test)]
mod tests;