use syn::DeriveInput;
use crate::derives::gpui_form::components::generate_component_field;
use crate::derives::gpui_form::intent::{ComponentFieldIntent, ComponentStruct};
use crate::derives::gpui_form::ir::{
ComponentFieldPlan, ConvertedValueIntent, FieldPlan, HolderFieldIr, HolderStoragePlan,
KorumaValidationInfo, RenderedValueIntent, Spanned, TypeOverride,
};
use crate::derives::gpui_form::utils::extract_option_inner_type;
use crate::derives::gpui_form::validation::{
KorumaOptions, parse_koruma_fields, validation_metadata_from_koruma,
};
pub struct FormPlan {
pub fields: Vec<FieldPlan>,
pub enable_koruma_fluent: bool,
pub effective_enable_koruma: bool,
}
impl FormPlan {
#[allow(dead_code)]
pub fn non_skipped_fields(&self) -> impl Iterator<Item = &HolderFieldIr> {
self.fields.iter().filter_map(FieldPlan::shared)
}
pub fn component_fields(&self) -> impl Iterator<Item = &ComponentFieldPlan> {
self.fields.iter().filter_map(FieldPlan::component_plan)
}
}
pub fn plan_form(
parsed: &ComponentStruct,
derive_input: &DeriveInput,
koruma_options: Option<&KorumaOptions>,
) -> syn::Result<FormPlan> {
let fields_iter = match &parsed.data {
darling::ast::Data::Struct(s) => &s.fields,
_ => {
return Err(syn::Error::new_spanned(
derive_input,
"`#[derive(GpuiForm)]` only supports named structs",
));
},
};
let parsed_koruma_fields = parse_koruma_fields(derive_input)?;
let enable_koruma = koruma_options.is_some() || !parsed_koruma_fields.is_empty();
let enable_koruma_fluent = koruma_options.map(|k| k.fluent).unwrap_or(false);
let mut fields = Vec::new();
for field in fields_iter {
let field_name = field.context.field_ident.clone();
let intent = field.intent();
if matches!(intent, ComponentFieldIntent::Skipped(_)) {
fields.push(FieldPlan::skipped(
field_name,
field.context.field_ty.clone(),
));
continue;
}
let field_name_str = field_name.to_string();
let (was_optional, source_value_type) = extract_option_inner_type(&field.context.field_ty);
let rendered = field.rendered().ok_or_else(|| {
syn::Error::new_spanned(
&field_name,
"field attribute must provide rendered options unless the field is `skip`d",
)
})?;
let value_mapping =
lower_rendered_value_mapping(rendered.value.clone(), &source_value_type);
let form_type = match &value_mapping {
RenderedValueIntent::Identity => source_value_type.clone(),
RenderedValueIntent::Converted(converted) => {
extract_option_inner_type(&converted.form_type.value.0).1
},
RenderedValueIntent::KorumaNewtype { .. } => {
unreachable!("`value(koruma_newtype)` should be lowered before form type planning")
},
};
let (component, storage) = match field.component() {
Some(component) => {
let component = generate_component_field(
&field_name,
&form_type,
component.component,
component.rendered.context,
)?;
let storage = HolderStoragePlan::from_component_shape_storage_policy(
was_optional,
component.storage_policy.clone(),
);
(Some(component), storage)
},
None => (
None,
HolderStoragePlan::from_non_component_field(was_optional),
),
};
let koruma_info = parsed_koruma_fields.get(&field_name_str);
let validation = koruma_info
.map(KorumaValidationInfo::from)
.unwrap_or_default();
let validation_metadata = koruma_info
.map(validation_metadata_from_koruma)
.unwrap_or_default();
let shared = HolderFieldIr {
field_name,
original_type: field.context.field_ty.clone(),
source_value_type,
form_type,
was_optional,
storage,
validation,
validation_metadata,
metadata: field.metadata().clone(),
default_expr: rendered.default.map(|expr| expr.value.0.clone()),
default_span: rendered.default.map(|expr| expr.span),
value_mapping,
context: rendered.context.clone(),
};
if let Some(component) = component {
fields.push(FieldPlan::component_field(shared, component));
} else {
fields.push(FieldPlan::hidden(shared));
}
}
let has_fields_needing_required = fields
.iter()
.filter_map(FieldPlan::shared)
.any(|field| field.needs_required_validation() && !field.validation().is_newtype);
let has_any_koruma_validations = fields.iter().filter_map(FieldPlan::shared).any(|field| {
let validation = field.validation();
!validation.field_validators.is_empty()
|| !validation.element_validators.is_empty()
|| validation.is_nested
});
let effective_enable_koruma =
enable_koruma || (has_fields_needing_required && has_any_koruma_validations);
Ok(FormPlan {
fields,
enable_koruma_fluent,
effective_enable_koruma,
})
}
fn lower_rendered_value_mapping(
value_mapping: RenderedValueIntent,
source_value_type: &syn::Type,
) -> RenderedValueIntent {
let RenderedValueIntent::KorumaNewtype { span } = value_mapping else {
return value_mapping;
};
let form_type = syn::parse_quote_spanned! {span=>
<#source_value_type as ::koruma::NewtypeValue>::Inner
};
let from_source = syn::parse_quote_spanned! {span=>
<#source_value_type as ::koruma::NewtypeValue>::into_inner
};
let into_source = syn::parse_quote_spanned! {span=>
<#source_value_type as ::koruma::NewtypeTryFromInner>::try_from_inner
};
RenderedValueIntent::Converted(Box::new(ConvertedValueIntent {
form_type: Spanned {
value: TypeOverride(form_type),
span,
},
from_source: Spanned {
value: from_source,
span,
},
into_source: Spanned {
value: into_source,
span,
},
into_source_is_fallible: true,
}))
}