#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc = include_str!("../README.md")]
#![doc = include_str!("../docs/app-config-reference.md")]
#![doc = include_str!("../docs/api-config-reference.md")]
#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
#[cfg(feature = "docs")]
pub mod jsonschema;
#[cfg(feature = "docs")]
pub use schemars;
mod api;
mod app;
mod validate;
pub use api::*;
pub use app::*;
pub use crate::validate::DOMAIN_REGEX;
use crate::validate::validate;
use std::collections::BTreeSet;
use anyhow::bail;
use arrayvec::ArrayVec;
use hashbrown::{HashMap, HashSet};
use ordinary_types::{Field, Kind};
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter, Write};
use std::path::Path;
use std::process::Command;
use std::{env, fs};
use tracing::instrument;
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ClientLoggingConfig {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
min_delay: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
max_delay: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
max_buffer: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
max_batch: Option<u16>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum RedactedHashAlg {
Blake2,
Blake3,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ServerLoggingConfig {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub ips: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub headers: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub credentials: Option<RedactedHashAlg>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub timing: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub sizes: Option<bool>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct LoggingConfig {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub client: Option<ClientLoggingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub server: Option<ServerLoggingConfig>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Check {
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum TokenAlgorithm {
HmacBlake2b256,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct RefreshTokenConfig {
pub algorithm: TokenAlgorithm,
pub lifetime: u32,
pub rotation: u32,
}
impl Default for RefreshTokenConfig {
fn default() -> RefreshTokenConfig {
RefreshTokenConfig {
algorithm: TokenAlgorithm::HmacBlake2b256,
lifetime: 60 * 60 * 24 * 7,
rotation: 60 * 60 * 24 * 7 * 2,
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct AccessTokenConfig {
pub algorithm: TokenAlgorithm,
pub lifetime: u32,
pub rotation: u32,
pub claims: Vec<Field>,
}
impl Default for AccessTokenConfig {
fn default() -> AccessTokenConfig {
AccessTokenConfig {
algorithm: TokenAlgorithm::HmacBlake2b256,
lifetime: 60 * 60 * 24,
rotation: 60 * 60 * 24 * 3,
claims: vec![],
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ClientPasswordHash {
Sha256,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum PasswordProtocol {
Opaque,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct PasswordConfig {
pub protocol: PasswordProtocol,
}
impl Default for PasswordConfig {
fn default() -> PasswordConfig {
PasswordConfig {
protocol: PasswordProtocol::Opaque,
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum TotpAlgorithm {
Sha1,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TotpConfig {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub template: Option<String>,
pub algorithm: TotpAlgorithm,
}
impl Default for TotpConfig {
fn default() -> TotpConfig {
TotpConfig {
template: None,
algorithm: TotpAlgorithm::Sha1,
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct MfaConfig {
pub totp: TotpConfig,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum InviteMode {
Root,
Admin,
Viral,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct InviteConfig {
pub mode: InviteMode,
pub lifetime: u32,
pub clean_interval: (u32, u32),
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub claims: Option<Vec<Field>>,
}
impl Default for InviteConfig {
fn default() -> InviteConfig {
InviteConfig {
mode: InviteMode::Viral,
lifetime: 60 * 60 * 24 * 7,
clean_interval: (30, 90),
claims: None,
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct AuthConfig {
pub password: PasswordConfig,
pub mfa: MfaConfig,
pub refresh_token: RefreshTokenConfig,
pub access_token: AccessTokenConfig,
pub cookies_enabled: bool,
pub client_hash: ClientPasswordHash,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub invite: Option<InviteConfig>,
}
impl Default for AuthConfig {
fn default() -> AuthConfig {
AuthConfig {
password: PasswordConfig::default(),
mfa: MfaConfig::default(),
refresh_token: RefreshTokenConfig::default(),
access_token: AccessTokenConfig::default(),
cookies_enabled: false,
client_hash: ClientPasswordHash::Sha256,
invite: None,
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
pub enum CompressionAlgorithm {
All,
Gzip,
Zstd { level: u8 },
Brotli,
Deflate,
}
impl CompressionAlgorithm {
#[must_use]
pub fn as_u8(&self) -> u8 {
match self {
Self::All => 0,
Self::Gzip => 1,
Self::Zstd { level: _ } => 2,
Self::Brotli => 3,
Self::Deflate => 4,
}
}
#[must_use]
pub fn as_char(&self) -> char {
match self {
Self::All => '0',
Self::Gzip => '1',
Self::Zstd { level: _ } => '2',
Self::Brotli => '3',
Self::Deflate => '4',
}
}
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::All => "",
Self::Gzip => "gzip",
Self::Zstd { level: _ } => "zstd",
Self::Brotli => "br",
Self::Deflate => "deflate",
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum StoredCachePolicy {
Permanent,
QuickCache,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct StoredCache {
pub policy: StoredCachePolicy,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub compression: Option<CompressionAlgorithms>,
#[serde(skip)]
#[serde(default)]
pub internal_compression: Option<ArrayVec<CompressionAlgorithm, 4>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub max_ttl: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub hit_ttl: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub max_size: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub max_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub frequency_window: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub clean_interval: Option<(u64, u64)>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub evict_on_dependency_change: Option<bool>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum XXH3Variation {
Bit64,
Bit128,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum HttpEtagAlgorithm {
AHash,
XXH3(XXH3Variation),
Rustc,
Blake3,
Foldhash,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct HttpEtag {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub alg: Option<HttpEtagAlgorithm>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct HttpCacheControl {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub max_age: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub s_maxage: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub no_cache: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub no_store: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub no_transform: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub must_revalidate: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub proxy_revalidate: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub private: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub public: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub immutable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub stale_while_revalidate: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub stale_if_error: Option<bool>,
}
impl HttpCacheControl {
pub fn header_value(&self, cache_control: &mut String, default: &str) -> anyhow::Result<()> {
if let Some(max_age) = self.max_age {
write!(cache_control, "max-age={max_age}, ")?;
}
if let Some(s_maxage) = self.s_maxage {
write!(cache_control, "s-maxage={s_maxage}, ")?;
}
if let Some(no_cache) = self.no_cache
&& no_cache
{
write!(cache_control, "no-cache, ")?;
}
if let Some(no_store) = self.no_store
&& no_store
{
write!(cache_control, "no-store, ")?;
}
if let Some(no_transform) = self.no_transform
&& no_transform
{
write!(cache_control, "no-transform, ")?;
}
if let Some(must_revalidate) = self.must_revalidate
&& must_revalidate
{
write!(cache_control, "must-revalidate, ")?;
}
if let Some(proxy_revalidate) = self.proxy_revalidate
&& proxy_revalidate
{
write!(cache_control, "proxy-revalidate, ")?;
}
if let Some(stale_while_revalidate) = self.stale_while_revalidate
&& stale_while_revalidate
{
write!(cache_control, "stale-while-revalidate, ")?;
}
if let Some(private) = self.private
&& private
{
write!(cache_control, "private, ")?;
}
if let Some(public) = self.public
&& public
{
write!(cache_control, "public, ")?;
}
if let Some(immutable) = self.immutable
&& immutable
{
write!(cache_control, "immutable, ")?;
}
if let Some(stale_if_error) = self.stale_if_error
&& stale_if_error
{
write!(cache_control, "stale-if-error, ")?;
}
if cache_control.is_empty() {
cache_control.push_str(default);
} else {
cache_control.pop();
cache_control.pop();
}
Ok(())
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct HttpCache {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub cache_control: Option<HttpCacheControl>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub expires: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub etag: Option<HttpEtag>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct CompressionAlgorithms(pub Vec<CompressionAlgorithm>);
impl CompressionAlgorithms {
#[must_use]
fn get_list(&self) -> ArrayVec<CompressionAlgorithm, 4> {
let mut list = ArrayVec::<CompressionAlgorithm, 4>::new();
let mut has_all = false;
for alg in &self.0 {
if *alg == CompressionAlgorithm::All {
has_all = true;
} else if !list.contains(alg) {
list.push(alg.clone());
}
}
if has_all {
for alg in [
CompressionAlgorithm::Brotli,
CompressionAlgorithm::Zstd { level: 17 },
CompressionAlgorithm::Deflate,
CompressionAlgorithm::Gzip,
] {
if !list.contains(&alg) {
list.push(alg);
}
}
}
list
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct AssetsConfig {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub dir_path: Option<String>,
#[serde(default = "AssetsConfig::default_base_route")]
pub base_route: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub append_index_html: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub skip_base_route_index_html: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub append_html_ext: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub preserve_exif: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub html_csp: Option<HttpCsp>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub http: Option<HttpCache>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub precompression: Option<CompressionAlgorithms>,
#[serde(skip)]
#[serde(default)]
pub internal_precompression: Option<ArrayVec<CompressionAlgorithm, 4>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub minify_css: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub minify_js: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub minify_html: Option<bool>,
#[serde(skip)]
#[serde(default)]
pub internal_cache_control_header_value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub middlewares: Option<Vec<String>>,
}
impl AssetsConfig {
fn default_base_route() -> String {
"/assets".to_string()
}
}
impl AssetsConfig {
pub fn init(&mut self, default_cache_control_header_value: &str) -> anyhow::Result<()> {
if let Some(http_cache) = &self.http
&& let Some(http_cache_control) = &http_cache.cache_control
{
let mut header = String::new();
http_cache_control.header_value(&mut header, default_cache_control_header_value)?;
self.internal_cache_control_header_value = Some(header);
}
Ok(())
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct FragmentsConfig {
pub dir_path: String,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Global {
pub name: String,
pub kind: Kind,
pub value: serde_json::Value,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum SecretSource {
Env,
Stored,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum SecretVisibility {
Integrations,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Secret {
pub name: String,
pub source: SecretSource,
pub visibility: SecretVisibility,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct FlagOption {
pub idx: u8,
pub name: String,
pub percentage: u8,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Flag {
pub idx: u8,
pub name: String,
pub options: Vec<FlagOption>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ContentDefinition {
pub idx: u8,
pub name: String,
pub fields: Vec<Field>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub lifecycle: Option<ContentObjectLifecycle>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ContentObjectLifecycle {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub before_all: Option<Vec<Vec<String>>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub on_add: Option<LifecycleBeforeAfterScripts>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub on_edit: Option<LifecycleBeforeAfterScripts>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub on_delete: Option<LifecycleBeforeAfterScripts>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct Content {
pub file_path: String,
pub definitions: Vec<ContentDefinition>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub update: Option<ContentUpdateConfig>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ContentUpdateConfig {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub lifecycle: Option<LifecycleBeforeAfterScripts>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum UuidVersion {
V4,
V7,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ModelConfig {
pub idx: u8,
pub name: String,
pub fields: Vec<Field>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub uuid: Option<UuidVersion>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum IntegrationProtocolHttpEncoding {
Json,
Text,
None,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum IntegrationProtocol {
Http {
method: String,
headers: Vec<(String, String)>,
send_encoding: IntegrationProtocolHttpEncoding,
recv_encoding: IntegrationProtocolHttpEncoding,
},
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct IntegrationConfig {
pub idx: u8,
pub name: String,
pub protocol: IntegrationProtocol,
pub endpoint: String,
pub send: Kind,
pub recv: Kind,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub secrets: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub timeout: Option<u16>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionLang {
Rust,
JavaScript,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionAccessModelOps {
Insert,
Get,
Query,
Search,
Update,
Delete,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionAccessAuthOps {
SetTokenFields,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionAccessPermission {
Model {
name: String,
ops: Vec<ActionAccessModelOps>,
},
Content {
name: String,
},
Integration {
name: String,
},
Action {
name: String,
},
Auth {
ops: Vec<ActionAccessAuthOps>,
},
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum HttpMethod {
PUT,
POST,
GET,
DELETE,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionTriggerModelOps {
Insert,
Update,
Delete,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionTrigger {
Ordinary,
Json {
route: String,
method: HttpMethod,
},
Form {
route: String,
method: HttpMethod,
redirect: String,
},
Login,
Registration,
Content { name: String },
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionFfiVersion {
V1,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionFfiSerialization {
FlexBufferVector,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ActionFfi {
pub version: ActionFfiVersion,
pub serialization: ActionFfiSerialization,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ActionConfig {
pub ffi: ActionFfi,
pub idx: u8,
pub name: String,
pub lang: ActionLang,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub dir_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub protected: Option<Check>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub transactional: Option<bool>,
pub access: Vec<ActionAccessPermission>,
pub accepts: Kind,
pub returns: Kind,
pub triggered_by: Vec<ActionTrigger>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub timeout: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub cors: Option<HttpCors>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub privileged: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub variables: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub middlewares: Option<Vec<String>>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct ErrorConfig {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub template: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub asset: Option<String>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum RuntimeMode {
Shared,
SingleThreaded,
MultiThreaded,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum HttpCorsAllowHeaders {
Any,
Headers(Vec<String>),
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum HttpCorsExposeHeaders {
Any,
Headers(Vec<String>),
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum HttpCorsAllowMethods {
Any,
Methods(Vec<String>),
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum HttpCorsAllowOrigin {
Any,
Origins(Vec<String>),
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct HttpCors {
pub allow_credentials: Option<bool>,
pub allow_headers: Option<HttpCorsAllowHeaders>,
pub max_age: Option<u32>,
pub allow_methods: Option<HttpCorsAllowMethods>,
pub allow_origin: Option<HttpCorsAllowOrigin>,
pub expose_headers: Option<HttpCorsExposeHeaders>,
pub allow_private_network: Option<bool>,
}
impl HttpCors {
#[must_use]
pub fn overwrite(&self, base: &Self) -> Self {
Self {
allow_credentials: if self.allow_credentials.is_none() {
base.allow_credentials
} else {
self.allow_credentials
},
allow_headers: if self.allow_headers.is_none() {
base.allow_headers.clone()
} else {
self.allow_headers.clone()
},
max_age: if self.max_age.is_none() {
base.max_age
} else {
self.max_age
},
allow_methods: if self.allow_methods.is_none() {
base.allow_methods.clone()
} else {
self.allow_methods.clone()
},
allow_origin: if self.allow_origin.is_none() {
base.allow_origin.clone()
} else {
self.allow_origin.clone()
},
expose_headers: if self.expose_headers.is_none() {
base.expose_headers.clone()
} else {
self.expose_headers.clone()
},
allow_private_network: if self.allow_private_network.is_none() {
base.allow_private_network
} else {
self.allow_private_network
},
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct HttpCsp {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub default_src: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub script_src: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub style_src: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub font_src: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub img_src: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub frame_src: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub include_inline_hashes: Option<bool>,
}
impl HttpCsp {
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn build_string(
&self,
base: &Self,
inline_style_hashes: Option<Vec<String>>,
inline_script_hashes: Option<Vec<String>>,
script_urls: Option<Vec<String>>,
secure: bool,
has_wasm: bool,
) -> String {
let mut out = String::new();
let include_inline_hashes = self
.include_inline_hashes
.unwrap_or(base.include_inline_hashes.unwrap_or(true));
let default_src = self
.default_src
.clone()
.unwrap_or(base.default_src.clone().unwrap_or("'self'".to_string()));
if !default_src.is_empty() {
out.push_str("default-src ");
out.push_str(default_src.as_str());
out.push_str("; ");
}
let mut script_src = self
.script_src
.clone()
.unwrap_or(base.script_src.clone().unwrap_or_default());
if include_inline_hashes
&& let Some(script_hashes) = inline_script_hashes
&& !script_hashes.is_empty()
{
if script_src.is_empty() {
script_src.push_str("'self'");
if has_wasm {
script_src.push_str(" 'wasm-unsafe-eval'");
}
}
for hash in script_hashes {
script_src.push_str(" '");
script_src.push_str(hash.as_str());
script_src.push('\'');
}
}
if let Some(script_urls) = script_urls {
if script_src.is_empty() {
script_src.push_str("'self'");
}
for script_url in script_urls {
script_src.push(' ');
script_src.push_str(&script_url);
}
}
if !script_src.is_empty() {
out.push_str("script-src ");
out.push_str(script_src.as_str());
out.push_str("; ");
}
let mut style_src = self
.style_src
.clone()
.unwrap_or(base.style_src.clone().unwrap_or_default());
if include_inline_hashes
&& let Some(style_hashes) = inline_style_hashes
&& !style_hashes.is_empty()
{
if style_src.is_empty() {
style_src.push_str("'self'");
}
for hash in style_hashes {
style_src.push_str(" '");
style_src.push_str(hash.as_str());
style_src.push('\'');
}
}
if !style_src.is_empty() {
out.push_str("style-src ");
out.push_str(style_src.as_str());
out.push_str("; ");
}
let font_src = self
.font_src
.clone()
.unwrap_or(base.font_src.clone().unwrap_or_default());
if !font_src.is_empty() {
out.push_str("font-src ");
out.push_str(font_src.as_str());
out.push_str("; ");
}
let img_src = self
.img_src
.clone()
.unwrap_or(base.img_src.clone().unwrap_or_default());
if !img_src.is_empty() {
out.push_str("img-src ");
out.push_str(img_src.as_str());
out.push_str("; ");
}
let frame_src = self
.frame_src
.clone()
.unwrap_or(base.frame_src.clone().unwrap_or_default());
if !frame_src.is_empty() {
out.push_str("frame-src ");
out.push_str(frame_src.as_str());
out.push_str("; ");
}
if secure {
out.push_str("upgrade-insecure-requests; ");
}
out.push_str("report-to csp");
out = out.trim().to_string();
out
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct LifecycleBeforeAfterScripts {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub before: Option<Vec<Vec<String>>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub after: Option<Vec<Vec<String>>>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TopLevelLifecycle {
pub before_all: Option<Vec<Vec<String>>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub build: Option<LifecycleBeforeAfterScripts>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum RedirectMethod {
Temporary,
Permanent,
}
impl Display for RedirectMethod {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Temporary => write!(f, "TEMPORARY"),
Self::Permanent => write!(f, "PERMANENT"),
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct HostRedirect {
pub from: String,
pub to: String,
pub method: RedirectMethod,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct RouteRedirect {
pub condition: String,
pub rule: (String, String),
pub method: RedirectMethod,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Redirects {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub host: Option<Vec<HostRedirect>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub route: Option<Vec<RouteRedirect>>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ProxyConfig {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub domain: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub port: Option<u16>,
pub target: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub middlewares: Option<Vec<String>>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum MiddlewareMechanism {
Request { endpoint: String },
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum MiddlewareValidationRule {
#[cfg_attr(feature = "utoipa", schema(no_recursion))]
Any(Vec<Box<MiddlewareValidationRule>>),
#[cfg_attr(feature = "utoipa", schema(no_recursion))]
All(Vec<Box<MiddlewareValidationRule>>),
#[cfg_attr(feature = "utoipa", schema(no_recursion))]
Not(Box<MiddlewareValidationRule>),
StatusCode(u16),
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, PartialOrd, PartialEq, Ord, Eq)]
pub enum MiddlewareValidationComponent {
Headers,
Path,
Query,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum MiddlewareOperation {
Validate {
rule: MiddlewareValidationRule,
components: BTreeSet<MiddlewareValidationComponent>,
},
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct MiddlewareConfig {
pub name: String,
pub operation: MiddlewareOperation,
pub mechanism: MiddlewareMechanism,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct OrdinaryConfig {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub lifecycle: Option<TopLevelLifecycle>,
pub domain: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub cnames: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub canonical: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub redirects: Option<Redirects>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub proxies: Option<Vec<ProxyConfig>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub middlewares: Option<Vec<MiddlewareConfig>>,
#[serde(skip)]
#[serde(default)]
pub internal_middlewares: Option<HashMap<String, MiddlewareConfig>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub contacts: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub hide_contacts: Option<bool>,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default = "OrdinaryConfig::default_storage_size")]
pub storage_size: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub default_timeout: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub csp: Option<HttpCsp>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub cors: Option<HttpCors>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub runtime: Option<RuntimeMode>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub hide_schema: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub client_events: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub port: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub redirect_port: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub logging: Option<LoggingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub error: Option<ErrorConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub auth: Option<AuthConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub globals: Option<Vec<Global>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub secrets: Option<Vec<Secret>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub flags: Option<Vec<Flag>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub content: Option<Content>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub models: Option<Vec<ModelConfig>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub integrations: Option<Vec<IntegrationConfig>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub actions: Option<Vec<ActionConfig>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub assets: Option<AssetsConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub fragments: Option<FragmentsConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub templates: Option<Vec<TemplateConfig>>,
}
impl OrdinaryConfig {
pub fn get(proj_path: impl AsRef<Path>, load_refs: bool) -> anyhow::Result<OrdinaryConfig> {
let path = proj_path.as_ref().join("ordinary.json");
let mut config_json = fs::read(&path)?;
let mut config = match simd_json::from_slice::<OrdinaryConfig>(&mut config_json) {
Ok(config) => config,
Err(err) => bail!("{}: {err}", path.display()),
};
if load_refs {
config.load_refs(proj_path.as_ref())?;
}
config.load_internal();
if let Some(content) = config.content.as_mut() {
content.definitions.sort_by_key(|d| d.idx);
}
if let Some(templates) = config.templates.as_mut() {
templates.sort_by_key(|t| t.idx);
}
if let Some(models) = config.models.as_mut() {
models.sort_by_key(|m| m.idx);
}
if let Some(actions) = config.actions.as_mut() {
actions.sort_by_key(|a| a.idx);
}
Ok(config)
}
pub fn write(&self, proj_path: &Path) -> anyhow::Result<()> {
use std::io::Write;
let ordinary_json = serde_json::to_string_pretty(self)?;
let mut file = fs::File::create(proj_path.join("ordinary.json"))?;
file.write_all(ordinary_json.as_bytes())?;
Ok(())
}
fn load_refs(&mut self, proj_path: &Path) -> anyhow::Result<()> {
if let Some(templates) = self.templates.as_mut() {
for base_template in templates {
if let Some(reference) = &base_template.r#ref {
let json_path = proj_path.join(reference);
let mut json_bytes = fs_err::read(json_path)?;
let mut ref_template: TemplateConfig =
simd_json::from_slice(json_bytes.as_mut_slice())?;
ref_template.load_managed();
ref_template.r#ref.clone_from(&base_template.r#ref);
if let Some(idx) = base_template.idx {
ref_template.idx = Some(idx);
}
if let Some(name) = &base_template.name {
ref_template.name = Some(name.clone());
}
if let Some(route) = &base_template.route {
ref_template.route = Some(route.clone());
}
if let Some(minify) = base_template.minify {
ref_template.minify = Some(minify);
}
if let Some(cache) = &base_template.cache {
ref_template.cache = Some(cache.clone());
}
if let Some(cors) = &base_template.cors {
ref_template.cors = Some(cors.clone());
}
if let Some(timeout) = base_template.timeout {
ref_template.timeout = Some(timeout);
}
*base_template = ref_template;
}
}
}
Ok(())
}
#[must_use]
pub fn content_map(&self) -> HashMap<String, ContentDefinition> {
let mut content_map = HashMap::new();
if let Some(content) = &self.content {
for content_def in &content.definitions {
content_map.insert(content_def.name.clone(), content_def.clone());
}
}
content_map
}
#[must_use]
pub fn model_map(&self) -> HashMap<String, ModelConfig> {
let mut model_map = HashMap::new();
if let Some(models) = &self.models {
for model_config in models {
model_map.insert(model_config.name.clone(), model_config.clone());
}
}
model_map
}
pub fn load_internal(&mut self) {
self.load_internal_middlewares();
self.load_internal_compression();
}
fn load_internal_middlewares(&mut self) {
if let Some(middlewares) = &self.middlewares {
let mut map = HashMap::new();
for middleware in middlewares {
map.insert(middleware.name.clone(), middleware.clone());
}
self.internal_middlewares = Some(map);
}
}
fn load_internal_compression(&mut self) {
if let Some(assets) = self.assets.as_mut()
&& let Some(precompression) = &assets.precompression
{
assets.internal_precompression = Some(precompression.get_list());
}
if let Some(templates) = self.templates.as_mut() {
for template in templates {
if let Some(cache) = template.cache.as_mut()
&& let Some(stored) = cache.stored.as_mut()
&& let Some(compression) = &stored.compression
{
stored.internal_compression = Some(compression.get_list());
}
}
}
}
#[must_use]
pub fn get_middlewares(&self, middleware_names: &Vec<String>) -> Option<Vec<MiddlewareConfig>> {
let mut middleware_configs = vec![];
if let Some(middleware_map) = &self.internal_middlewares {
for middleware in middleware_names {
if let Some(middleware_config) = middleware_map.get(middleware) {
middleware_configs.push(middleware_config.clone());
}
}
}
if !middleware_configs.is_empty() {
return Some(middleware_configs);
}
None
}
pub fn for_send(&self) -> anyhow::Result<OrdinaryConfig> {
let mut config = self.clone();
config.lifecycle = None;
if let Some(assets) = config.assets.as_mut() {
assets.dir_path = None;
}
if let Some(content) = config.content.as_mut() {
content.update = None;
for def in &mut content.definitions {
def.lifecycle = None;
}
}
if let Some(templates) = config.templates.as_mut() {
for template in templates {
template.r#ref = None;
template.build = None;
template.bin = None;
template.path = None;
template.minify = None;
}
}
if let Some(actions) = config.actions.as_mut() {
for action in actions {
action.dir_path = None;
}
}
Ok(config)
}
#[instrument(skip_all, err, level = "debug")]
pub fn validate(&self) -> anyhow::Result<()> {
validate(self)
}
#[must_use]
#[allow(clippy::unnecessary_wraps)]
pub fn default_storage_size() -> Option<u64> {
Some(5_000_000)
}
#[must_use]
pub fn has_ordinary_actions(&self) -> bool {
if let Some(actions) = &self.actions {
actions
.iter()
.find(|a| {
for trigger in &a.triggered_by {
if let ActionTrigger::Ordinary = trigger {
return true;
}
}
false
})
.is_some()
} else {
false
}
}
#[allow(clippy::too_many_lines)]
pub fn check_config_against_limits(
&self,
limits: &OrdinaryApiLimits,
privileged_domains: &HashSet<String>,
) -> anyhow::Result<()> {
check_config_against_limits(self, limits, privileged_domains)
}
pub fn exec_script(
proj_path: &Path,
argument: &Option<String>,
name: &str,
when: &str,
scripts: &Vec<Vec<String>>,
) -> anyhow::Result<()> {
let span = tracing::info_span!("lifecycle", %when, %name);
span.in_scope(|| {
exec_script(proj_path, argument, scripts)?;
anyhow::Ok(())
})
}
#[must_use]
pub fn get_next_template_idx(&self) -> u8 {
if let Some(templates) = &self.templates
&& let Some(last) = templates.last()
&& let Some(idx) = last.idx
{
idx + 1
} else {
0
}
}
pub fn check_template_name_exists(&self, name: &str) -> anyhow::Result<()> {
if let Some(templates) = &self.templates {
for template in templates {
if template.name.as_deref() == Some(name) {
bail!("template with name {name} already exists");
}
}
}
Ok(())
}
}
pub fn exec_script(
proj_path: &Path,
argument: &Option<String>,
scripts: &Vec<Vec<String>>,
) -> anyhow::Result<()> {
let curr_dir = env::current_dir()?;
env::set_current_dir(proj_path)?;
for script in scripts {
let mut script_iter = script.iter();
if let Some(command) = script_iter.next() {
let mut command_str = command.clone();
let mut command = Command::new(command);
for arg in script_iter {
write!(command_str, " {arg}")?;
command.arg(arg);
}
tracing::info!(cmd = %command_str, "exec");
let output = match &argument {
Some(arg) => command.arg(arg).output()?,
None => command.output()?,
};
if !output.status.success() {
let stderr = str::from_utf8(&output.stderr)?;
let stdout = str::from_utf8(&output.stdout)?;
tracing::error!(%stderr, %stdout, "failed");
bail!(stderr.to_string());
}
}
}
env::set_current_dir(curr_dir)?;
Ok(())
}