use std::collections::BTreeSet;
use cratestack_core::{
Schema, TypeRef, computed_params_type_name, is_computed_field as is_computed,
};
use crate::diagnostics::{SchemaError, span_error};
use crate::validate::computed_params::{ComputedParamsNameSets, validate_computed_params_type};
pub(super) fn computed_bearing_names(schema: &Schema) -> BTreeSet<String> {
let mut bearing: BTreeSet<String> = schema
.models
.iter()
.filter(|model| model.fields.iter().any(is_computed))
.map(|model| model.name.clone())
.chain(
schema
.types
.iter()
.filter(|ty| ty.fields.iter().any(is_computed))
.map(|ty| ty.name.clone()),
)
.collect();
loop {
let mut grew = false;
for ty in &schema.types {
if bearing.contains(&ty.name) {
continue;
}
if ty
.fields
.iter()
.any(|field| bearing.contains(&field.ty.name))
{
bearing.insert(ty.name.clone());
grew = true;
}
}
if !grew {
break;
}
}
bearing
}
fn first_bearing_reference<'a>(ty: &'a TypeRef, bearing: &BTreeSet<String>) -> Option<&'a str> {
if bearing.contains(&ty.name) {
return Some(&ty.name);
}
ty.generic_args
.iter()
.find_map(|arg| first_bearing_reference(arg, bearing))
}
pub(super) fn validate_computed(schema: &Schema) -> Result<(), SchemaError> {
let model_names: BTreeSet<&str> = schema
.models
.iter()
.map(|model| model.name.as_str())
.collect();
let name_sets = ComputedParamsNameSets::collect(schema, &model_names);
let bearing = computed_bearing_names(schema);
let owners = schema
.models
.iter()
.map(|model| ("model", &model.name, &model.fields))
.chain(schema.types.iter().map(|ty| ("type", &ty.name, &ty.fields)));
for (owner_kind, owner_name, fields) in owners {
for field in fields.iter().filter(|field| is_computed(field)) {
if model_names.contains(field.ty.name.as_str()) {
return Err(span_error(
format!(
"field `{}` on {} `{}` is `@computed` but its type `{}` is a model — \
a computed field's resolver must return a plain value (scalar, enum, \
or non-computed `type`), not a database row",
field.name, owner_kind, owner_name, field.ty.name,
),
field.span,
));
}
if bearing.contains(&field.ty.name) {
return Err(span_error(
format!(
"field `{}` on {} `{}` is `@computed` but its type `{}` itself \
contains `@computed` fields — resolver return values are serialized \
as-is, so nested computed fields inside them would never be resolved",
field.name, owner_kind, owner_name, field.ty.name,
),
field.span,
));
}
if let Some(params_type) = computed_params_type_name(field) {
validate_computed_params_type(
owner_kind,
owner_name,
field,
params_type,
&name_sets,
&bearing,
)?;
}
}
}
for procedure in &schema.procedures {
for arg in &procedure.args {
if let Some(name) = first_bearing_reference(&arg.ty, &bearing) {
return Err(span_error(
format!(
"procedure `{}` argument `{}` references `{}`, which contains \
`@computed` fields — computed fields exist only in responses (the \
client-side shape includes them, the server-side shape doesn't), so \
a computed-bearing type cannot be used as procedure input",
procedure.name, arg.name, name,
),
arg.span,
));
}
}
let is_stream = procedure.attributes.iter().any(|a| a.raw == "@stream");
if is_stream && let Some(name) = first_bearing_reference(&procedure.return_type, &bearing) {
return Err(span_error(
format!(
"procedure `{}` declares @stream but returns `{}`, which contains \
`@computed` fields — computed-field resolution inside the incremental \
stream encoder is not supported yet; drop @stream (buffered list \
responses resolve computed fields) or remove the computed field",
procedure.name, name,
),
procedure.span,
));
}
}
Ok(())
}