csaf-crud 1.5.21

CSAF 2.0 / 2.1 advisory CRUD server with HATEOAS JSON API and HTML UI (TLS 1.3, HTTP/1.1 + HTTP/2 + HTTP/3)
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Pierre Gronau, ndaal in Cologne

//! Compile-time invariants for `csaf-crud`, per `skills/rust-static-assertions`.
//!
//! `AppState` is the type axum clones into every request handler and holds
//! across every `.await`. Axum's `FromRequestParts` / `Handler` machinery
//! requires it to be `Clone + Send + Sync + 'static`, and when that bound is
//! lost the compiler does not point at the line that broke it. It points at
//! the router, with a multi-screen trait-resolution error mentioning types
//! nobody wrote. Engineers then "fix" it by restructuring the router.
//!
//! These assertions move that failure back to its cause: change the shape of
//! `AppState` in a way that breaks the contract, and the build fails HERE,
//! naming this file.

use csaf_crud::app_state::AppState;
use csaf_crud::i18n::Lang;
use static_assertions::{assert_eq_size, assert_impl_all, assert_type_eq_all, const_assert};

// ---------------------------------------------------------------------------
// 1. The axum shared-state contract
// ---------------------------------------------------------------------------
//
// Every one of these is required by axum's `with_state`. `Clone` in
// particular must stay CHEAP — `AppState` is cloned per request, which is why
// it is a single `Arc<AppStateInner>` and not a struct of owned fields. A
// refactor that flattens the inner Arc away would still satisfy `Clone` and
// still compile, but would deep-copy settings and config on every request.
// That is a performance cliff no test would catch, so it is called out here
// even though the type system cannot enforce it.
assert_impl_all!(AppState: Clone, Send, Sync);

// `'static` is what lets the state be moved into a spawned task. Asserting it
// explicitly documents that any future borrowed field is a breaking change.
//
// `assert_impl_all!` takes traits, not lifetime bounds, so this is the
// hand-rolled equivalent: it is evaluated at compile time and costs nothing
// at runtime.
const _: fn() = || {
    const fn assert_static<T: 'static>() {}
    assert_static::<AppState>();
};

// ---------------------------------------------------------------------------
// 2. Settings cross the RwLock boundary by value
// ---------------------------------------------------------------------------
//
// `AppState::settings()` returns an owned `Settings` clone rather than a
// guard. That is deliberate: handing a `RwLockReadGuard` to a handler would
// let it be held across an `.await`, which deadlocks the writer. The return
// type being the owned value — not a guard — is the invariant.
assert_type_eq_all!(
    <AppState as StateSettings>::Out,
    csaf_models::settings::Settings
);

/// Local witness trait: pins the *return type* of `AppState::settings()` so
/// that changing it to a guard breaks the build here rather than deadlocking
/// under load. Implemented by hand so `assert_type_eq_all!` has a name to
/// compare against.
trait StateSettings {
    /// What `settings()` hands back.
    type Out;
}

impl StateSettings for AppState {
    // If `settings()` is ever changed to return a guard or a reference, this
    // associated type no longer matches the assertion above.
    type Out = csaf_models::settings::Settings;
}

// ---------------------------------------------------------------------------
// 3. The Lang enum's shape and menu contract
// ---------------------------------------------------------------------------
//
// `Lang` is copied into every request and matched against three exhaustive
// tables; it must stay a payload-free fieldless enum. A variant that grows a
// payload would still compile everywhere `Lang` is matched — these pin the
// consequences instead.
assert_impl_all!(Lang: Copy, Clone, Send, Sync, Eq, Default, core::fmt::Debug);

// Fieldless with <= 256 variants — one byte, and the niche keeps
// `Option<Lang>` at one byte too (no hidden regression in the per-request
// footprint or in struct layouts that embed it).
assert_eq_size!(Lang, u8);
assert_eq_size!(Option<Lang>, u8);

// Adding a language without wiring it through `all()` fails the BUILD, not a
// runtime test. 46 European (skills/languages-europe-rust) + 3 Asian —
// Chinese, Hindi, Urdu (skills/languages-asia) = 49.
const_assert!(Lang::all().len() == 49);

// The first five menu positions are a UI contract (this product's historical
// order — En, De, Fr, Es, It). Reordering them fails the build here.
const_assert!(matches!(Lang::all()[0], Lang::En));
const_assert!(matches!(Lang::all()[1], Lang::De));
const_assert!(matches!(Lang::all()[2], Lang::Fr));
const_assert!(matches!(Lang::all()[3], Lang::Es));
const_assert!(matches!(Lang::all()[4], Lang::It));