Skip to main content

siderite_cli/check/
mod.rs

1//! `check`: validate the app, its models and its configuration without
2//! starting a server or opening a database.
3//!
4//! [`check`] returns every [`CheckIssue`] it finds. The checks, by id prefix:
5//!
6//! | Prefix | Checks |
7//! |---|---|
8//! | `config` | a `default` database exists when models are managed; database URLs parse and name a known scheme |
9//! | `models` | duplicate tables, models and columns; a primary key; foreign-key and many-to-many targets are registered models |
10//! | `migrations` | migration files load; the dependency graph is sound; model changes not yet in a migration |
11//! | `openapi` | the OpenAPI document generates (duplicate operations and ids) |
12//! | `backend` | the models fit the capabilities of the `default` database's backend |
13//!
14//! Issue messages never contain database URLs.
15
16mod backend;
17pub(crate) mod config;
18mod migrations;
19mod models;
20mod openapi;
21
22use crate::settings::CliSettings;
23use siderite_core::App;
24use siderite_orm::ModelMeta;
25use std::fmt;
26use std::path::Path;
27
28/// How serious a [`CheckIssue`] is.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
30pub enum CheckLevel {
31    /// Worth fixing; the app still runs.
32    Warning,
33    /// The app or its migrations will not work.
34    Error,
35}
36
37impl fmt::Display for CheckLevel {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        f.write_str(match self {
40            Self::Warning => "warning",
41            Self::Error => "error",
42        })
43    }
44}
45
46/// One problem found by [`check`].
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct CheckIssue {
49    /// Severity.
50    pub level: CheckLevel,
51    /// Stable identifier such as `models.E001` (`E` errors, `W` warnings).
52    pub id: &'static str,
53    /// Human-readable description. Never contains a database URL.
54    pub message: String,
55}
56
57impl CheckIssue {
58    /// An error issue.
59    pub fn error(id: &'static str, message: impl Into<String>) -> Self {
60        Self {
61            level: CheckLevel::Error,
62            id,
63            message: message.into(),
64        }
65    }
66
67    /// A warning issue.
68    pub fn warning(id: &'static str, message: impl Into<String>) -> Self {
69        Self {
70            level: CheckLevel::Warning,
71            id,
72            message: message.into(),
73        }
74    }
75}
76
77impl fmt::Display for CheckIssue {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        write!(f, "{}: [{}] {}", self.level, self.id, self.message)
80    }
81}
82
83/// Whether any of `issues` is an error.
84pub fn has_errors(issues: &[CheckIssue]) -> bool {
85    issues.iter().any(|i| i.level == CheckLevel::Error)
86}
87
88/// Run every check.
89///
90/// `migrations_dir` is where the JSON migrations live; `None` skips the
91/// migration checks. Issues come out in a stable order: config, models,
92/// migrations, OpenAPI, backend.
93pub fn check(
94    app: &App,
95    models: &[&'static ModelMeta],
96    settings: &CliSettings,
97    migrations_dir: Option<&Path>,
98) -> Vec<CheckIssue> {
99    let mut issues = Vec::new();
100    issues.extend(config::check(models, settings));
101    issues.extend(models::check(models));
102    if let Some(dir) = migrations_dir {
103        issues.extend(migrations::check(models, dir));
104    }
105    issues.extend(openapi::check(app));
106    issues.extend(backend::check(models, settings));
107    issues
108}
109
110/// Check that `app` can be turned into a router, which catches duplicate
111/// routes and malformed path templates that the OpenAPI check does not see
112/// (for example hidden endpoints). Consumes the app: call it on a fresh one.
113pub fn check_build(app: App) -> Vec<CheckIssue> {
114    openapi::check_build(app)
115}
116
117/// Whether a managed model exists, i.e. whether any table is expected.
118pub(crate) fn has_managed_models(models: &[&'static ModelMeta]) -> bool {
119    models.iter().any(|m| m.managed)
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn issues_display_level_id_and_message() {
128        let issue = CheckIssue::error("models.E001", "boom");
129        assert_eq!(issue.to_string(), "error: [models.E001] boom");
130        let issue = CheckIssue::warning("migrations.W001", "meh");
131        assert_eq!(issue.to_string(), "warning: [migrations.W001] meh");
132    }
133
134    #[test]
135    fn has_errors_ignores_warnings() {
136        let warn = CheckIssue::warning("x.W001", "w");
137        assert!(!has_errors(std::slice::from_ref(&warn)));
138        assert!(has_errors(&[warn, CheckIssue::error("x.E001", "e")]));
139        assert!(CheckLevel::Warning < CheckLevel::Error);
140    }
141}