ordinary-app 0.8.2

Application server for Ordinary
Documentation
// Copyright (C) 2026 Ordinary Labs, LLC.
//
// SPDX-License-Identifier: AGPL-3.0-only

use crate::server::middleware::apply_custom_to_method_router;
use crate::server::{OrdinaryAppRouter, OrdinaryAppServerState, cors, ops};
use axum::Router;
use axum::http::StatusCode;
use axum::http::header::ETAG;
use axum::routing::get;
use bytes::Bytes;
use hashbrown::HashMap;
use ordinary_action::Engine;
use ordinary_auth::Auth;
use ordinary_config::{ContentDefinition, ModelConfig, OrdinaryConfig};
use ordinary_storage::{ArtifactKind, Storage};
use ordinary_template::Template;
use ordinary_types::json_to_flexbuffer_vec;
use ordinary_utils::middleware::{modify_etag_for_encoding, x_via};
use std::sync::Arc;
use std::time::Duration;
use tower::ServiceBuilder;
use tower_http::set_header::SetResponseHeaderLayer;
use tower_http::timeout::TimeoutLayer;

#[allow(clippy::too_many_lines, clippy::type_complexity)]
pub(crate) fn setup(
    config: &mut OrdinaryConfig,
    auth: &Arc<Auth>,
    storage: &Arc<Storage>,
    secure: bool,
    engine: &Engine,
    model_map: &mut HashMap<String, ModelConfig>,
    content_map: &mut HashMap<String, ContentDefinition>,
) -> (
    Vec<Template>,
    HashMap<String, usize>,
    Option<u8>,
    Option<u8>,
) {
    let template_ct = match &config.templates {
        Some(i) => i.len(),
        None => 0,
    };

    let mut templates = Vec::with_capacity(template_ct);
    let mut template_route_map = HashMap::with_capacity(template_ct);

    let mut error_template_idx = None;
    let mut mfa_totp_template_idx = None;

    let mut globals_map = HashMap::new();

    if let Some(globals) = &config.globals {
        for global in globals {
            globals_map.insert(global.name.clone(), global.clone());
        }
    }

    if let Some(mut template_configs) = config.templates.clone()
        && !template_configs.is_empty()
    {
        template_configs.sort_by_key(|a| a.idx);

        templates = template_configs
            .iter()
            .enumerate()
            .filter_map(|(i, template_config)| {
                if i != template_config.idx as usize {
                    tracing::error!("gap in or duplicate template indexes");
                }

                let artifact_span = tracing::info_span!("artifact");

                let src = artifact_span.in_scope(|| {
                    match storage
                        .artifact
                        .get(template_config.idx, ArtifactKind::Template)
                    {
                        Ok(val) => Some(val),
                        Err(err) => {
                            tracing::warn!(
                                name = template_config.name,
                                %err,
                                "template not stored",
                            );
                            None
                        }
                    }
                });

                let mut globals =
                    flexbuffers::Builder::new(&flexbuffers::BuilderOptions::SHARE_NONE);
                let mut globals_vec = globals.start_vector();

                if let Some(template_globals) = &template_config.globals {
                    for template_global in template_globals {
                        if let Some(global) = globals_map.get(template_global)
                            && let Err(err) = json_to_flexbuffer_vec(
                                &global.kind,
                                &global.value,
                                &mut globals_vec,
                            )
                        {
                            tracing::error!(%err);
                        }
                    }
                }

                globals_vec.end_vector();
                let template_globals = Bytes::copy_from_slice(globals.view());

                let mut fields =
                    flexbuffers::Builder::new(&flexbuffers::BuilderOptions::SHARE_NONE);
                let mut fields_vec = fields.start_vector();

                if let Some(template_fields) = &template_config.fields {
                    for field in template_fields {
                        if let Err(err) =
                            json_to_flexbuffer_vec(&field.kind, &field.value, &mut fields_vec)
                        {
                            tracing::error!(%err);
                        }
                    }
                }

                fields_vec.end_vector();
                let template_fields = Bytes::copy_from_slice(fields.view());

                Template::new(
                    &config.domain,
                    secure,
                    src,
                    template_config.clone(),
                    auth.clone(),
                    model_map,
                    content_map,
                    template_config
                        .globals
                        .is_some()
                        .then_some(template_globals),
                    template_config.fields.is_some().then_some(template_fields),
                    engine.clone(),
                    storage.clone(),
                )
                .ok()
            })
            .collect::<Vec<Template>>();

        for template in &templates {
            if let Some(auth) = &config.auth
                && let Some(totp_template) = &auth.mfa.totp.template
                && &template.config.name == totp_template
            {
                mfa_totp_template_idx = Some(template.idx);
            }

            if let Some(error) = &config.error
                && let Some(template_name) = &error.template
                && &template.config.name == template_name
            {
                error_template_idx = Some(template.idx);
            }

            template_route_map.insert(template.config.route.clone(), template.idx as usize);

            template.start_tasks();
        }
    }

    (
        templates,
        template_route_map,
        error_template_idx,
        mfa_totp_template_idx,
    )
}

#[allow(clippy::ref_option)]
pub(crate) fn setup_router(
    config: &Arc<OrdinaryConfig>,
    state: &Arc<OrdinaryAppServerState>,
    api_domain: &Option<String>,
    forwarded_by: &str,
    forwarded_proto: &str,
) -> Option<OrdinaryAppRouter> {
    if let Some(template_configs) = &config.templates
        && !template_configs.is_empty()
    {
        // todo: handle timeouts
        let mut router = Router::new().route(
            "/.ordinary/v1/templates/query/{idx}/{*path}",
            get(ops::templates::query).route_layer(
                ServiceBuilder::new()
                    .layer(SetResponseHeaderLayer::overriding(
                        ETAG,
                        modify_etag_for_encoding,
                    ))
                    .layer(axum::middleware::from_fn(x_via)),
            ),
        );

        for template_config in template_configs {
            // todo: default for default should come from Ordinary API
            let timeout_s = u64::from(
                template_config
                    .timeout
                    .unwrap_or(config.default_timeout.unwrap_or(10)),
            );

            let mut route = get(ops::templates::get).route_layer(
                ServiceBuilder::new()
                    .layer(TimeoutLayer::with_status_code(
                        StatusCode::REQUEST_TIMEOUT,
                        Duration::from_secs(timeout_s),
                    ))
                    .layer(SetResponseHeaderLayer::overriding(
                        ETAG,
                        modify_etag_for_encoding,
                    ))
                    .layer(axum::middleware::from_fn(x_via)),
            );

            if let Some(names) = &template_config.middlewares {
                route = apply_custom_to_method_router(
                    route,
                    config,
                    state,
                    names,
                    config.domain.clone(),
                    forwarded_by.to_string(),
                    forwarded_proto.to_string(),
                    api_domain.clone(),
                );
            }

            route = cors::apply_to_route(&config.cors, &config.cors, route);
            router = router.route(&template_config.route, route);
        }

        return Some(router);
    }

    None
}