Skip to main content

rorm_declaration/
lints.rs

1//! Some common lints whose code can be shared between rorm-macro and rorm-cli.
2
3use crate::imr::Annotation;
4
5/// Simple struct storing whether a specific annotation is set on a given field or not.
6#[derive(Copy, Clone, Default, Debug)]
7pub struct Annotations {
8    /// Does the field have the [Annotation::AutoCreateTime]?
9    pub auto_create_time: bool,
10
11    /// Does the field have the [Annotation::AutoUpdateTime]?
12    pub auto_update_time: bool,
13
14    /// Does the field have the [Annotation::AutoIncrement]?
15    pub auto_increment: bool,
16
17    /// Does the field have the [Annotation::Choices]?
18    pub choices: bool,
19
20    /// Does the field have the [Annotation::DefaultValue]?
21    pub default: bool,
22
23    /// Does the field have an [Annotation::Index] *without* a name?
24    ///
25    /// A named index may span several columns, which makes it valid on columns
26    /// where a single column index wouldn't be, most notably a primary key.
27    /// Since this struct can't express which columns an index spans,
28    /// named indexes are not tracked here at all.
29    pub index: bool,
30
31    /// Does the field have the [Annotation::MaxLength]?
32    pub max_length: bool,
33
34    /// Does the field have the [Annotation::NotNull]?
35    pub not_null: bool,
36
37    /// Does the field have the [Annotation::PrimaryKey]?
38    pub primary_key: bool,
39
40    /// Does the field have the [Annotation::Unique]?
41    pub unique: bool,
42
43    /// Does the field have the [Annotation::ForeignKey]?
44    pub foreign_key: bool,
45}
46
47impl Annotations {
48    /// Check whether this set of annotations is valid.
49    ///
50    /// Returns a non-empty error message, when it is not.
51    // Disable auto-format to make the following match compacter and more readable.
52    #[rustfmt::skip]
53    pub const fn check(self) -> Result<(), &'static str> {
54        // Alias to reduce line length and noise
55        use Annotations as A;
56
57        let msg = match self {
58            A { auto_create_time: true, auto_increment: true, .. } => "AutoCreateTime and AutoIncrement are mutually exclusive",
59            A { auto_create_time: true, choices: true, .. } => "AutoCreateTime and Choices are mutually exclusive",
60            A { auto_create_time: true, default: true, .. } => "AutoCreateTime and DefaultValue are mutually exclusive",
61            A { auto_create_time: true, max_length: true, .. } => "AutoCreateTime and MaxLength are mutually exclusive",
62            A { auto_create_time: true, primary_key: true, .. } => "AutoCreateTime and PrimaryKey are mutually exclusive",
63            A { auto_create_time: true, unique: true, .. } => "AutoCreateTime and Unique are mutually exclusive",
64            A { auto_update_time: true, auto_increment: true, .. } => "AutoUpdateTime and AutoIncrement are mutually exclusive",
65            A { auto_update_time: true, choices: true, .. } => "AutoUpdateTime and Choices are mutually exclusive",
66            A { auto_update_time: true, max_length: true, .. } => "AutoUpdateTime and MaxLength are mutually exclusive",
67            A { auto_update_time: true, primary_key: true, .. } => "AutoUpdateTime and PrimaryKey are mutually exclusive",
68            A { auto_update_time: true, unique: true, .. } => "AutoUpdateTime and Unique are mutually exclusive",
69            A { auto_increment: true, choices: true, .. } => "AutoIncrement and Choices are mutually exclusive",
70            A { auto_increment: true, max_length: true, .. } => "AutoIncrement and MaxLength are mutually exclusive",
71            A { choices: true, max_length: true, .. } => "Choices and MaxLength are mutually exclusive",
72            A { choices: true, primary_key: true, .. } => "Choices and PrimaryKey are mutually exclusive",
73            A { choices: true, unique: true, .. } => "Choices and Unique are mutually exclusive",
74            A { default: true, auto_update_time: true, .. } => "DefaultValue and AutoUpdateTime are mutually exclusive",
75            A { default: true, auto_increment: true, .. } => "DefaultValue and AutoIncrement are mutually exclusive",
76            A { default: true, primary_key: true, .. } => "DefaultValue and PrimaryKey are mutually exclusive",
77            A { default: true, unique: true, .. } => "DefaultValue and Unique are mutually exclusive",
78            A { index: true, primary_key: true, .. } => "An unnamed Index and PrimaryKey are mutually exclusive; name the index to span the column as part of a composite index",
79            A { not_null: true, primary_key: true, .. } => "NotNull and PrimaryKey are mutually exclusive",
80
81            A { auto_increment: true, primary_key: false, .. } => "AutoIncrement requires PrimaryKey",
82
83            A { auto_update_time: true, not_null: true, auto_create_time: false, default: false, ..} => "AutoUpdateTime in combination with NotNull requires ether DefaultValue or AutoCreateTime",
84
85            _ => "",
86        };
87
88        // Create Result based on error message length to avoid using Err() in the match expression.
89        if !msg.is_empty() {
90            Err(msg)
91        } else {
92            Ok(())
93        }
94    }
95}
96
97impl From<&[Annotation]> for Annotations {
98    fn from(annotations: &[Annotation]) -> Self {
99        let mut result = Annotations::default();
100        for annotation in annotations {
101            match annotation {
102                Annotation::AutoCreateTime => result.auto_create_time = true,
103                Annotation::AutoUpdateTime => result.auto_update_time = true,
104                Annotation::AutoIncrement => result.auto_increment = true,
105                Annotation::Choices(_) => result.choices = true,
106                Annotation::DefaultValue(_) => result.default = true,
107                // A named index may span several columns and is therefore not tracked
108                Annotation::Index(index) => result.index |= index.is_none(),
109                Annotation::MaxLength(_) => result.max_length = true,
110                Annotation::NotNull => result.not_null = true,
111                Annotation::PrimaryKey => result.primary_key = true,
112                Annotation::Unique => result.unique = true,
113                Annotation::ForeignKey(_) => result.foreign_key = true,
114            }
115        }
116        result
117    }
118}
119
120#[cfg(test)]
121mod test_index_on_primary_key {
122    use crate::imr::{Annotation, IndexValue};
123    use crate::lints::Annotations;
124
125    fn check(annotations: &[Annotation]) -> Result<(), &'static str> {
126        Annotations::from(annotations).check()
127    }
128
129    #[test]
130    fn unnamed_index_on_primary_key_is_redundant() {
131        assert!(check(&[Annotation::PrimaryKey, Annotation::Index(None)]).is_err());
132    }
133
134    #[test]
135    fn named_index_on_primary_key_is_allowed() {
136        // A primary key is a perfectly valid column of a composite index,
137        // e.g. "(collection, uuid)" for filtering by a foreign key
138        // and sorting by the primary key.
139        assert!(check(&[
140            Annotation::PrimaryKey,
141            Annotation::Index(Some(IndexValue {
142                name: "collection_uuid".to_string(),
143                priority: Some(2),
144            })),
145        ])
146        .is_ok());
147    }
148
149    #[test]
150    fn an_unnamed_index_is_still_caught_next_to_a_named_one() {
151        assert!(check(&[
152            Annotation::PrimaryKey,
153            Annotation::Index(Some(IndexValue {
154                name: "collection_uuid".to_string(),
155                priority: None,
156            })),
157            Annotation::Index(None),
158        ])
159        .is_err());
160    }
161}