Skip to main content

siderite_cli/report/
build.rs

1//! Pure report builders. They read compiled metadata only: no database
2//! connection is opened and nothing is printed.
3
4use super::{CheckSummary, Diagnostic, Envelope, Severity, redact_text};
5use super::{FieldReport, ModelReport, ModelsData};
6use super::{MigrationReport, MigrationStatusData, UrlSource};
7use super::{RouteReport, RoutesData};
8use crate::check::{CheckIssue, CheckLevel, check, check_build};
9use crate::error::CliError;
10use crate::settings::CliSettings;
11use siderite_core::App;
12use siderite_orm::ModelMeta;
13use std::path::Path;
14
15/// The documented routes of `app`, sorted by path then method.
16///
17/// Rows come from the OpenAPI document, so hidden endpoints are omitted.
18///
19/// Errors:
20///   [`CliError::OpenApi`] when the document cannot be generated.
21pub fn route_reports(app: &App) -> Result<Vec<RouteReport>, CliError> {
22    let document = app
23        .openapi()
24        .map_err(|err| CliError::OpenApi(err.to_string()))?;
25    let mut rows = Vec::new();
26    for (path, item) in &document.paths {
27        for (method, operation) in &item.0 {
28            let mut tags = operation.tags.clone();
29            tags.sort();
30            tags.dedup();
31            rows.push(RouteReport {
32                method: method.to_ascii_uppercase(),
33                path: path.clone(),
34                operation_id: operation.operation_id.clone(),
35                tags,
36                summary: operation.summary.as_deref().map(redact_text),
37            });
38        }
39    }
40    rows.sort_by(|a, b| (&a.path, &a.method).cmp(&(&b.path, &b.method)));
41    Ok(rows)
42}
43
44/// The `routes` report of `app`.
45///
46/// Errors:
47///   [`CliError::OpenApi`] when the document cannot be generated.
48pub fn routes(app: &App) -> Result<Envelope<RoutesData>, CliError> {
49    let routes = route_reports(app)?;
50    Ok(Envelope::new("routes", RoutesData { routes }, Vec::new()))
51}
52
53/// Every registered model, sorted by name then table, fields sorted by
54/// name then column.
55pub fn model_reports(models: &[&'static ModelMeta]) -> Vec<ModelReport> {
56    let mut reports: Vec<ModelReport> = models
57        .iter()
58        .map(|model| {
59            let mut fields: Vec<FieldReport> = model
60                .fields
61                .iter()
62                .map(|f| FieldReport {
63                    name: f.name.to_owned(),
64                    column: f.column.to_owned(),
65                    sql_type: format!("{:?}", f.sql_type),
66                    nullable: f.nullable,
67                    pk: f.primary_key,
68                })
69                .collect();
70            fields.sort_by_key(|f| (f.name.clone(), f.column.clone()));
71            ModelReport {
72                name: model.name.to_owned(),
73                table: model.table.to_owned(),
74                managed: model.managed,
75                fields,
76            }
77        })
78        .collect();
79    reports.sort_by(|a, b| (&a.name, &a.table).cmp(&(&b.name, &b.table)));
80    reports
81}
82
83/// The `models` report of the registered `models`.
84pub fn models(models: &[&'static ModelMeta]) -> Envelope<ModelsData> {
85    let models = model_reports(models);
86    Envelope::new("models", ModelsData { models }, Vec::new())
87}
88
89/// Convert a [`CheckIssue`] to a [`Diagnostic`].
90pub fn diagnostic(issue: &CheckIssue) -> Diagnostic {
91    let severity = match issue.level {
92        CheckLevel::Warning => Severity::Warning,
93        CheckLevel::Error => Severity::Error,
94    };
95    Diagnostic::new(issue.id, severity, &issue.message)
96}
97
98/// The `check` report: every issue in check order (config, models,
99/// migrations, OpenAPI, backend, router build).
100///
101/// `factory` builds a fresh app; the router-build check consumes one.
102/// `migrations_dir` is where the JSON migrations live (`None` skips the
103/// migration checks).
104pub fn check_report(
105    factory: &dyn Fn() -> App,
106    models: &[&'static ModelMeta],
107    settings: &CliSettings,
108    migrations_dir: Option<&Path>,
109) -> Envelope<CheckSummary> {
110    let mut issues = check(&factory(), models, settings, migrations_dir);
111    // A duplicate operation already fails the OpenAPI check; do not
112    // report the same route problem a second time.
113    if !issues.iter().any(|i| i.id.starts_with("openapi.")) {
114        issues.extend(check_build(factory()));
115    }
116    let errors = issues
117        .iter()
118        .filter(|i| i.level == CheckLevel::Error)
119        .count();
120    let summary = CheckSummary {
121        errors,
122        warnings: issues.len() - errors,
123    };
124    let diagnostics = issues.iter().map(diagnostic).collect();
125    Envelope::new("check", summary, diagnostics)
126}
127
128/// The `showmigrations` report over rows of `Migrator::show`, kept in
129/// their order so JSON and text agree.
130pub fn migration_status(
131    alias: Option<&str>,
132    source: UrlSource,
133    rows: Vec<(String, bool)>,
134) -> Envelope<MigrationStatusData> {
135    let applied = rows.iter().filter(|(_, applied)| *applied).count();
136    let pending = rows.len() - applied;
137    let migrations = rows
138        .into_iter()
139        .map(|(id, applied)| MigrationReport { id, applied })
140        .collect();
141    let data = MigrationStatusData {
142        alias: alias.map(str::to_owned),
143        source,
144        migrations,
145        applied,
146        pending,
147    };
148    Envelope::new("showmigrations", data, Vec::new())
149}