coil-core 0.1.1

Core runtime contracts and composition primitives for the Coil framework.
Documentation
use super::*;

pub(crate) fn cache_topology_from_config(config: &PlatformConfig) -> CacheTopology {
    match config.cache.l2 {
        Some(DistributedCache::Redis) => CacheTopology::with_redis(),
        Some(DistributedCache::Valkey) => CacheTopology::with_valkey(),
        None => CacheTopology::moka_only(),
    }
}

pub(crate) fn browser_security_from_config(config: &PlatformConfig) -> BrowserSecurityServices {
    BrowserSecurityServices {
        sessions: SessionSecurityServices {
            store: match config.http.session.store {
                ConfigSessionStore::Memory => SessionStoreTopology::Memory,
                ConfigSessionStore::Database => SessionStoreTopology::Database,
                ConfigSessionStore::Redis => SessionStoreTopology::Redis,
                ConfigSessionStore::Valkey => SessionStoreTopology::Valkey,
            },
            idle_timeout: Duration::from_secs(config.http.session.idle_timeout_secs),
            absolute_timeout: Duration::from_secs(config.http.session.absolute_timeout_secs),
            session_cookie: CookiePolicy::from_config(&config.http.session_cookie),
            flash_cookie: CookiePolicy::from_config(&config.http.flash_cookie),
        },
        csrf: CsrfProtection::from_config(&config.http.csrf),
    }
}

pub(crate) fn observability_runtime_from_config(
    config: &PlatformConfig,
) -> ObservabilityRuntimeServices {
    let mut runtime = ObservabilityRuntime::baseline(&config.observability, config.app.environment)
        .expect("baseline observability runtime must be valid");

    runtime.liveness = HealthReport::new(HealthProbeKind::Liveness);

    let mut readiness = HealthReport::new(HealthProbeKind::Readiness)
        .with_dependency(DependencyKind::Database, true, DependencyStatus::Healthy)
        .expect("database dependency must be unique")
        .with_dependency(
            DependencyKind::ExtensionRegistry,
            true,
            DependencyStatus::Healthy,
        )
        .expect("extension registry dependency must be unique")
        .with_dependency(DependencyKind::Queue, true, DependencyStatus::Healthy)
        .expect("queue dependency must be unique");

    if config.cache.l2.is_some()
        || matches!(
            config.http.session.store,
            ConfigSessionStore::Redis | ConfigSessionStore::Valkey
        )
    {
        readiness = readiness
            .with_dependency(
                DependencyKind::DistributedCache,
                true,
                DependencyStatus::Healthy,
            )
            .expect("distributed cache dependency must be unique");
    }

    if config.storage.object_store.is_some() {
        readiness = readiness
            .with_dependency(DependencyKind::ObjectStore, true, DependencyStatus::Healthy)
            .expect("object store dependency must be unique");
    }

    if config.storage.object_store_secret.is_some()
        || config.auth.tuple_store_secret.is_some()
        || config.tls.provider.is_some()
    {
        readiness = readiness
            .with_dependency(DependencyKind::Secrets, true, DependencyStatus::Healthy)
            .expect("secrets dependency must be unique");
    }

    if config.tls.mode != TlsMode::External {
        readiness = readiness
            .with_dependency(DependencyKind::Tls, true, DependencyStatus::Healthy)
            .expect("tls dependency must be unique");
    }

    runtime.readiness = readiness;
    runtime.maintenance = MaintenanceMode::disabled();
    runtime
}

pub(crate) fn jobs_runtime_from_config(config: &PlatformConfig) -> JobsRuntimeServices {
    JobsRuntime::from_config(&config.jobs).expect("jobs runtime config must be valid")
}

pub(crate) fn data_runtime_from_config(config: &PlatformConfig) -> DataRuntimeServices {
    DataRuntime::from_config(&config.database).expect("data runtime config must be valid")
}

pub(crate) fn cli_runtime_from_config(config: &PlatformConfig) -> CliRuntimeServices {
    CliRuntimeServices::new(config.app.name.clone(), 4)
}

pub(crate) fn tls_runtime_from_config(config: &PlatformConfig) -> TlsRuntimeServices {
    TlsRuntime::from_config(&config.tls)
}

pub(crate) fn template_runtime_services() -> TemplateRuntimeServices {
    let registry = TemplateRegistry::new();

    TemplateRuntimeServices {
        customer_app_namespace: TemplateNamespace::new("customer-app")
            .expect("constant template namespace is valid"),
        core_namespace: TemplateNamespace::new("core")
            .expect("constant template namespace is valid"),
        runtime: TemplateRuntime::new(registry.clone()),
        registry,
    }
}

pub(crate) fn i18n_runtime_from_config(
    config: &PlatformConfig,
    customer_catalogs: Vec<TranslationCatalog>,
) -> I18nRuntimeServices {
    let default_locale =
        LocaleTag::new(config.i18n.default_locale.clone()).expect("validated locale");
    let supported_locales = config
        .i18n
        .supported_locales
        .iter()
        .cloned()
        .map(LocaleTag::new)
        .collect::<Result<Vec<_>, _>>()
        .expect("validated locales");
    let fallback_locale =
        LocaleTag::new(config.i18n.fallback_locale.clone()).expect("validated locale");
    let router = LocaleRouter::new(
        LocaleUrlConfig::path_prefix(config.seo.canonical_host.clone())
            .expect("validated canonical host"),
    );
    let customer_catalogs_by_locale = customer_catalogs
        .into_iter()
        .map(|catalog| (catalog.locale().clone(), catalog))
        .collect::<std::collections::HashMap<_, _>>();
    let translations = TranslationRuntime::new(
        default_locale.clone(),
        supported_locales
            .iter()
            .cloned()
            .map(|locale| {
                merged_translation_catalog(
                    locale.clone(),
                    customer_catalogs_by_locale.get(&locale),
                )
            })
            .collect::<Vec<_>>(),
    )
    .expect("default translation runtime");

    I18nRuntimeServices {
        default_locale,
        supported_locales,
        fallback_locale,
        router,
        translations,
    }
}

fn merged_translation_catalog(
    locale: LocaleTag,
    customer_catalog: Option<&TranslationCatalog>,
) -> TranslationCatalog {
    let core_locale_key = coil_i18n::MessageKey::new("core.locale").expect("static key");
    let mut messages = vec![(
        core_locale_key.clone(),
        locale.to_string(),
    )];
    if let Some(customer_catalog) = customer_catalog {
        messages.extend(
            customer_catalog
                .messages()
                .filter(|(key, _)| *key != &core_locale_key)
                .map(|(key, value)| (key.clone(), value.to_string())),
        );
    }
    TranslationCatalog::new(locale, messages).expect("merged translation catalog")
}

pub(crate) fn seo_runtime_from_config(config: &PlatformConfig) -> SeoRuntimeServices {
    SeoRuntimeServices {
        canonical_host: config.seo.canonical_host.clone(),
        emit_json_ld: config.seo.emit_json_ld,
        sitemap_enabled: config.seo.sitemap_enabled,
    }
}

pub(crate) fn a11y_runtime_services() -> A11yRuntimeServices {
    A11yRuntimeServices {
        navigation: NavigationContract::standard(),
        theme_baseline: ThemeAccessibilityContract::new(4.5, 3.0, 3.0, true, true)
            .expect("static baseline"),
    }
}

pub(crate) fn wasm_runtime_from_config(config: &PlatformConfig) -> WasmRuntimeServices {
    let request_limit = Duration::from_millis(config.wasm.default_time_limit_ms);
    let tighten = |point| tighten_runtime_limit(ResourceLimits::baseline_for(point), request_limit);

    WasmRuntimeServices {
        extension_directory: config.wasm.directory.clone(),
        allow_network: config.wasm.allow_network,
        limits: WasmLimitsProfile {
            page: tighten(ExtensionPointKind::Page),
            api: tighten(ExtensionPointKind::Api),
            admin_widget: tighten(ExtensionPointKind::AdminWidget),
            render_hook: tighten(ExtensionPointKind::RenderHook),
            webhook: tighten(ExtensionPointKind::Webhook),
            job: ResourceLimits::baseline_for(ExtensionPointKind::Job),
            scheduled_job: ResourceLimits::baseline_for(ExtensionPointKind::ScheduledJob),
        },
    }
}

pub(crate) fn tighten_runtime_limit(
    mut limits: ResourceLimits,
    max_runtime: Duration,
) -> ResourceLimits {
    if max_runtime < limits.max_runtime {
        limits.max_runtime = max_runtime;
    }

    limits
}

pub(crate) fn currency_for_locale(locale: &LocaleTag) -> CurrencyCode {
    let currency = match locale.as_str() {
        "fr-FR" => "EUR",
        _ => "GBP",
    };
    CurrencyCode::new(currency).expect("static currency")
}

pub(crate) fn timezone_for_locale(locale: &LocaleTag) -> TimeZoneId {
    let timezone = match locale.as_str() {
        "fr-FR" => "Europe/Paris",
        _ => "Europe/London",
    };
    TimeZoneId::new(timezone).expect("static timezone")
}