use cratestack_core::{
Attribute, ComputedParamsArg, Field, is_computed_attribute, parse_computed_params_arg,
};
use crate::diagnostics::{SchemaError, span_error};
#[derive(Clone, Copy)]
pub(super) enum ComputedFieldSupport {
Rejected,
Supported,
}
pub(super) fn validate_computed_field_attribute(
field: &Field,
owner_kind: &str,
owner_name: &str,
support: ComputedFieldSupport,
) -> Result<(), SchemaError> {
let mut computed_count = 0usize;
for attribute in &field.attributes {
if !is_computed_attribute(attribute) {
continue;
}
computed_count += 1;
if attribute.raw != "@computed" {
validate_computed_argument_form(field, owner_kind, owner_name, attribute)?;
}
if matches!(support, ComputedFieldSupport::Rejected) {
return Err(span_error(
format!(
"field `{}` on {} `{}` cannot use `@computed`; resolver-backed computed fields are only supported on `type` and `model` declarations",
field.name, owner_kind, owner_name,
),
field.span,
));
}
}
if computed_count > 1 {
return Err(span_error(
format!(
"field `{}` on {} `{}` declares `@computed` more than once",
field.name, owner_kind, owner_name,
),
field.span,
));
}
if computed_count == 1 && field.attributes.len() > 1 {
let other = field
.attributes
.iter()
.find(|attribute| !is_computed_attribute(attribute))
.map(|attribute| attribute.raw.as_str())
.unwrap_or_default();
return Err(span_error(
format!(
"field `{}` on {} `{}` combines `@computed` with `{}`; a computed field is \
resolved at response-composition time and is never stored or accepted as \
input, so no other field attribute applies to it",
field.name, owner_kind, owner_name, other,
),
field.span,
));
}
Ok(())
}
fn validate_computed_argument_form(
field: &Field,
owner_kind: &str,
owner_name: &str,
attribute: &Attribute,
) -> Result<(), SchemaError> {
let unsupported_directive_error = || {
span_error(
format!(
"field `{}` on {} `{}` uses unsupported computed field directive `{}`; use bare `@computed` or `@computed(params: <Type>?)`",
field.name, owner_kind, owner_name, attribute.raw,
),
field.span,
)
};
let Some(inner) = attribute
.raw
.strip_prefix("@computed(")
.and_then(|rest| rest.strip_suffix(')'))
else {
return Err(unsupported_directive_error());
};
match parse_computed_params_arg(inner) {
ComputedParamsArg::Optional(_) => Ok(()),
ComputedParamsArg::Required(name) => Err(span_error(
format!(
"field `{}` on {} `{}` uses `@computed(params: {name})`; required computed \
params are not supported yet — add a trailing `?` (`@computed(params: \
{name}?)`). Params are always optional in v1: a required param would make \
plain CRUD reads unsatisfiable, and there is no wire slot for one on \
non-read paths.",
field.name, owner_kind, owner_name,
),
field.span,
)),
ComputedParamsArg::Unsupported => Err(unsupported_directive_error()),
}
}