1use proc_macro2::Span;
24use ravenrs_criteria::Criteria;
25use syn::spanned::Spanned;
26use syn::{Data, DeriveInput, Error, Fields, Ident, LitBool, LitInt, LitStr, Result};
27
28macro_rules! opaque_debug {
29 ($($ty:ty),*) => {$(
30 impl std::fmt::Debug for $ty {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 f.debug_struct(stringify!($ty)).finish_non_exhaustive()
33 }
34 }
35 )*};
36}
37
38opaque_debug!(
39 ActionSpec,
40 ObjectSettings,
41 KindSpec,
42 FieldSettings,
43 FieldSpec,
44 RuleKindSpec,
45 RuleSpec,
46 ContextSpec,
47 AppearancePiece,
48 AppearanceSpec,
49 ChoiceSpec,
50 ObjectSpec,
51 OptionSpec,
52 EnumSpec,
53 FileSpec
54);
55
56#[derive(Clone, Default)]
58pub struct ObjectSettings {
59 pub caption: Option<LitStr>,
61 pub navigation: Option<LitStr>,
63 pub icon: Option<LitStr>,
65 pub list_criteria: Option<LitStr>,
67 pub form_columns: Option<LitInt>,
69 pub cloneable: bool,
71 pub transient: bool,
73 pub checks_on_leave: bool,
75 pub list_editable: bool,
77 pub data_access: Option<DataAccessSpec>,
79 pub form_actions: Vec<LitStr>,
81 pub focus_order: Vec<String>,
83}
84
85#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub enum DataAccessSpec {
88 Client,
90 Server,
92 DataView,
94}
95
96#[derive(Clone)]
98pub enum KindSpec {
99 Money,
101 LongText,
103 Color,
105 Date,
107 Guid,
109 Tags(Vec<String>),
111 Decimal(u8),
113 Reference(LitStr),
115 Collection(LitStr),
117}
118
119#[derive(Clone, Default)]
121pub struct FieldSettings {
122 pub key: bool,
124 pub typed_key: bool,
126 pub caption: Option<LitStr>,
128 pub default_property: bool,
130 pub read_only: bool,
132 pub no_list: bool,
134 pub no_detail: bool,
136 pub lookup: bool,
138 pub kind: Option<KindSpec>,
140 pub calculated: Option<LitStr>,
142 pub default: Option<syn::Expr>,
144 pub required: bool,
146 pub unique: bool,
148 pub min: Option<syn::Expr>,
150 pub max: Option<syn::Expr>,
152 pub matches: Option<LitStr>,
154 pub message: Option<LitStr>,
156 pub editor: Option<LitStr>,
158 pub placeholder: Option<LitStr>,
160 pub hint: Option<LitStr>,
162 pub accessible_name: Option<LitStr>,
164 pub accessible_description: Option<LitStr>,
166 pub lookup_filter: Option<LitStr>,
168 pub none_caption: Option<LitStr>,
170 pub back_reference: Option<LitStr>,
172 pub most_bytes: Option<LitInt>,
174 pub wide: bool,
176 pub autofocus: bool,
178 pub non_cloneable: bool,
180 pub secret: bool,
182 pub aggregated: bool,
184 pub group: Option<LitStr>,
186 pub display_format: Option<LitStr>,
188 pub edit_mask: Option<LitStr>,
190 pub predefined: Option<Vec<String>>,
192 pub criteria_of: Option<LitStr>,
194}
195
196#[derive(Clone)]
198pub struct FieldSpec {
199 pub ident: Ident,
201 pub ty: syn::Type,
203 pub settings: FieldSettings,
205}
206
207#[derive(Clone)]
209pub enum RuleKindSpec {
210 Required(String),
212 Unique(String),
214 Range {
216 field: String,
218 min: Option<Box<syn::Expr>>,
220 max: Option<Box<syn::Expr>>,
222 },
223 Matches(String, LitStr),
225 Criteria(String, LitStr),
227 UniqueTogether(String, Vec<String>),
229}
230
231#[derive(Clone, Copy, Debug, PartialEq, Eq)]
233pub enum SeveritySpec {
234 Error,
236 Warning,
238 Information,
240}
241
242#[derive(Clone)]
244pub struct RuleSpec {
245 pub kind: RuleKindSpec,
247 pub message: Option<LitStr>,
249 pub context: Option<LitStr>,
251 pub target: Option<LitStr>,
253 pub severity: SeveritySpec,
255}
256
257#[derive(Clone, Copy, Debug, PartialEq, Eq)]
259pub enum TintSpec {
260 Danger,
262 Warning,
264 Success,
266 Accent,
268 Muted,
270}
271
272#[derive(Clone)]
274pub enum ContextSpec {
275 List,
277 Detail,
279 View(LitStr),
281}
282
283#[derive(Clone)]
285pub enum AppearancePiece {
286 Criteria(LitStr),
288 Font(TintSpec),
290 Back(TintSpec),
292 Bold,
294 Enabled(LitBool),
296 Visible(LitBool),
298 Priority(LitInt),
300 Action,
302 Layout,
304 Context(ContextSpec),
306}
307
308#[derive(Clone)]
310pub struct AppearanceSpec {
311 pub id: String,
313 pub targets: Vec<String>,
315 pub pieces: Vec<AppearancePiece>,
317}
318
319#[derive(Clone)]
321pub enum ChoiceSpec {
322 Variant {
324 id: LitStr,
326 caption: LitStr,
328 filter: Option<LitStr>,
330 columns: Vec<String>,
332 },
333 ListFilter {
335 id: LitStr,
337 caption: LitStr,
339 filter: Option<LitStr>,
341 },
342}
343
344#[derive(Clone)]
346pub struct ObjectSpec {
347 pub name: Ident,
349 pub settings: ObjectSettings,
351 pub fields: Vec<FieldSpec>,
353 pub rules: Vec<RuleSpec>,
355 pub appearance: Vec<AppearanceSpec>,
357 pub choices: Vec<ChoiceSpec>,
359 pub actions: Vec<ActionSpec>,
361}
362
363#[derive(Clone, Copy, Debug, PartialEq, Eq)]
365pub enum SelectionSpec {
366 One,
368 Many,
370 Any,
372}
373
374#[derive(Clone)]
376pub struct ActionSpec {
377 pub caption: LitStr,
379 pub id: Option<LitStr>,
381 pub selection: Option<SelectionSpec>,
383 pub enabled: Option<LitStr>,
385 pub set: LitStr,
387 pub confirm: Option<LitStr>,
389}
390
391#[derive(Clone)]
393pub struct OptionSpec {
394 pub ident: Ident,
396 pub code: String,
398 pub caption: String,
400}
401
402#[derive(Clone)]
404pub struct EnumSpec {
405 pub name: Ident,
407 pub options: Vec<OptionSpec>,
409}
410
411#[derive(Clone, Debug, PartialEq, Eq)]
413pub enum TypeKind {
414 Text,
416 Boolean,
418 Integer,
420 Decimal,
422 DateTime,
424 Named(String),
426}
427
428pub fn type_kind(ty: &syn::Type) -> Option<TypeKind> {
431 let syn::Type::Path(path) = ty else { return None };
432 if path.qself.is_some() {
433 return None;
434 }
435 let last = path.path.segments.last()?;
436 let name = last.ident.to_string();
437 if name == "Option" {
438 let syn::PathArguments::AngleBracketed(arguments) = &last.arguments else { return None };
439 let Some(syn::GenericArgument::Type(inner)) = arguments.args.first() else { return None };
440 return type_kind(inner);
441 }
442 if !last.arguments.is_empty() {
443 return None;
444 }
445 Some(match name.as_str() {
446 "String" => TypeKind::Text,
447 "bool" => TypeKind::Boolean,
448 "i32" | "i64" | "u32" | "u64" | "usize" => TypeKind::Integer,
449 "f64" => TypeKind::Decimal,
450 "DateTime" => TypeKind::DateTime,
451 _ => TypeKind::Named(name),
452 })
453}
454
455pub fn split_names(text: &LitStr) -> Vec<String> {
457 text.value().split(',').map(|name| name.trim().to_owned()).filter(|name| !name.is_empty()).collect()
458}
459
460fn object_settings(input: &DeriveInput) -> Result<(ObjectSettings, Option<LitStr>)> {
461 let mut attrs = ObjectSettings::default();
462 let mut focus_order = None;
463 for attr in input.attrs.iter().filter(|a| a.path().is_ident("object")) {
464 attr.parse_nested_meta(|meta| {
465 if meta.path.is_ident("caption") {
466 attrs.caption = Some(meta.value()?.parse()?);
467 } else if meta.path.is_ident("navigation") {
468 attrs.navigation = Some(meta.value()?.parse()?);
469 } else if meta.path.is_ident("icon") {
470 attrs.icon = Some(meta.value()?.parse()?);
471 } else if meta.path.is_ident("list_criteria") {
472 let text: LitStr = meta.value()?.parse()?;
473 Criteria::parse(&text.value()).map_err(|e| Error::new(text.span(), e.to_string()))?;
474 attrs.list_criteria = Some(text);
475 } else if meta.path.is_ident("form_columns") {
476 attrs.form_columns = Some(meta.value()?.parse()?);
477 } else if meta.path.is_ident("cloneable") {
478 attrs.cloneable = true;
479 } else if meta.path.is_ident("transient") {
480 attrs.transient = true;
481 } else if meta.path.is_ident("checks_on_leave") {
482 attrs.checks_on_leave = true;
483 } else if meta.path.is_ident("list_editable") {
484 attrs.list_editable = true;
485 } else if meta.path.is_ident("data_access") {
486 let text: LitStr = meta.value()?.parse()?;
487 attrs.data_access = Some(match text.value().as_str() {
488 "client" => DataAccessSpec::Client,
489 "server" => DataAccessSpec::Server,
490 "data_view" => DataAccessSpec::DataView,
491 _ => return Err(Error::new(text.span(), "use client, server or data_view")),
492 });
493 } else if meta.path.is_ident("form_actions") {
494 attrs.form_actions.push(meta.value()?.parse()?);
495 } else if meta.path.is_ident("focus_order") {
496 focus_order = Some(meta.value()?.parse()?);
497 } else {
498 return Err(meta.error(
499 "unknown setting: use caption, navigation, icon, list_criteria, form_columns, cloneable, \
500 transient, checks_on_leave, list_editable, data_access, form_actions or focus_order",
501 ));
502 }
503 Ok(())
504 })?;
505 }
506 Ok((attrs, focus_order))
507}
508
509fn set_kind(attrs: &mut FieldSettings, path: &syn::Path, kind: KindSpec) -> Result<()> {
510 if attrs.kind.is_some() {
511 return Err(Error::new(path.span(), "a field holds one kind of thing: this says a second"));
512 }
513 attrs.kind = Some(kind);
514 Ok(())
515}
516
517pub fn field_settings(field: &syn::Field) -> Result<FieldSettings> {
519 let mut attrs = FieldSettings::default();
520 for attr in field.attrs.iter().filter(|a| a.path().is_ident("field")) {
521 attr.parse_nested_meta(|meta| {
522 let path = &meta.path;
523 let named = |name: &str| path.is_ident(name);
524 if named("key") {
525 attrs.key = true;
526 } else if named("typed_key") {
527 attrs.typed_key = true;
528 } else if named("caption") {
529 attrs.caption = Some(meta.value()?.parse()?);
530 } else if named("default_property") {
531 attrs.default_property = true;
532 } else if named("read_only") {
533 attrs.read_only = true;
534 } else if named("no_list") {
535 attrs.no_list = true;
536 } else if named("no_detail") {
537 attrs.no_detail = true;
538 } else if named("lookup") {
539 attrs.lookup = true;
540 } else if named("money") {
541 set_kind(&mut attrs, path, KindSpec::Money)?;
542 } else if named("long") {
543 set_kind(&mut attrs, path, KindSpec::LongText)?;
544 } else if named("color") {
545 set_kind(&mut attrs, path, KindSpec::Color)?;
546 } else if named("date") {
547 set_kind(&mut attrs, path, KindSpec::Date)?;
548 } else if named("guid") {
549 set_kind(&mut attrs, path, KindSpec::Guid)?;
550 } else if named("tags") {
551 let options: LitStr = meta.value()?.parse()?;
552 set_kind(&mut attrs, path, KindSpec::Tags(split_names(&options)))?;
553 } else if named("decimals") {
554 let places: LitInt = meta.value()?.parse()?;
555 let places: u8 = places.base10_parse()?;
556 set_kind(&mut attrs, path, KindSpec::Decimal(places))?;
557 } else if named("reference") {
558 let target: LitStr = meta.value()?.parse()?;
559 set_kind(&mut attrs, path, KindSpec::Reference(target))?;
560 } else if named("collection") {
561 let target: LitStr = meta.value()?.parse()?;
562 set_kind(&mut attrs, path, KindSpec::Collection(target))?;
563 } else if named("calculated") {
564 attrs.calculated = Some(meta.value()?.parse()?);
565 } else if named("default") {
566 attrs.default = Some(meta.value()?.parse()?);
567 } else if named("required") {
568 attrs.required = true;
569 } else if named("unique") {
570 attrs.unique = true;
571 } else if named("min") {
572 attrs.min = Some(meta.value()?.parse()?);
573 } else if named("max") {
574 attrs.max = Some(meta.value()?.parse()?);
575 } else if named("matches") {
576 let pattern: LitStr = meta.value()?.parse()?;
577 regex_lite::Regex::new(&pattern.value())
578 .map_err(|e| Error::new(pattern.span(), format!("this pattern is not valid: {e}")))?;
579 attrs.matches = Some(pattern);
580 } else if named("message") {
581 attrs.message = Some(meta.value()?.parse()?);
582 } else if named("editor") {
583 attrs.editor = Some(meta.value()?.parse()?);
584 } else if named("placeholder") {
585 attrs.placeholder = Some(meta.value()?.parse()?);
586 } else if named("hint") {
587 attrs.hint = Some(meta.value()?.parse()?);
588 } else if named("accessible_name") {
589 attrs.accessible_name = Some(meta.value()?.parse()?);
590 } else if named("accessible_description") {
591 attrs.accessible_description = Some(meta.value()?.parse()?);
592 } else if named("lookup_filter") {
593 let text: LitStr = meta.value()?.parse()?;
594 Criteria::parse(&text.value()).map_err(|e| Error::new(text.span(), e.to_string()))?;
595 attrs.lookup_filter = Some(text);
596 } else if named("none_caption") {
597 attrs.none_caption = Some(meta.value()?.parse()?);
598 } else if named("back_reference") {
599 attrs.back_reference = Some(meta.value()?.parse()?);
600 } else if named("most_bytes") {
601 attrs.most_bytes = Some(meta.value()?.parse()?);
602 } else if named("wide") {
603 attrs.wide = true;
604 } else if named("autofocus") {
605 attrs.autofocus = true;
606 } else if named("non_cloneable") {
607 attrs.non_cloneable = true;
608 } else if named("secret") {
609 attrs.secret = true;
610 } else if named("aggregated") {
611 attrs.aggregated = true;
612 } else if named("group") {
613 attrs.group = Some(meta.value()?.parse()?);
614 } else if named("display_format") {
615 attrs.display_format = Some(meta.value()?.parse()?);
616 } else if named("edit_mask") {
617 attrs.edit_mask = Some(meta.value()?.parse()?);
618 } else if named("predefined") {
619 let text: LitStr = meta.value()?.parse()?;
620 attrs.predefined = Some(split_names(&text));
621 } else if named("criteria_of") {
622 attrs.criteria_of = Some(meta.value()?.parse()?);
623 } else {
624 return Err(meta.error(
625 "unknown setting: use key, typed_key, caption, default_property, read_only, no_list, \
626 no_detail, lookup, money, long, color, date, guid, tags, decimals, reference, collection, \
627 calculated, default, required, unique, min, max, matches, message, editor, placeholder, \
628 hint, accessible_name, accessible_description, lookup_filter, none_caption, back_reference, most_bytes, wide, autofocus, \
629 non_cloneable, secret, aggregated, group, display_format, edit_mask, predefined or criteria_of",
630 ));
631 }
632 Ok(())
633 })?;
634 }
635 if attrs.key && attrs.typed_key {
636 return Err(Error::new(field.span(), "a field is key or typed_key, not both"));
637 }
638 Ok(attrs)
639}
640
641pub fn check_expression(expression: &LitStr, names: &[String]) -> Result<()> {
643 let parsed = Criteria::parse(&expression.value())
644 .map_err(|e| Error::new(expression.span(), format!("this expression does not parse: {e}")))?;
645 for property in parsed.properties() {
646 if !names.contains(&property) {
647 return Err(Error::new(expression.span(), format!("this struct has no field `{property}`")));
648 }
649 }
650 Ok(())
651}
652
653fn field_rules(ident: &Ident, attrs: &FieldSettings) -> Vec<RuleSpec> {
654 let name = ident.to_string();
655 let mut kinds = Vec::new();
656 if attrs.required {
657 kinds.push(RuleKindSpec::Required(name.clone()));
658 }
659 if attrs.unique {
660 kinds.push(RuleKindSpec::Unique(name.clone()));
661 }
662 if attrs.min.is_some() || attrs.max.is_some() {
663 kinds.push(RuleKindSpec::Range {
664 field: name.clone(),
665 min: attrs.min.clone().map(Box::new),
666 max: attrs.max.clone().map(Box::new),
667 });
668 }
669 if let Some(pattern) = &attrs.matches {
670 kinds.push(RuleKindSpec::Matches(name, pattern.clone()));
671 }
672 kinds
673 .into_iter()
674 .map(|kind| RuleSpec {
675 kind,
676 message: attrs.message.clone(),
677 context: None,
678 target: None,
679 severity: SeveritySpec::Error,
680 })
681 .collect()
682}
683
684#[derive(Default)]
685struct RuleAttrs {
686 criteria: Option<LitStr>,
687 unique: Option<LitStr>,
688 message: Option<LitStr>,
689 context: Option<LitStr>,
690 target: Option<LitStr>,
691 warning: bool,
692 information: bool,
693}
694
695fn struct_rules(input: &DeriveInput, names: &[String]) -> Result<Vec<RuleSpec>> {
696 let mut rules = Vec::new();
697 for attr in input.attrs.iter().filter(|a| a.path().is_ident("rule")) {
698 let mut found = RuleAttrs::default();
699 attr.parse_nested_meta(|meta| {
700 if meta.path.is_ident("warning") {
701 found.warning = true;
702 return Ok(());
703 }
704 if meta.path.is_ident("information") {
705 found.information = true;
706 return Ok(());
707 }
708 let slot = if meta.path.is_ident("criteria") {
709 &mut found.criteria
710 } else if meta.path.is_ident("unique") {
711 &mut found.unique
712 } else if meta.path.is_ident("message") {
713 &mut found.message
714 } else if meta.path.is_ident("context") {
715 &mut found.context
716 } else if meta.path.is_ident("target") {
717 &mut found.target
718 } else {
719 return Err(meta.error(
720 "unknown setting: use criteria, unique, message, context, target, warning or information",
721 ));
722 };
723 *slot = Some(meta.value()?.parse()?);
724 Ok(())
725 })?;
726 let id = format!("rule{}", rules.len());
727 let kind = match (&found.criteria, &found.unique) {
728 (Some(expression), None) => {
729 check_expression(expression, names)?;
730 RuleKindSpec::Criteria(id, expression.clone())
731 }
732 (None, Some(fields)) => {
733 let fields = split_names(fields);
734 if let Some(missing) = fields.iter().find(|f| !names.contains(f)) {
735 return Err(Error::new(attr.span(), format!("this struct has no field `{missing}`")));
736 }
737 RuleKindSpec::UniqueTogether(id, fields)
738 }
739 _ => return Err(Error::new(attr.span(), "a rule says either criteria or unique")),
740 };
741 if let Some(expression) = &found.target {
742 check_expression(expression, names)?;
743 }
744 if found.warning && found.information {
745 return Err(Error::new(attr.span(), "a rule is a warning or information, not both"));
746 }
747 let severity = if found.warning {
748 SeveritySpec::Warning
749 } else if found.information {
750 SeveritySpec::Information
751 } else {
752 SeveritySpec::Error
753 };
754 rules.push(RuleSpec {
755 kind,
756 message: found.message,
757 context: found.context,
758 target: found.target,
759 severity,
760 });
761 }
762 Ok(rules)
763}
764
765fn tint(text: &LitStr) -> Result<TintSpec> {
766 Ok(match text.value().as_str() {
767 "danger" => TintSpec::Danger,
768 "warning" => TintSpec::Warning,
769 "success" => TintSpec::Success,
770 "accent" => TintSpec::Accent,
771 "muted" => TintSpec::Muted,
772 _ => return Err(Error::new(text.span(), "use danger, warning, success, accent or muted")),
773 })
774}
775
776fn struct_appearance(input: &DeriveInput, names: &[String]) -> Result<Vec<AppearanceSpec>> {
777 let mut rules = Vec::new();
778 for attr in input.attrs.iter().filter(|a| a.path().is_ident("appearance")) {
779 let mut targets: Option<LitStr> = None;
780 let mut pieces = Vec::new();
781 attr.parse_nested_meta(|meta| {
782 let named = |name: &str| meta.path.is_ident(name);
783 if named("names") {
784 targets = Some(meta.value()?.parse()?);
785 } else if named("criteria") {
786 let expression: LitStr = meta.value()?.parse()?;
787 check_expression(&expression, names)?;
788 pieces.push(AppearancePiece::Criteria(expression));
789 } else if named("font") {
790 pieces.push(AppearancePiece::Font(tint(&meta.value()?.parse()?)?));
791 } else if named("back") {
792 pieces.push(AppearancePiece::Back(tint(&meta.value()?.parse()?)?));
793 } else if named("bold") {
794 pieces.push(AppearancePiece::Bold);
795 } else if named("enabled") {
796 pieces.push(AppearancePiece::Enabled(meta.value()?.parse()?));
797 } else if named("visible") {
798 pieces.push(AppearancePiece::Visible(meta.value()?.parse()?));
799 } else if named("priority") {
800 pieces.push(AppearancePiece::Priority(meta.value()?.parse()?));
801 } else if named("action") {
802 pieces.push(AppearancePiece::Action);
803 } else if named("layout") {
804 pieces.push(AppearancePiece::Layout);
805 } else if named("context") {
806 let context: LitStr = meta.value()?.parse()?;
807 pieces.push(AppearancePiece::Context(match context.value().as_str() {
808 "list" => ContextSpec::List,
809 "detail" => ContextSpec::Detail,
810 _ => ContextSpec::View(context),
811 }));
812 } else {
813 return Err(meta.error(
814 "unknown setting: use names, criteria, font, back, bold, enabled, visible, priority, \
815 context, action or layout",
816 ));
817 }
818 Ok(())
819 })?;
820 let Some(targets) = targets else {
821 return Err(Error::new(attr.span(), "an appearance rule needs names = \"...\""));
822 };
823 let id = format!("appearance{}", rules.len());
824 rules.push(AppearanceSpec { id, targets: split_names(&targets), pieces });
825 }
826 Ok(rules)
827}
828
829fn struct_actions(input: &DeriveInput, names: &[String]) -> Result<Vec<ActionSpec>> {
830 let mut found = Vec::new();
831 for attr in input.attrs.iter().filter(|a| a.path().is_ident("action")) {
832 let (mut caption, mut id, mut selection, mut enabled, mut set, mut confirm) =
833 (None::<LitStr>, None, None, None, None::<LitStr>, None);
834 attr.parse_nested_meta(|meta| {
835 let named = |name: &str| meta.path.is_ident(name);
836 if named("caption") {
837 caption = Some(meta.value()?.parse()?);
838 } else if named("id") {
839 id = Some(meta.value()?.parse()?);
840 } else if named("selection") {
841 let text: LitStr = meta.value()?.parse()?;
842 selection = Some(match text.value().as_str() {
843 "one" => SelectionSpec::One,
844 "many" => SelectionSpec::Many,
845 "any" => SelectionSpec::Any,
846 _ => return Err(Error::new(text.span(), "use one, many or any")),
847 });
848 } else if named("enabled") {
849 let expression: LitStr = meta.value()?.parse()?;
850 check_expression(&expression, names)?;
851 enabled = Some(expression);
852 } else if named("set") {
853 let text: LitStr = meta.value()?.parse()?;
854 check_assignments(&text, names)?;
855 set = Some(text);
856 } else if named("confirm") {
857 confirm = Some(meta.value()?.parse()?);
858 } else {
859 return Err(
860 meta.error("unknown setting: use caption, id, selection, enabled, set or confirm")
861 );
862 }
863 Ok(())
864 })?;
865 let (Some(caption), Some(set)) = (caption, set) else {
866 return Err(Error::new(attr.span(), "an action needs caption = \"...\" and set = \"...\""));
867 };
868 found.push(ActionSpec { caption, id, selection, enabled, set, confirm });
869 }
870 Ok(found)
871}
872
873pub fn check_assignments(text: &LitStr, names: &[String]) -> Result<()> {
876 let found = Criteria::assignments(&text.value())
877 .map_err(|e| Error::new(text.span(), format!("this does not parse: {e}")))?;
878 if found.is_empty() {
879 return Err(Error::new(text.span(), "this sets nothing: use `field = expression; ...`"));
880 }
881 for (field, expression) in &found {
882 let unknown = std::iter::once(field.clone()).chain(expression.properties());
883 if let Some(unknown) = unknown.into_iter().find(|name| !names.contains(name)) {
884 return Err(Error::new(text.span(), format!("this struct has no field `{unknown}`")));
885 }
886 }
887 Ok(())
888}
889
890fn struct_choices(input: &DeriveInput, names: &[String]) -> Result<Vec<ChoiceSpec>> {
891 let mut found = Vec::new();
892 for attr in &input.attrs {
893 let is_variant = attr.path().is_ident("variant");
894 if !is_variant && !attr.path().is_ident("list_filter") {
895 continue;
896 }
897 let (mut id, mut caption, mut filter, mut columns) =
898 (None::<LitStr>, None::<LitStr>, None::<LitStr>, None::<LitStr>);
899 attr.parse_nested_meta(|meta| {
900 let slot = if meta.path.is_ident("id") {
901 &mut id
902 } else if meta.path.is_ident("caption") {
903 &mut caption
904 } else if meta.path.is_ident("filter") {
905 &mut filter
906 } else if is_variant && meta.path.is_ident("columns") {
907 &mut columns
908 } else {
909 return Err(meta.error("unknown setting: use id, caption, filter or, on a variant, columns"));
910 };
911 *slot = Some(meta.value()?.parse()?);
912 Ok(())
913 })?;
914 let (Some(id), Some(caption)) = (id, caption) else {
915 return Err(Error::new(attr.span(), "this needs an id and a caption"));
916 };
917 if let Some(text) = &filter {
918 let parsed =
919 Criteria::parse(&text.value()).map_err(|e| Error::new(text.span(), e.to_string()))?;
920 if let Some(unknown) = parsed.properties().iter().find(|name| !names.contains(name)) {
921 return Err(Error::new(text.span(), format!("there is no field {unknown}")));
922 }
923 }
924 let shown: Vec<String> = columns
925 .iter()
926 .flat_map(|text| text.value().split(',').map(|c| c.trim().to_owned()).collect::<Vec<_>>())
927 .collect();
928 if let (Some(text), Some(unknown)) = (&columns, shown.iter().find(|name| !names.contains(name))) {
929 return Err(Error::new(text.span(), format!("there is no field {unknown}")));
930 }
931 found.push(if is_variant {
932 ChoiceSpec::Variant { id, caption, filter, columns: shown }
933 } else {
934 ChoiceSpec::ListFilter { id, caption, filter }
935 });
936 }
937 Ok(found)
938}
939
940pub fn parse_object(input: &DeriveInput) -> Result<ObjectSpec> {
943 let name = &input.ident;
944 if !input.generics.params.is_empty() {
945 return Err(Error::new(input.generics.span(), "a business object cannot be generic"));
946 }
947 let Data::Struct(data) = &input.data else {
948 return Err(Error::new(name.span(), "a business object is a struct with named fields"));
949 };
950 let Fields::Named(named) = &data.fields else {
951 return Err(Error::new(name.span(), "a business object is a struct with named fields"));
952 };
953 let names: Vec<String> =
954 named.named.iter().filter_map(|f| f.ident.as_ref().map(Ident::to_string)).collect();
955 let (mut settings, focus_order) = object_settings(input)?;
956 let mut fields = Vec::new();
957 let mut rules = Vec::new();
958 let mut keys = 0;
959 for field in &named.named {
960 let Some(ident) = &field.ident else { continue };
961 let attrs = field_settings(field)?;
962 if attrs.key || attrs.typed_key {
963 keys += 1;
964 if keys > 1 {
965 return Err(Error::new(
966 ident.span(),
967 "only one field can be the key, and another one already is",
968 ));
969 }
970 }
971 if let Some(expression) = &attrs.calculated {
972 check_expression(expression, &names)?;
973 }
974 rules.extend(field_rules(ident, &attrs));
975 fields.push(FieldSpec { ident: ident.clone(), ty: field.ty.clone(), settings: attrs });
976 }
977 if keys == 0 {
978 return Err(Error::new(
979 name.span(),
980 "a business object needs a key: mark a field with #[field(key)] or #[field(typed_key)]",
981 ));
982 }
983 rules.extend(struct_rules(input, &names)?);
984 let appearance = struct_appearance(input, &names)?;
985 if let Some(order) = &focus_order {
986 let order_fields = split_names(order);
987 if let Some(missing) = order_fields.iter().find(|field| !names.contains(field)) {
988 return Err(Error::new(order.span(), format!("this struct has no field `{missing}`")));
989 }
990 settings.focus_order = order_fields;
991 }
992 let choices = struct_choices(input, &names)?;
993 let actions = struct_actions(input, &names)?;
994 Ok(ObjectSpec { name: name.clone(), settings, fields, rules, appearance, choices, actions })
995}
996
997pub fn parse_enum(input: &DeriveInput) -> Result<EnumSpec> {
999 let name = &input.ident;
1000 let Data::Enum(data) = &input.data else {
1001 return Err(Error::new(name.span(), "a business enum is an enum with no data"));
1002 };
1003 if data.variants.is_empty() {
1004 return Err(Error::new(name.span(), "a business enum needs at least one option"));
1005 }
1006 let mut options = Vec::new();
1007 for variant in &data.variants {
1008 if !matches!(variant.fields, Fields::Unit) {
1009 return Err(Error::new(variant.ident.span(), "an option of a business enum cannot hold data"));
1010 }
1011 let ident = &variant.ident;
1012 let (mut caption, mut code) = (ident.to_string(), ident.to_string());
1013 for attr in variant.attrs.iter().filter(|a| a.path().is_ident("option")) {
1014 attr.parse_nested_meta(|meta| {
1015 let value: LitStr = if meta.path.is_ident("caption") || meta.path.is_ident("code") {
1016 meta.value()?.parse()?
1017 } else {
1018 return Err(meta.error("unknown setting: use caption or code"));
1019 };
1020 if meta.path.is_ident("caption") {
1021 caption = value.value();
1022 } else {
1023 code = value.value();
1024 }
1025 Ok(())
1026 })?;
1027 }
1028 options.push(OptionSpec { ident: ident.clone(), code, caption });
1029 }
1030 Ok(EnumSpec { name: name.clone(), options })
1031}
1032
1033#[derive(Clone, Debug, PartialEq, Eq)]
1035pub struct SpecError {
1036 pub line: usize,
1038 pub column: usize,
1040 pub length: usize,
1042 pub message: String,
1044}
1045
1046impl SpecError {
1047 pub fn at(span: Span, message: impl Into<String>) -> Self {
1049 let (start, end) = (span.start(), span.end());
1050 let length = if end.line == start.line { end.column.saturating_sub(start.column).max(1) } else { 1 };
1051 Self { line: start.line, column: start.column + 1, length, message: message.into() }
1052 }
1053
1054 pub fn all(error: &Error) -> Vec<Self> {
1056 error.clone().into_iter().map(|e| Self::at(e.span(), e.to_string())).collect()
1057 }
1058}
1059
1060impl std::fmt::Display for SpecError {
1061 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1062 write!(f, "{}:{}: {}", self.line, self.column, self.message)
1063 }
1064}
1065
1066#[derive(Clone, Default)]
1069pub struct FileSpec {
1070 pub objects: Vec<ObjectSpec>,
1072 pub enums: Vec<EnumSpec>,
1074 pub items: Vec<syn::Item>,
1076}
1077
1078pub fn derives(attrs: &[syn::Attribute], name: &str) -> bool {
1080 attrs.iter().filter(|a| a.path().is_ident("derive")).any(|attr| {
1081 let mut found = false;
1082 let _ = attr.parse_nested_meta(|meta| {
1083 if meta.path.segments.last().is_some_and(|s| s.ident == name) {
1084 found = true;
1085 }
1086 Ok(())
1087 });
1088 found
1089 })
1090}
1091
1092pub fn parse_file(text: &str) -> std::result::Result<FileSpec, Vec<SpecError>> {
1095 let file = syn::parse_file(text).map_err(|e| SpecError::all(&e))?;
1096 let mut spec = FileSpec::default();
1097 let mut errors = Vec::new();
1098 for item in file.items {
1099 match item {
1100 syn::Item::Struct(item) if derives(&item.attrs, "BusinessObject") => {
1101 match parse_object(&DeriveInput::from(item)) {
1102 Ok(object) => spec.objects.push(object),
1103 Err(e) => errors.extend(SpecError::all(&e)),
1104 }
1105 }
1106 syn::Item::Enum(item) if derives(&item.attrs, "BusinessEnum") => {
1107 match parse_enum(&DeriveInput::from(item)) {
1108 Ok(options) => spec.enums.push(options),
1109 Err(e) => errors.extend(SpecError::all(&e)),
1110 }
1111 }
1112 other => spec.items.push(other),
1113 }
1114 }
1115 if errors.is_empty() { Ok(spec) } else { Err(errors) }
1116}