pub(crate) mod embedded;
mod fields;
mod layouts;
mod lenses;
mod options;
mod relationship;
mod tree;
mod validation;
use std::collections::{HashMap, HashSet};
pub use embedded::EmbeddedForm;
pub(crate) use fields::option_view;
pub use fields::{
ChoiceField, Control, ControlInput, CustomField, Field, FileField, IntoOptions, TextField,
Toggle,
};
pub use layouts::{Grid, Group, Repeater, Section};
pub(crate) use lenses::{ResolvedLens, declare_with, lens_field, lens_field_unique, schema_of};
pub use lenses::{declare, form_key};
pub use options::Options;
pub(crate) use relationship::OptionLoadError;
pub use relationship::{MAX_RELATIONSHIP_OPTIONS, OptionSource};
use topcoat::{Result, context::Cx, view::*};
pub use tree::{IntoSchema, Source};
pub(crate) use tree::{LeafPlace, Node, render_nodes, walk_absent_groups};
pub use validation::TypedValue;
pub(crate) use validation::required_error;
use crate::form::FieldErrors;
#[derive(Debug, Clone)]
pub(crate) struct ControlCheck {
pub(crate) name: String,
pub(crate) required: bool,
pub(crate) required_error: Option<String>,
pub(crate) in_repeater: bool,
pub(crate) place: LeafPlace,
}
impl ControlCheck {
pub(crate) fn needs_answer(&self) -> bool {
match self.place {
LeafPlace::Rendered => true,
LeafPlace::Payload => self.in_repeater,
LeafPlace::Discriminant => false,
}
}
}
#[derive(Debug, Default)]
pub struct Schema {
pub(crate) nodes: Vec<Node>,
pub(crate) fields: Vec<Field>,
}
impl Schema {
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn new(children: impl IntoSchema) -> Self {
children.into_schema()
}
pub fn empty() -> Self {
Self::default()
}
pub fn fields(&self) -> impl Iterator<Item = &Field> {
self.fields.iter()
}
pub async fn render<'a>(&self, cx: &'a Cx, source: Source<'_>) -> Result<BoxView<'a>> {
let errors = self.declaration_errors();
if !errors.is_empty() {
return Err(crate::error::misdeclared(&errors));
}
render_nodes(cx, &self.nodes, &self.fields, &source).await
}
pub(crate) fn append(&mut self, other: Schema) {
let Schema { mut nodes, fields } = other;
let offset = self.fields.len();
for node in &mut nodes {
node.offset(offset);
}
self.fields.extend(fields);
self.nodes.extend(nodes);
}
pub(crate) fn embedded_root(&self) -> &embedded::Embedded {
match self.nodes.as_slice() {
[Node::Embedded(node)] => node,
_ => panic!("an embedded value's schema is its one embedded node"),
}
}
pub fn normalize_values(&self, values: &mut HashMap<String, String>) {
for field in &self.fields {
let Some(submitted) = values.get_mut(field.name()) else {
continue;
};
if submitted.trim().is_empty() {
continue;
}
if let Ok(normalized) = field.normalize(submitted) {
*submitted = normalized;
}
}
}
pub fn extend(mut self, other: Schema) -> Schema {
self.append(other);
self
}
pub(crate) fn controls(&self) -> Vec<ControlCheck> {
fn mark(nodes: &[Node], inside: bool, in_repeater: &mut [bool], place: &mut [LeafPlace]) {
for node in nodes {
match node {
Node::Repeater(r) => mark(&r.children.nodes, true, in_repeater, place),
Node::Field(_) | Node::Embedded(_) => {
node.visit_fields(&mut |index, leaf_place| {
in_repeater[index] = inside;
place[index] = leaf_place;
});
}
_ => mark(
node.children().unwrap_or_default(),
inside,
in_repeater,
place,
),
}
}
}
let mut in_repeater = vec![false; self.fields.len()];
let mut place = vec![LeafPlace::Rendered; self.fields.len()];
mark(&self.nodes, false, &mut in_repeater, &mut place);
self.fields
.iter()
.zip(in_repeater)
.zip(place)
.map(|((field, in_repeater), place)| {
let errors = field.validate("");
ControlCheck {
name: field.name().to_string(),
required: !errors.is_empty(),
required_error: errors
.into_iter()
.next()
.map(|error| error.message)
.or_else(|| Some(required_error(field.label_str()))),
in_repeater,
place,
}
})
.collect()
}
pub fn unknown_keys(&self, values: &HashMap<String, String>) -> Vec<String> {
let known: HashSet<&str> = self.fields.iter().map(Field::name).collect();
let mut out: Vec<String> = values
.keys()
.filter(|k| !known.contains(k.as_str()))
.cloned()
.collect();
out.sort();
out
}
pub fn declaration_errors(&self) -> Vec<crate::DeclarationErrorKind> {
let mut errors = Vec::new();
let mut seen = HashSet::new();
for field in &self.fields {
match field.misdeclared() {
Some(error) => errors.push(error.clone()),
None if !seen.insert(field.name()) => {
errors.push(crate::DeclarationErrorKind::DuplicateField {
name: field.name().to_string(),
});
}
None => {}
}
}
errors
}
pub fn validate(&self, values: &HashMap<String, String>) -> FieldErrors {
let mut errors = FieldErrors::new();
let mut skip: HashSet<String> = HashSet::new();
walk_absent_groups(
&self.nodes,
&self.fields,
values,
&mut skip,
&mut errors,
false,
);
for field in &self.fields {
if skip.contains(field.name()) {
continue;
}
let value = values.get(field.name()).map(String::as_str).unwrap_or("");
for error in field.validate(value) {
errors.push(error);
}
}
errors
}
pub(crate) fn absent_fields(&self, values: &HashMap<String, String>) -> HashSet<String> {
let mut skip = HashSet::new();
let mut discarded = FieldErrors::new();
walk_absent_groups(
&self.nodes,
&self.fields,
values,
&mut skip,
&mut discarded,
false,
);
skip
}
pub(crate) fn renders_error_key(&self, values: &HashMap<String, String>, key: &str) -> bool {
fn labels(nodes: &[Node], key: &str) -> bool {
nodes.iter().any(|node| match node {
Node::Repeater(repeater) => {
repeater.label == key || labels(&repeater.children.nodes, key)
}
node => node
.children()
.is_some_and(|children| labels(children, key)),
})
}
let hidden = self.hidden_fields(values);
let renders_field = self
.fields
.iter()
.any(|field| field.name() == key && !hidden.contains(key));
renders_field || labels(&self.nodes, key)
}
fn hidden_fields(&self, values: &HashMap<String, String>) -> HashSet<String> {
fn walk(nodes: &[Node], values: &HashMap<String, String>, out: &mut Vec<usize>) {
for node in nodes {
match node {
Node::Embedded(embedded) => embedded.hidden_fields(values, out),
node => {
if let Some(children) = node.children() {
walk(children, values, out);
}
}
}
}
}
let mut indices = Vec::new();
walk(&self.nodes, values, &mut indices);
indices
.into_iter()
.map(|index| self.fields[index].name().to_string())
.collect()
}
pub async fn validate_async(&self, cx: &Cx, values: &HashMap<String, String>) -> FieldErrors {
let mut errors = self.validate(values);
let absent = self.absent_fields(values);
for field in &self.fields {
let name = field.name();
if errors.contains_key(name) || absent.contains(name) {
continue;
}
let Some(value) = values.get(name) else {
continue;
};
for message in field.validate_exists(cx, value).await {
errors.add(name, message);
}
}
errors
}
pub(crate) async fn recheck_relationships(
&self,
cx: &Cx,
values: &HashMap<String, String>,
ex: &mut dyn toasty::Executor,
) -> FieldErrors {
let mut errors = FieldErrors::new();
let absent = self.absent_fields(values);
for field in &self.fields {
let name = field.name();
if absent.contains(name) {
continue;
}
let Some(value) = values.get(name) else {
continue;
};
for message in field.recheck(cx, value, &mut *ex).await {
errors.add(name, message);
}
}
errors
}
}
#[cfg(test)]
mod tests;