use std::{cell::RefCell, sync::Arc};
use toasty::stmt::Path;
use toasty_core::stmt::PathRoot;
use topcoat::context::Cx;
use crate::{DeclarationErrorKind, naming::capitalize};
thread_local! {
static SCHEMA: RefCell<Option<Arc<toasty_core::Schema>>> = const { RefCell::new(None) };
}
pub fn declare<T>(db: &toasty::Db, declarations: impl FnOnce() -> T) -> T {
declare_with(Some(db.schema().clone()), declarations)
}
pub(crate) fn declare_with<T>(
schema: Option<Arc<toasty_core::Schema>>,
declarations: impl FnOnce() -> T,
) -> T {
struct Restore(Option<Arc<toasty_core::Schema>>);
impl Drop for Restore {
fn drop(&mut self) {
SCHEMA.with(|scope| *scope.borrow_mut() = self.0.take());
}
}
let _restore = Restore(SCHEMA.with(|scope| scope.replace(schema)));
declarations()
}
pub(crate) fn schema_of(cx: &Cx) -> Option<Arc<toasty_core::Schema>> {
topcoat::context::try_app_context::<toasty::Db>(cx).map(|db| db.schema().clone())
}
pub(crate) struct ResolvedLens<M, T> {
pub(crate) path: Path<M, T>,
pub(crate) name: String,
pub(crate) label: String,
pub(crate) nullable: bool,
pub(crate) unique: bool,
pub(crate) misdeclared: Option<DeclarationErrorKind>,
}
impl<M, T> ResolvedLens<M, T>
where
M: toasty::schema::Model,
{
pub(crate) fn of(path: impl Into<Path<M, T>>) -> Self {
let path = path.into();
match FieldResolver::current().resolve(path.clone()) {
Ok(leaf) => Self {
path,
name: leaf.name,
label: leaf.label,
nullable: leaf.nullable,
unique: leaf.unique,
misdeclared: None,
},
Err(error) => {
let core_path: toasty_core::stmt::Path = path.clone().into();
Self {
path,
name: format!("{:?}", core_path.projection.as_slice()),
label: String::new(),
nullable: true,
unique: false,
misdeclared: Some(error),
}
}
}
}
}
#[doc(hidden)]
pub fn form_key<M, T>(path: Path<M, T>) -> String
where
M: toasty::schema::Model,
{
ResolvedLens::of(path).name
}
#[derive(Debug, Clone)]
pub(crate) struct LeafField {
pub(crate) name: String,
pub(crate) label: String,
pub(crate) nullable: bool,
pub(crate) unique: bool,
}
pub(crate) struct FieldResolver {
schema: Option<Arc<toasty_core::Schema>>,
}
impl FieldResolver {
pub(crate) fn new(schema: Option<Arc<toasty_core::Schema>>) -> Self {
Self { schema }
}
pub(crate) fn current() -> Self {
Self::new(SCHEMA.with(|scope| scope.borrow().clone()))
}
pub(crate) fn has_schema(&self) -> bool {
self.schema.is_some()
}
pub(crate) fn resolve<M, T>(&self, path: Path<M, T>) -> Result<LeafField, DeclarationErrorKind>
where
M: toasty::schema::Model,
{
let model = M::schema();
let core_path: toasty_core::stmt::Path = path.into();
let segments = core_path.projection.as_slice().len();
let is_embedded_path = segments > 1 || matches!(core_path.root, PathRoot::Variant { .. });
if is_embedded_path && let Some(schema) = self.schema.as_deref() {
return Self::walk_embedded(schema, &core_path).ok_or_else(|| {
DeclarationErrorKind::UnresolvedLens {
model: std::any::type_name::<M>(),
steps: core_path.projection.as_slice().to_vec(),
}
});
}
let idx = single_segment(&core_path)?;
let field = model
.as_root_unwrap()
.fields
.get(idx)
.cloned()
.unwrap_or_else(|| {
panic!(
"field index {idx} out of bounds for {}",
std::any::type_name::<M>()
)
});
Ok(LeafField {
name: field.name.app_unwrap().to_string(),
label: lens_label(&field),
nullable: field.nullable(),
unique: lens_field_unique(&field, model.as_root_unwrap()),
})
}
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(toasty::schema::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(),
)
}
}
}
pub(crate) fn resolve_enum<M, T>(&self, path: Path<M, T>) -> Option<EnumShape>
where
M: toasty::schema::Model,
{
let schema = self.schema.as_deref()?;
let core_path: toasty_core::stmt::Path = path.into();
let PathRoot::Model(id) = core_path.root else {
return None;
};
let root = schema.app.get_model(id)?.as_root()?;
let mapping = schema.mapping.models.get(&root.id)?;
let steps = core_path.projection.as_slice();
let app_field = app_field_at(schema, &root.fields, steps)?;
let (toasty::schema::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| {
crate::toasty_compat::value_text(&v.discriminant)
.map(|value| (value, capitalize(&v.name.snake_case())))
})
.collect::<Option<Vec<_>>>()?;
Some(EnumShape {
discriminant,
variants,
})
}
}
#[derive(Debug, Clone)]
pub(crate) struct EnumShape {
pub(crate) discriminant: String,
pub(crate) variants: Vec<(String, String)>,
}
type MappingField = toasty_core::schema::mapping::Field;
fn descend<F>(
schema: &toasty_core::Schema,
app_fields: &[F],
mapping_fields: &[MappingField],
steps: &[usize],
) -> Option<LeafField>
where
F: std::borrow::Borrow<toasty::schema::app::Field>,
{
let (first, rest) = steps.split_first()?;
let app_field: &toasty::schema::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 toasty::schema::app::FieldTy::Embedded(embedded) = &app_field.ty else {
return None;
};
match schema.app.get_model(embedded.target)? {
toasty::schema::app::Model::EmbeddedStruct(e) => {
let MappingField::Struct(ms) = mapping_field else {
return None;
};
descend(schema, &e.fields, &ms.fields, rest)
}
toasty::schema::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: &toasty::schema::app::Field) -> bool {
matches!(
&field.ty,
toasty::schema::app::FieldTy::Primitive(p)
if matches!(p.ty, toasty_core::stmt::Type::Model(_))
)
}
fn app_embedded<'a>(
schema: &'a toasty_core::Schema,
field: &toasty::schema::app::Field,
) -> Option<&'a toasty::schema::app::Model> {
let toasty::schema::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 [toasty::schema::app::Field],
steps: &[usize],
) -> Option<&'a toasty::schema::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)? {
toasty::schema::app::Model::EmbeddedStruct(e) => app_field_at(schema, &e.fields, rest),
toasty::schema::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..])
}
}
toasty::schema::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,
}
}
pub(crate) fn lens_field<M, T>(
path: Path<M, T>,
model: &toasty::schema::app::Model,
) -> Result<toasty::schema::app::Field, DeclarationErrorKind>
where
M: toasty::schema::Model,
{
let core_path: toasty_core::stmt::Path = path.into();
let idx = single_segment(&core_path)?;
Ok(model
.as_root_unwrap()
.fields
.get(idx)
.cloned()
.unwrap_or_else(|| {
panic!(
"field index {idx} out of bounds for {}",
std::any::type_name::<M>()
)
}))
}
pub(crate) fn lens_label(field: &toasty::schema::app::Field) -> String {
capitalize(field.name.app_unwrap())
}
pub(crate) fn lens_field_unique(
field: &toasty::schema::app::Field,
model: &toasty::schema::app::ModelRoot,
) -> bool {
model.indices.iter().any(|index| {
index.unique && !index.primary_key && index.fields.iter().any(|f| f.field == field.id)
})
}
pub(crate) fn single_segment(
path: &toasty_core::stmt::Path,
) -> Result<usize, DeclarationErrorKind> {
match path.projection.as_slice() {
[index] => Ok(*index),
steps => Err(DeclarationErrorKind::TraversalLens { steps: steps.len() }),
}
}
#[cfg(test)]
mod tests;