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::routing::{delete, get, post, put};
use hashbrown::HashMap;
use ordinary_action::{Action, Engine, PrivilegedComponents};
use ordinary_auth::Auth;
use ordinary_config::{ActionTrigger, ContentDefinition, HttpMethod, ModelConfig, OrdinaryConfig};
use ordinary_integration::Integration;
use ordinary_storage::{ArtifactKind, Storage};
use ordinary_utils::middleware::x_via;
use std::sync::Arc;
use std::time::Duration;
use tower::ServiceBuilder;
use tower_http::timeout::TimeoutLayer;

#[allow(clippy::too_many_arguments, clippy::too_many_lines, clippy::ref_option)]
pub(super) fn setup(
    config: &mut OrdinaryConfig,
    auth: &Arc<Auth>,
    storage: &Arc<Storage>,
    privileged_components: &Option<PrivilegedComponents>,
    integrations: &Arc<Vec<Integration>>,
    engine: &Engine,
    model_map: &mut HashMap<String, ModelConfig>,
    content_map: &mut HashMap<String, ContentDefinition>,
) -> (Vec<Action>, HashMap<String, usize>, Vec<u8>) {
    let action_ct = match &config.actions {
        Some(i) => i.len(),
        None => 0,
    };

    let mut actions = Vec::with_capacity(action_ct);
    let mut action_route_map = HashMap::with_capacity(action_ct);
    let mut registration_actions = vec![];

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

        actions = action_configs
            .iter()
            .enumerate()
            .filter_map(|(i, action_config)| {
                if i != action_config.idx as usize {
                    tracing::error!("gap in or duplicate action indexes");
                }

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

                let src = artifact_span.in_scope(|| {
                    match storage
                        .artifact
                        .get(action_config.idx, ArtifactKind::Action)
                    {
                        Ok(val) => Some(val),
                        Err(err) => {
                            tracing::warn!("action '{}' not stored - {err}", action_config.name);
                            None
                        }
                    }
                });

                Action::new(
                    src,
                    engine.clone(),
                    action_config.clone(),
                    auth.clone(),
                    storage.clone(),
                    integrations.clone(),
                    model_map,
                    content_map,
                    &config.actions,
                    privileged_components.clone(),
                )
                .ok()
            })
            .collect::<Vec<Action>>();

        for action in &actions {
            for trigger in &action.config.triggered_by {
                match trigger {
                    ActionTrigger::Registration => {
                        registration_actions.push(action.idx);
                    }
                    ActionTrigger::Json { route, method: _ }
                    | ActionTrigger::Form {
                        route,
                        method: _,
                        redirect: _,
                    } => {
                        action_route_map.insert(route.clone(), action.idx as usize);
                    }
                    _ => {}
                }
            }
        }
    }

    actions.shrink_to_fit();
    action_route_map.shrink_to_fit();
    registration_actions.shrink_to_fit();

    (actions, action_route_map, registration_actions)
}

#[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(action_configs) = &config.actions
        && !action_configs.is_empty()
    {
        let mut router = Router::new();

        // todo: handle timeouts
        router = router.route(
            "/.ordinary/v1/actions/invoke/{idx}",
            post(ops::actions::invoke),
        );

        for action_config in action_configs {
            // todo: default for default should come from Ordinary API
            let timeout_s = u64::from(
                action_config
                    .timeout
                    .unwrap_or(config.default_timeout.unwrap_or(10)),
            );
            let timeout_layer = TimeoutLayer::with_status_code(
                StatusCode::REQUEST_TIMEOUT,
                Duration::from_secs(timeout_s),
            );

            for trigger in &action_config.triggered_by {
                let (route, mut mr) = match trigger {
                    ActionTrigger::Form {
                        route,
                        method,
                        redirect: _,
                    } => match method {
                        HttpMethod::GET => (route, get(ops::actions::form)),
                        HttpMethod::PUT => (route, put(ops::actions::form)),
                        HttpMethod::POST => (route, post(ops::actions::form)),
                        HttpMethod::DELETE => (route, delete(ops::actions::form)),
                    },
                    ActionTrigger::Json { route, method } => match method {
                        HttpMethod::GET => (route, get(ops::actions::json)),
                        HttpMethod::PUT => (route, put(ops::actions::json)),
                        HttpMethod::POST => (route, post(ops::actions::json)),
                        HttpMethod::DELETE => (route, delete(ops::actions::json)),
                    },
                    _ => {
                        continue;
                    }
                };

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

                mr = mr.route_layer(
                    ServiceBuilder::new()
                        .layer(timeout_layer)
                        .layer(axum::middleware::from_fn(x_via)),
                );
                mr = cors::apply_to_route(&config.cors, &config.cors, mr);

                router = router.route(route, mr);
            }
        }

        return Some(router);
    }

    None
}