Skip to main content

ravenrs_app_spec/
lib.rs

1//! What `#[derive(BusinessObject)]` and `#[derive(BusinessEnum)]` read from a struct or an
2//! enum, as plain data: the derive turns it into code, and `ravenrs-app` (feature `source`)
3//! turns it into entities while a program runs, so both accept and refuse the same things,
4//! with the same words.
5//!
6//! ```
7//! let file = ravenrs_app_spec::parse_file(
8//!     r#"
9//!     #[derive(BusinessObject)]
10//!     #[object(caption = "Banks")]
11//!     struct Bank {
12//!         #[field(typed_key, caption = "Code")]
13//!         code: String,
14//!     }
15//!     "#,
16//! )
17//! .map_err(|errors| errors[0].message.clone())?;
18//! assert_eq!(file.objects[0].name.to_string(), "Bank");
19//! assert!(file.objects[0].fields[0].settings.typed_key);
20//! # Ok::<(), String>(())
21//! ```
22
23use 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/// `#[object(...)]` on a struct.
57#[derive(Clone, Default)]
58pub struct ObjectSettings {
59    /// `caption = "..."`.
60    pub caption: Option<LitStr>,
61    /// `navigation = "..."`.
62    pub navigation: Option<LitStr>,
63    /// `icon = "..."`.
64    pub icon: Option<LitStr>,
65    /// `list_criteria = "..."`, checked to parse.
66    pub list_criteria: Option<LitStr>,
67    /// `form_columns = 2`.
68    pub form_columns: Option<LitInt>,
69    /// `cloneable`.
70    pub cloneable: bool,
71    /// `transient`.
72    pub transient: bool,
73    /// `checks_on_leave`.
74    pub checks_on_leave: bool,
75    /// `list_editable`.
76    pub list_editable: bool,
77    /// `data_access = "client" | "server" | "data_view"`.
78    pub data_access: Option<DataAccessSpec>,
79    /// `form_actions = "..."`, as many as given.
80    pub form_actions: Vec<LitStr>,
81    /// `focus_order = "a,b"`, as field names, checked against the struct.
82    pub focus_order: Vec<String>,
83}
84
85/// Where a list's records are read.
86#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub enum DataAccessSpec {
88    /// All in memory.
89    Client,
90    /// A page at a time.
91    Server,
92    /// Only the shown values.
93    DataView,
94}
95
96/// What a field holds when its attribute says so, instead of its type.
97#[derive(Clone)]
98pub enum KindSpec {
99    /// `money`.
100    Money,
101    /// `long`.
102    LongText,
103    /// `color`.
104    Color,
105    /// `date`.
106    Date,
107    /// `guid`.
108    Guid,
109    /// `tags = "a,b"`.
110    Tags(Vec<String>),
111    /// `decimals = 2`.
112    Decimal(u8),
113    /// `reference = "Entity"`.
114    Reference(LitStr),
115    /// `collection = "Entity"`.
116    Collection(LitStr),
117}
118
119/// `#[field(...)]` on a field.
120#[derive(Clone, Default)]
121pub struct FieldSettings {
122    /// `key` in the attribute.
123    pub key: bool,
124    /// `typed_key` in the attribute.
125    pub typed_key: bool,
126    /// What people see.
127    pub caption: Option<LitStr>,
128    /// `default_property` in the attribute.
129    pub default_property: bool,
130    /// `read_only` in the attribute.
131    pub read_only: bool,
132    /// `no_list` in the attribute.
133    pub no_list: bool,
134    /// `no_detail` in the attribute.
135    pub no_detail: bool,
136    /// `lookup` in the attribute.
137    pub lookup: bool,
138    /// What it holds, when the attribute says.
139    pub kind: Option<KindSpec>,
140    /// `calculated = "..."`, checked against the struct's fields.
141    pub calculated: Option<LitStr>,
142    /// `default = ...`, any expression; a running program reads only literals and enum options.
143    pub default: Option<syn::Expr>,
144    /// `required` in the attribute.
145    pub required: bool,
146    /// `unique` in the attribute.
147    pub unique: bool,
148    /// `min` in the attribute.
149    pub min: Option<syn::Expr>,
150    /// `max` in the attribute.
151    pub max: Option<syn::Expr>,
152    /// `matches = "..."`, checked to be a valid pattern.
153    pub matches: Option<LitStr>,
154    /// What to say instead of the usual words.
155    pub message: Option<LitStr>,
156    /// `editor` in the attribute.
157    pub editor: Option<LitStr>,
158    /// `placeholder` in the attribute.
159    pub placeholder: Option<LitStr>,
160    /// `hint` in the attribute.
161    pub hint: Option<LitStr>,
162    /// `accessible_name` in the attribute.
163    pub accessible_name: Option<LitStr>,
164    /// `accessible_description` in the attribute.
165    pub accessible_description: Option<LitStr>,
166    /// `lookup_filter` in the attribute.
167    pub lookup_filter: Option<LitStr>,
168    /// `none_caption` in the attribute.
169    pub none_caption: Option<LitStr>,
170    /// `back_reference` in the attribute.
171    pub back_reference: Option<LitStr>,
172    /// `most_bytes` in the attribute.
173    pub most_bytes: Option<LitInt>,
174    /// `wide` in the attribute.
175    pub wide: bool,
176    /// `autofocus` in the attribute.
177    pub autofocus: bool,
178    /// `non_cloneable` in the attribute.
179    pub non_cloneable: bool,
180    /// `secret` in the attribute.
181    pub secret: bool,
182    /// `aggregated` in the attribute.
183    pub aggregated: bool,
184    /// `group` in the attribute.
185    pub group: Option<LitStr>,
186    /// `display_format` in the attribute.
187    pub display_format: Option<LitStr>,
188    /// `edit_mask` in the attribute.
189    pub edit_mask: Option<LitStr>,
190    /// `predefined = "a,b"`, as values.
191    pub predefined: Option<Vec<String>>,
192    /// `criteria_of` in the attribute.
193    pub criteria_of: Option<LitStr>,
194}
195
196/// A field of a business object.
197#[derive(Clone)]
198pub struct FieldSpec {
199    /// Its name in the code.
200    pub ident: Ident,
201    /// Its Rust type.
202    pub ty: syn::Type,
203    /// What its attribute says.
204    pub settings: FieldSettings,
205}
206
207/// What a rule checks.
208#[derive(Clone)]
209pub enum RuleKindSpec {
210    /// `#[field(required)]`.
211    Required(String),
212    /// `#[field(unique)]`.
213    Unique(String),
214    /// `#[field(min = .., max = ..)]`.
215    Range {
216        /// The field.
217        field: String,
218        /// The least value, if any.
219        min: Option<Box<syn::Expr>>,
220        /// The greatest value, if any.
221        max: Option<Box<syn::Expr>>,
222    },
223    /// `#[field(matches = "...")]`.
224    Matches(String, LitStr),
225    /// `#[rule(criteria = "...")]`, with its id.
226    Criteria(String, LitStr),
227    /// `#[rule(unique = "a,b")]`, with its id.
228    UniqueTogether(String, Vec<String>),
229}
230
231/// How much a broken rule stops.
232#[derive(Clone, Copy, Debug, PartialEq, Eq)]
233pub enum SeveritySpec {
234    /// It stops saving.
235    Error,
236    /// `warning`: people can go on.
237    Warning,
238    /// `information`: only a note.
239    Information,
240}
241
242/// A rule of a field or of a struct.
243#[derive(Clone)]
244pub struct RuleSpec {
245    /// What it checks.
246    pub kind: RuleKindSpec,
247    /// What to say instead of the usual words.
248    pub message: Option<LitStr>,
249    /// Where it applies.
250    pub context: Option<LitStr>,
251    /// Which records it checks, as an expression.
252    pub target: Option<LitStr>,
253    /// How much it stops.
254    pub severity: SeveritySpec,
255}
256
257/// A color of an appearance rule.
258#[derive(Clone, Copy, Debug, PartialEq, Eq)]
259pub enum TintSpec {
260    /// `Danger`.
261    Danger,
262    /// `Warning`.
263    Warning,
264    /// `Success`.
265    Success,
266    /// `Accent`.
267    Accent,
268    /// `Muted`.
269    Muted,
270}
271
272/// Where an appearance rule applies.
273#[derive(Clone)]
274pub enum ContextSpec {
275    /// `List`.
276    List,
277    /// `Detail`.
278    Detail,
279    /// `View`.
280    View(LitStr),
281}
282
283/// One setting of `#[appearance(...)]`, in the order written.
284#[derive(Clone)]
285pub enum AppearancePiece {
286    /// `Criteria`.
287    Criteria(LitStr),
288    /// `Font`.
289    Font(TintSpec),
290    /// `Back`.
291    Back(TintSpec),
292    /// `Bold`.
293    Bold,
294    /// `Enabled`.
295    Enabled(LitBool),
296    /// `Visible`.
297    Visible(LitBool),
298    /// `Priority`.
299    Priority(LitInt),
300    /// `Action`.
301    Action,
302    /// `Layout`.
303    Layout,
304    /// `Context`.
305    Context(ContextSpec),
306}
307
308/// `#[appearance(...)]` on a struct.
309#[derive(Clone)]
310pub struct AppearanceSpec {
311    /// Its id.
312    pub id: String,
313    /// The names it applies to.
314    pub targets: Vec<String>,
315    /// Its settings, in the order written.
316    pub pieces: Vec<AppearancePiece>,
317}
318
319/// `#[variant(...)]` or `#[list_filter(...)]` on a struct.
320#[derive(Clone)]
321pub enum ChoiceSpec {
322    /// `Variant`.
323    Variant {
324        /// Its id.
325        id: LitStr,
326        /// What people see.
327        caption: LitStr,
328        /// The records it shows, as an expression.
329        filter: Option<LitStr>,
330        /// The columns it shows.
331        columns: Vec<String>,
332    },
333    /// `ListFilter`.
334    ListFilter {
335        /// Its id.
336        id: LitStr,
337        /// What people see.
338        caption: LitStr,
339        /// The records it keeps, as an expression.
340        filter: Option<LitStr>,
341    },
342}
343
344/// A struct with `#[derive(BusinessObject)]`.
345#[derive(Clone)]
346pub struct ObjectSpec {
347    /// Its name in the code.
348    pub name: Ident,
349    /// What its attribute says.
350    pub settings: ObjectSettings,
351    /// Its fields, in order.
352    pub fields: Vec<FieldSpec>,
353    /// The rules of the fields, in their order, then those of the struct.
354    pub rules: Vec<RuleSpec>,
355    /// Its appearance rules.
356    pub appearance: Vec<AppearanceSpec>,
357    /// Its view variants and list filters.
358    pub choices: Vec<ChoiceSpec>,
359    /// Its actions, from `#[action(...)]`.
360    pub actions: Vec<ActionSpec>,
361}
362
363/// How many records an action takes.
364#[derive(Clone, Copy, Debug, PartialEq, Eq)]
365pub enum SelectionSpec {
366    /// `selection = "one"`.
367    One,
368    /// `selection = "many"`, one or more.
369    Many,
370    /// `selection = "any"`, none or any number.
371    Any,
372}
373
374/// `#[action(...)]` on a struct: an action that sets fields of the records selected.
375#[derive(Clone)]
376pub struct ActionSpec {
377    /// `caption = "..."`.
378    pub caption: LitStr,
379    /// `id = "..."`.
380    pub id: Option<LitStr>,
381    /// `selection = "one" | "many" | "any"`.
382    pub selection: Option<SelectionSpec>,
383    /// `enabled = "..."`, checked to parse and to name only fields of the struct.
384    pub enabled: Option<LitStr>,
385    /// `set = "field = expression; ..."`, checked the same way.
386    pub set: LitStr,
387    /// `confirm = "..."`.
388    pub confirm: Option<LitStr>,
389}
390
391/// An option of a business enum.
392#[derive(Clone)]
393pub struct OptionSpec {
394    /// Its name in the code.
395    pub ident: Ident,
396    /// What a record holds for it.
397    pub code: String,
398    /// What people see.
399    pub caption: String,
400}
401
402/// An enum with `#[derive(BusinessEnum)]`.
403#[derive(Clone)]
404pub struct EnumSpec {
405    /// Its name in the code.
406    pub name: Ident,
407    /// Its options, the first one being what a new record starts with.
408    pub options: Vec<OptionSpec>,
409}
410
411/// What a field's Rust type holds, by the `FieldValue` implementations of `ravenrs-app`.
412#[derive(Clone, Debug, PartialEq, Eq)]
413pub enum TypeKind {
414    /// `String`.
415    Text,
416    /// `bool`.
417    Boolean,
418    /// `i32`, `i64`, `u32`, `u64`, `usize`.
419    Integer,
420    /// `f64`, with two decimals.
421    Decimal,
422    /// `DateTime`.
423    DateTime,
424    /// Any other name: a business enum, if one has it.
425    Named(String),
426}
427
428/// What a field's type holds, looking through `Option`. `None` for types no field can have,
429/// such as tuples or references.
430pub 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
455/// Names separated by commas, trimmed, without empty ones.
456pub 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
517/// `#[field(...)]` on one field.
518pub 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
641/// Checks that an expression parses and names only fields of the struct.
642pub 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
873/// Checks `field = expression; ...`: it parses, sets something, and names only fields of the
874/// struct.
875pub 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
940/// Reads a struct with `#[derive(BusinessObject)]`, checking what the derive checks, in the
941/// same order.
942pub 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
997/// Reads an enum with `#[derive(BusinessEnum)]`.
998pub 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/// Something wrong at a place of a source text.
1034#[derive(Clone, Debug, PartialEq, Eq)]
1035pub struct SpecError {
1036    /// The line, from 1.
1037    pub line: usize,
1038    /// The column of the first character, from 1.
1039    pub column: usize,
1040    /// How many characters it covers, at least 1.
1041    pub length: usize,
1042    /// What is wrong.
1043    pub message: String,
1044}
1045
1046impl SpecError {
1047    /// Where a span is, with a message.
1048    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    /// Every error a `syn::Error` holds.
1055    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/// The business objects and enums of a source file, in the order written, and the items it
1067/// leaves for others to read, such as functions.
1068#[derive(Clone, Default)]
1069pub struct FileSpec {
1070    /// The structs with `#[derive(BusinessObject)]`.
1071    pub objects: Vec<ObjectSpec>,
1072    /// The enums with `#[derive(BusinessEnum)]`.
1073    pub enums: Vec<EnumSpec>,
1074    /// Everything else in the file.
1075    pub items: Vec<syn::Item>,
1076}
1077
1078/// Whether an item derives `name`, as `#[derive(name)]` or `#[derive(path::name)]`.
1079pub 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
1092/// Reads a whole Rust file: the structs that derive `BusinessObject` and the enums that derive
1093/// `BusinessEnum`. Every problem found is returned, each with its place.
1094pub 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}