1use crate::condition_editor::{check_condition_editor, fields_error};
2use crate::{FormError, FormResult};
3use polyspec_crudui_validator::compose::{
4 compose_properties, member_ordered, member_ordered_map, ComposeOptions, FileLoader,
5 MemoryLoader,
6};
7use polyspec_crudui_validator::validate::group_source::{group_source_failure, scalar_child};
8use polyspec_crudui_validator::validate::numeric;
9use serde::{Deserialize, Serialize};
10use serde_json::{Map, Value};
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14pub struct FieldTemplate {
15 pub name: String,
17 pub spec: Map<String, Value>,
19 pub children: Vec<FieldTemplate>,
21}
22
23#[derive(Debug, Clone, Serialize, PartialEq)]
28#[serde(rename_all = "camelCase")]
29pub struct FormTemplate {
30 pub kind: String,
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub key_prefix: Option<String>,
35 pub fields: Vec<FieldTemplate>,
37 pub buttons: Vec<Map<String, Value>>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub action: Option<Map<String, Value>>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub description: Option<Value>,
46}
47
48impl FormTemplate {
49 pub fn from_json(value: &Value) -> FormResult<FormTemplate> {
56 let shape = || FormError::input("Unsupported form template");
57 let object = value.as_object().ok_or_else(shape)?;
58 if !only_members(
59 object,
60 &[
61 "kind",
62 "keyPrefix",
63 "fields",
64 "buttons",
65 "action",
66 "description",
67 ],
68 ) || object.get("kind").and_then(Value::as_str) != Some("crudui/form-template")
69 {
70 return Err(shape());
71 }
72 let key_prefix = match object.get("keyPrefix") {
73 None => None,
74 Some(Value::String(prefix)) => Some(prefix.clone()),
75 Some(_) => return Err(shape()),
76 };
77 let action = match object.get("action") {
78 None => None,
79 Some(Value::Object(action)) => Some(action.clone()),
80 Some(_) => return Err(shape()),
81 };
82 let fields = field_templates(object.get("fields")).ok_or_else(shape)?;
83 let buttons = object
84 .get("buttons")
85 .and_then(Value::as_array)
86 .ok_or_else(shape)?
87 .iter()
88 .map(|button| button.as_object().cloned())
89 .collect::<Option<Vec<_>>>()
90 .ok_or_else(shape)?;
91 Ok(FormTemplate {
92 kind: "crudui/form-template".into(),
93 key_prefix,
94 fields,
95 buttons,
96 action,
97 description: object.get("description").cloned(),
98 })
99 }
100}
101
102impl<'de> Deserialize<'de> for FormTemplate {
103 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
104 let value = Value::deserialize(deserializer)?;
105 FormTemplate::from_json(&value).map_err(serde::de::Error::custom)
106 }
107}
108
109fn only_members(object: &Map<String, Value>, allowed: &[&str]) -> bool {
110 object.keys().all(|key| allowed.contains(&key.as_str()))
111}
112
113fn field_templates(value: Option<&Value>) -> Option<Vec<FieldTemplate>> {
115 value?
116 .as_array()?
117 .iter()
118 .map(|field| {
119 let field = field.as_object()?;
120 if field.len() != 3 || !only_members(field, &["name", "spec", "children"]) {
121 return None;
122 }
123 Some(FieldTemplate {
124 name: field.get("name")?.as_str()?.to_owned(),
125 spec: field.get("spec")?.as_object()?.clone(),
126 children: field_templates(field.get("children"))?,
127 })
128 })
129 .collect()
130}
131
132#[derive(Default)]
134pub struct CompileOptions<'a> {
135 pub files: Map<String, Value>,
137 pub loader: Option<&'a dyn FileLoader>,
139 pub basepath: String,
141 pub key_prefix: Option<String>,
143}
144
145fn condition_value(value: &Value) -> bool {
147 value.is_string() || value.as_object().is_some_and(|map| !map.is_empty())
148}
149
150const MULTIPLE_KEYS: &[&str] = &[
152 "only", "min", "max", "copy", "sortable", "title", "controls", "header", "onclick", "keys",
153];
154const ONLY_MULTIPLE_KEYS: &[&str] = &["only", "title", "header", "keys"];
156const LANG_KEYS: &[&str] = &[
158 "mode",
159 "only",
160 "name",
161 "key",
162 "frame",
163 "title",
164 "group_class",
165];
166const DESIGN_KEYS: &[&str] = &[
168 "show", "class", "style", "label", "wrapper", "group", "prepend",
169];
170const DESIGN_NODE_KEYS: &[&str] = &["class", "style"];
172const CONDITION_DESIGN_KEYS: &[&str] = &[
175 "show",
176 "class",
177 "style",
178 "attributes",
179 "editor",
180 "label",
181 "wrapper",
182 "group",
183 "prepend",
184];
185const FIELD_DESIGN_KEYS: &[&str] = &[
186 "show",
187 "class",
188 "style",
189 "attributes",
190 "label",
191 "wrapper",
192 "group",
193 "prepend",
194];
195const GROUP_DESIGN_KEYS: &[&str] = &[
197 "show",
198 "class",
199 "style",
200 "attributes",
201 "layout",
202 "label",
203 "wrapper",
204 "group",
205 "prepend",
206];
207const FIELD_WRAPPER_KEYS: &[&str] = &["class", "style", "attributes"];
209const OWNED_ATTRIBUTE_PREFIXES: &[&str] = &["data-crudui-", "data-source-"];
211const OWNED_ATTRIBUTE_NAMES: &[&str] = &[
213 "data-field-path",
214 "data-lang",
215 "data-name",
216 "data-rule-name",
217 "data-default",
218 "data-is-default",
219 "data-type",
220 "data-height",
221 "data-upload-server",
222 "data-fileserver",
223 "data-server",
224 "data-max-tags",
225 "data-keyword-min-length",
226 "data-delay",
227 "data-api-server",
228 "data-max-width",
229 "data-min-width",
230 "data-max-height",
231 "data-min-height",
232 "data-preview-max-width",
233 "data-preview-max-height",
234 "data-unsupported-type",
235];
236
237fn declared_attribute_name(name: &str) -> bool {
240 let Some(rest) = name
241 .strip_prefix("data-")
242 .or_else(|| name.strip_prefix("aria-"))
243 else {
244 return false;
245 };
246 let valid = !rest.is_empty()
247 && rest.bytes().enumerate().all(|(index, c)| {
248 c.is_ascii_lowercase()
249 || c.is_ascii_digit()
250 || (index > 0 && (c == b'-' || c == b'_' || c == b'.'))
251 });
252 valid
253 && !OWNED_ATTRIBUTE_NAMES.contains(&name)
254 && !OWNED_ATTRIBUTE_PREFIXES
255 .iter()
256 .any(|prefix| name.starts_with(prefix))
257}
258
259pub(crate) fn check_declared_attributes(
262 attributes: &Value,
263 key: &str,
264 path: &str,
265) -> FormResult<()> {
266 let Some(attributes) = attributes.as_object() else {
267 return Err(FormError::input(format!(
268 "Invalid {key} at {path}: expected an object"
269 )));
270 };
271 if let Some(name) = attributes
272 .keys()
273 .find(|name| !declared_attribute_name(name))
274 {
275 return Err(FormError::input(format!(
276 "Invalid {key}.{name} at {path}: expected a data-* or aria-* name that crudui does not write"
277 )));
278 }
279 if let Some((name, _)) = attributes.iter().find(|(_, value)| !value.is_string()) {
280 return Err(FormError::input(format!(
281 "Invalid {key}.{name} at {path}: expected a string"
282 )));
283 }
284 Ok(())
285}
286const BEHAVIOR_KEYS: &[&str] = &["onchange", "onclick", "onload"];
288
289pub(crate) fn check_known_keys(
291 bucket: &str,
292 settings: &Map<String, Value>,
293 allowed: &[&str],
294 path: &str,
295) -> FormResult<()> {
296 match settings.keys().find(|key| !allowed.contains(&key.as_str())) {
297 Some(key) => Err(FormError::input(format!(
298 "Invalid {bucket}.{key} at {path}: unknown key"
299 ))),
300 None => Ok(()),
301 }
302}
303
304pub(crate) fn check_design_declaration(
309 design: &Value,
310 path: &str,
311 field: bool,
312 layouts: Option<&[&str]>,
313 condition: bool,
314) -> FormResult<()> {
315 let fail = |key: &str, expected: &str| -> FormResult<()> {
316 Err(FormError::input(format!(
317 "Invalid {key} at {path}: expected {expected}"
318 )))
319 };
320 if !design.is_boolean() && !design.is_object() {
321 return fail("design", "a boolean or an object");
322 }
323 let Some(design) = design.as_object() else {
324 return Ok(());
325 };
326 let design_keys = if layouts.is_some() {
327 GROUP_DESIGN_KEYS
328 } else if condition {
329 CONDITION_DESIGN_KEYS
330 } else if field {
331 FIELD_DESIGN_KEYS
332 } else {
333 DESIGN_KEYS
334 };
335 check_known_keys("design", design, design_keys, path)?;
336 if design
337 .get("show")
338 .is_some_and(|v| !v.is_boolean() && !condition_value(v))
339 {
340 return fail("design.show", "an expression, a boolean or a condition map");
341 }
342 for key in ["class", "style"] {
343 if design.get(key).is_some_and(|v| !condition_value(v)) {
344 return fail(&format!("design.{key}"), "a string or a condition map");
345 }
346 }
347 if let Some(attributes) = design.get("attributes") {
348 check_declared_attributes(attributes, "design.attributes", path)?;
349 }
350 if let (Some(layouts), Some(layout)) = (layouts, design.get("layout")) {
351 if !layout
352 .as_str()
353 .is_some_and(|layout| layouts.contains(&layout))
354 {
355 return fail(
356 "design.layout",
357 if layouts.len() == 3 {
358 "stacked, inline or line"
359 } else {
360 "stacked or inline"
361 },
362 );
363 }
364 }
365 for node in ["label", "wrapper", "group", "prepend"] {
366 let Some(value) = design.get(node) else {
367 continue;
368 };
369 let Some(value) = value.as_object() else {
370 return fail(&format!("design.{node}"), "an object");
371 };
372 let node_keys = if field && node == "wrapper" {
373 FIELD_WRAPPER_KEYS
374 } else {
375 DESIGN_NODE_KEYS
376 };
377 check_known_keys(&format!("design.{node}"), value, node_keys, path)?;
378 for key in ["class", "style"] {
379 if value.get(key).is_some_and(|v| !condition_value(v)) {
380 return fail(
381 &format!("design.{node}.{key}"),
382 "a string or a condition map",
383 );
384 }
385 }
386 if let Some(attributes) = value.get("attributes") {
387 check_declared_attributes(attributes, &format!("design.{node}.attributes"), path)?;
388 }
389 }
390 Ok(())
391}
392
393fn check_declarations(spec: &Map<String, Value>, path: &str, field: bool) -> FormResult<()> {
396 let fail = |key: &str, expected: &str| -> FormResult<()> {
397 Err(FormError::input(format!(
398 "Invalid {key} at {path}: expected {expected}"
399 )))
400 };
401 for key in ["buttons", "action"] {
403 if spec.contains_key(key) {
404 return fail(key, "the form root");
405 }
406 }
407 if let Some(multiple) = spec.get("multiple") {
408 if !multiple.is_boolean() && multiple != "only" && !multiple.is_object() {
409 return fail("multiple", "a boolean, only or an object");
410 }
411 if let Some(settings) = multiple.as_object() {
412 check_known_keys("multiple", settings, MULTIPLE_KEYS, path)?;
413 if settings.get("only").is_some_and(|v| !v.is_boolean()) {
414 return fail("multiple.only", "a boolean");
415 }
416 if settings.get("only").is_some_and(|v| *v == true) {
419 check_known_keys("multiple", settings, ONLY_MULTIPLE_KEYS, path)?;
420 }
421 if let Some(keys) = settings.get("keys") {
423 if settings.get("only").is_none_or(|v| *v != true) {
424 return fail("multiple.keys", "multiple.only: true");
425 }
426 if !keys.is_string() {
427 return fail("multiple.keys", "a reference to a repeated group");
428 }
429 }
430 for key in ["min", "max"] {
431 if settings.get(key).is_some_and(|v| !v.is_number()) {
432 return fail(&format!("multiple.{key}"), "a number");
433 }
434 }
435 for key in ["copy", "sortable"] {
436 if settings.get(key).is_some_and(|v| !v.is_boolean()) {
437 return fail(&format!("multiple.{key}"), "a boolean");
438 }
439 }
440 if let Some(title) = settings.get("title") {
441 if spec.get("type").is_none_or(|t| t != "group") {
442 return fail("multiple.title", "a repeated group");
443 }
444 let child = title.as_str().and_then(|name| {
445 spec.get("properties")
446 .and_then(Value::as_object)
447 .and_then(|properties| properties.get(name))
448 });
449 if !child.is_some_and(scalar_child) {
450 return fail(
451 "multiple.title",
452 "the name of a direct child field without multiple, properties or lang",
453 );
454 }
455 }
456 if settings
457 .get("controls")
458 .is_some_and(|v| !["header", "footer", "outline"].iter().any(|p| v == p))
459 {
460 return fail("multiple.controls", "header, footer or outline");
461 }
462 if settings
463 .get("header")
464 .is_some_and(|v| !["static", "sticky"].iter().any(|p| v == p))
465 {
466 return fail("multiple.header", "static or sticky");
467 }
468 }
469 }
470 if spec
471 .get("lang")
472 .is_some_and(|v| !v.is_boolean() && !v.is_object())
473 {
474 return fail("lang", "a boolean or an object");
475 }
476 if let Some(lang) = spec.get("lang").and_then(Value::as_object) {
477 check_known_keys("lang", lang, LANG_KEYS, path)?;
478 }
479 if let Some(only) = spec
480 .get("lang")
481 .and_then(Value::as_object)
482 .and_then(|lang| lang.get("only"))
483 {
484 let codes = only
485 .as_array()
486 .is_some_and(|codes| codes.iter().all(Value::is_string));
487 if !codes && !only.is_object() {
488 return fail("lang.only", "a list of language codes or an object");
489 }
490 }
491 if let Some(design) = spec.get("design") {
492 let layouts = if field { group_layouts(spec) } else { None };
493 check_design_declaration(design, path, field, layouts, field && is_condition(spec))?;
494 }
495 if let Some(behavior) = spec.get("behavior").and_then(Value::as_object) {
496 check_known_keys("behavior", behavior, BEHAVIOR_KEYS, path)?;
497 }
498 if field
499 && spec
500 .get("type")
501 .and_then(Value::as_str)
502 .is_some_and(|t| t.to_lowercase() == "range")
503 {
504 check_range_declaration(spec, path)?;
505 }
506 if field && is_condition(spec) {
507 check_condition_declaration(spec, path)?;
508 check_condition_editor(spec, path)?;
509 } else if field
510 && spec
511 .get("options")
512 .and_then(Value::as_object)
513 .is_some_and(|options| options.contains_key("fields"))
514 {
515 return Err(fields_error(path));
516 }
517 Ok(())
518}
519
520fn is_condition(spec: &Map<String, Value>) -> bool {
522 spec.get("type")
523 .and_then(Value::as_str)
524 .is_some_and(|t| t.to_lowercase() == "condition")
525}
526
527fn check_condition_declaration(spec: &Map<String, Value>, path: &str) -> FormResult<()> {
530 let parameter = spec
531 .get("validate")
532 .and_then(|v| v.get("conditionTree"))
533 .unwrap_or(&Value::Null);
534 if *parameter == Value::Bool(true) || parameter.as_str() == Some("judge") {
535 return Ok(());
536 }
537 Err(FormError::input(format!(
538 "Invalid validate.conditionTree at {path}: expected true or judge"
539 )))
540}
541
542fn check_range_declaration(spec: &Map<String, Value>, path: &str) -> FormResult<()> {
545 let validate = spec.get("validate").filter(|v| v.is_object());
546 let rule = |key: &str| validate.and_then(|v| v.get(key)).unwrap_or(&Value::Null);
547 let Some((minimum, _)) = numeric::number_range(rule("range")) else {
548 return Err(FormError::input(format!(
549 "Invalid validate.range at {path}: expected [minimum, maximum] finite numbers with minimum not above maximum"
550 )));
551 };
552 let Some(step) = numeric::step(rule("step")) else {
553 return Err(FormError::input(format!(
554 "Invalid validate.step at {path}: expected a finite number above 0"
555 )));
556 };
557 if !numeric::is_multiple(minimum, step) {
558 return Err(FormError::input(format!(
559 "Invalid validate.range at {path}: expected a minimum that is a multiple of validate.step"
560 )));
561 }
562 Ok(())
563}
564
565fn group_layouts(spec: &Map<String, Value>) -> Option<&'static [&'static str]> {
568 if spec.get("type").is_none_or(|t| t != "group") {
569 return None;
570 }
571 let repeated = spec
572 .get("multiple")
573 .is_some_and(|v| *v == true || *v == "only" || v.is_object());
574 Some(if repeated {
575 &["stacked", "inline"]
576 } else {
577 &["stacked", "inline", "line"]
578 })
579}
580
581fn check_form_declarations(spec: &Map<String, Value>) -> FormResult<()> {
583 let fail = |key: &str, expected: &str| -> FormResult<()> {
584 Err(FormError::input(format!(
585 "Invalid {key} at form: expected {expected}"
586 )))
587 };
588 if let Some(action) = spec.get("action") {
589 let Some(action) = action.as_object() else {
590 return fail("action", "an object");
591 };
592 for key in ["method", "url", "enctype"] {
593 if action.get(key).is_some_and(|v| !v.is_string()) {
594 return fail(&format!("action.{key}"), "a string");
595 }
596 }
597 }
598 let Some(buttons) = spec.get("buttons") else {
599 return Ok(());
600 };
601 let Some(buttons) = buttons.as_array() else {
602 return fail("buttons", "a list of buttons");
603 };
604 for (index, button) in buttons.iter().enumerate() {
605 let key = format!("buttons.{index}");
606 let Some(button) = button.as_object() else {
607 return fail(&key, "an object");
608 };
609 let kind = button.get("type").and_then(Value::as_str).unwrap_or("");
610 if !crate::buttons::FORM_BUTTON_TYPES.contains(&kind) {
611 return fail(&format!("{key}.type"), "submit, reset, button or link");
612 }
613 for name in ["name", "value", "href"] {
614 if button.get(name).is_some_and(|v| !v.is_string()) {
615 return fail(&format!("{key}.{name}"), "a string");
616 }
617 }
618 if crate::buttons::button_text(crate::messages::form_messages("ko")?, kind).is_empty()
620 && !button.contains_key("text")
621 {
622 return fail(&format!("{key}.text"), "content for this button type");
623 }
624 if kind == "link" && !button.contains_key("href") {
625 return fail(&format!("{key}.href"), "a link target");
626 }
627 check_declarations(button, &format!("form.{key}"), false)?;
628 }
629 Ok(())
630}
631
632fn fields(
633 root: &Map<String, Value>,
634 properties: &Map<String, Value>,
635 parent: &str,
636) -> FormResult<Vec<FieldTemplate>> {
637 let mut out = Vec::new();
638 for (name, value) in properties {
639 let Some(raw) = value.as_object() else {
640 continue;
641 };
642 let path = if parent.is_empty() {
643 name.clone()
644 } else {
645 format!("{parent}.{name}")
646 };
647 check_declarations(raw, &path, true)?;
648 if let Some(message) = group_source_failure(value, &path, root) {
650 return Err(FormError::input(message));
651 }
652 let mut spec = raw.clone();
653 let children = match spec.shift_remove("properties") {
654 Some(Value::Object(children)) => fields(root, &children, &path)?,
655 _ => Vec::new(),
656 };
657 out.push(FieldTemplate {
658 name: name.clone(),
659 spec,
660 children,
661 });
662 }
663 Ok(out)
664}
665
666fn member_ordered_field(field: &FieldTemplate) -> FieldTemplate {
668 FieldTemplate {
669 name: field.name.clone(),
670 spec: member_ordered_map(&field.spec),
671 children: field.children.iter().map(member_ordered_field).collect(),
672 }
673}
674
675pub(crate) fn member_ordered_template(template: &FormTemplate) -> FormTemplate {
678 FormTemplate {
679 kind: template.kind.clone(),
680 key_prefix: template.key_prefix.clone(),
681 fields: template.fields.iter().map(member_ordered_field).collect(),
682 buttons: template.buttons.iter().map(member_ordered_map).collect(),
683 action: template.action.as_ref().map(member_ordered_map),
684 description: template.description.as_ref().map(member_ordered),
685 }
686}
687
688pub fn compile_form(spec: &Value, options: &CompileOptions<'_>) -> FormResult<FormTemplate> {
690 let spec = &member_ordered(spec);
692 if spec["type"] != "group" || !spec["properties"].is_object() {
693 return Err(FormError::input(
694 "A form spec must be a group with properties",
695 ));
696 }
697 check_form_declarations(spec.as_object().expect("checked group"))?;
698 if spec["design"].get("layout").is_some() {
700 return Err(FormError::input(
701 "Invalid design.layout at form: unknown key",
702 ));
703 }
704 let memory = MemoryLoader::new(options.files.clone());
705 let properties = compose_properties(
706 spec["properties"]
707 .as_object()
708 .expect("checked properties")
709 .clone(),
710 options.loader.unwrap_or(&memory),
711 &ComposeOptions::with_basepath(&options.basepath),
712 )?;
713 Ok(FormTemplate {
714 kind: "crudui/form-template".into(),
715 key_prefix: options.key_prefix.clone(),
716 fields: fields(&properties, &properties, "")?,
717 buttons: match spec.get("buttons").and_then(Value::as_array) {
718 Some(declared) => declared
719 .iter()
720 .filter_map(|b| b.as_object().cloned())
721 .collect(),
722 None => vec![Map::from_iter([(
723 "type".to_string(),
724 Value::from("submit"),
725 )])],
726 },
727 action: spec.get("action").and_then(Value::as_object).cloned(),
728 description: spec.get("description").cloned(),
729 })
730}
731
732pub(crate) fn repeats(field: &FieldTemplate) -> bool {
733 field
734 .spec
735 .get("multiple")
736 .is_some_and(|v| *v == true || *v == "only" || v.is_object())
737}
738
739pub(crate) fn data_only(field: &FieldTemplate) -> bool {
741 field
742 .spec
743 .get("multiple")
744 .is_some_and(|v| *v == "only" || v.get("only").is_some_and(|only| *only == true))
745}