use std::path::PathBuf;
use std::sync::LazyLock;
use clap::ValueEnum;
#[cfg(feature = "_tiles")]
use martin_core::tiles::BoxedSource;
use serde::{Deserialize, Serialize};
use tracing::{error, instrument, warn};
#[cfg(any(
feature = "pmtiles",
feature = "mbtiles",
feature = "unstable-cog",
feature = "styles",
feature = "sprites",
feature = "fonts",
))]
use crate::config::file::FileConfigEnum;
#[cfg(feature = "unstable-cog")]
use crate::config::file::cog::CogConfig;
#[cfg(feature = "fonts")]
use crate::config::file::fonts::FontConfig;
#[cfg(feature = "mbtiles")]
use crate::config::file::mbtiles::MbtConfig;
#[cfg(feature = "pmtiles")]
use crate::config::file::pmtiles::PmtConfig;
#[cfg(feature = "postgres")]
use crate::config::file::postgres::PostgresConfig;
#[cfg(all(feature = "mlt", feature = "_tiles"))]
use crate::config::file::process::{MltProcessConfig, MvtProcessConfig};
#[cfg(feature = "sprites")]
use crate::config::file::sprites::SpriteConfig;
use crate::config::file::srv::SrvConfig;
#[cfg(feature = "styles")]
use crate::config::file::styles::StyleConfig;
use crate::config::file::{GlobalCacheConfig, UnrecognizedValues};
#[cfg(feature = "postgres")]
use crate::config::primitives::OptOneMany;
#[cfg(feature = "_tiles")]
use crate::tile_source_manager::TileSourceManager;
use crate::{MartinError, MartinResult};
#[derive(thiserror::Error, Debug)]
pub enum TileSourceWarning {
#[error("Source {source_id}: {error}")]
SourceError { source_id: String, error: String },
#[error("Path {path}: {error}")]
PathError { path: PathBuf, error: String },
}
#[cfg(feature = "_tiles")]
pub type ResolutionResult = MartinResult<(Vec<BoxedSource>, Vec<TileSourceWarning>)>;
pub struct ServerState {
#[cfg(feature = "_tiles")]
pub tile_manager: TileSourceManager,
#[cfg(feature = "sprites")]
pub sprites: martin_core::sprites::SpriteSources,
#[cfg(feature = "sprites")]
pub sprite_cache: martin_core::sprites::OptSpriteCache,
#[cfg(feature = "fonts")]
pub fonts: martin_core::fonts::FontSources,
#[cfg(feature = "fonts")]
pub font_cache: martin_core::fonts::OptFontCache,
#[cfg(feature = "styles")]
pub styles: martin_core::styles::StyleSources,
}
#[serde_with::skip_serializing_none]
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "unstable-schemas", derive(schemars::JsonSchema))]
pub struct Config {
#[serde(default, skip_serializing_if = "GlobalCacheConfig::is_empty")]
#[cfg_attr(
feature = "unstable-schemas",
schemars(with = "crate::config::file::GlobalCacheConfigShape")
)]
pub cache: GlobalCacheConfig,
#[serde(default)]
pub on_invalid: Option<OnInvalid>,
#[serde(flatten)]
pub srv: SrvConfig,
#[cfg(feature = "postgres")]
#[serde(default, skip_serializing_if = "OptOneMany::is_none")]
pub postgres: OptOneMany<PostgresConfig>,
#[cfg(feature = "pmtiles")]
#[serde(default, skip_serializing_if = "FileConfigEnum::is_none")]
pub pmtiles: FileConfigEnum<PmtConfig>,
#[cfg(feature = "mbtiles")]
#[serde(default, skip_serializing_if = "FileConfigEnum::is_none")]
pub mbtiles: FileConfigEnum<MbtConfig>,
#[cfg(feature = "unstable-cog")]
#[serde(default, skip_serializing_if = "FileConfigEnum::is_none")]
pub cog: FileConfigEnum<CogConfig>,
#[cfg(feature = "sprites")]
#[serde(default, skip_serializing_if = "FileConfigEnum::is_none")]
pub sprites: SpriteConfig,
#[cfg(feature = "styles")]
#[serde(default, skip_serializing_if = "FileConfigEnum::is_none")]
pub styles: StyleConfig,
#[cfg(feature = "fonts")]
#[serde(default, skip_serializing_if = "FileConfigEnum::is_none")]
pub fonts: FontConfig,
#[cfg(all(feature = "mlt", feature = "_tiles"))]
#[serde(default)]
pub convert_to_mlt: Option<MltProcessConfig>,
#[cfg(all(feature = "mlt", feature = "_tiles"))]
#[serde(default)]
pub convert_to_mvt: Option<MvtProcessConfig>,
#[serde(flatten, skip_serializing)]
#[cfg_attr(feature = "unstable-schemas", schemars(skip))]
pub unrecognized: UnrecognizedValues,
}
#[derive(PartialEq, Eq, Debug, Clone, Copy, Default, Serialize, Deserialize, ValueEnum)]
#[cfg_attr(feature = "unstable-schemas", derive(schemars::JsonSchema))]
#[serde(rename_all = "lowercase")]
pub enum OnInvalid {
#[serde(
alias = "warnings",
alias = "warning",
alias = "continue",
alias = "ignore"
)]
Warn,
#[default]
Abort,
}
fn fmt_warnings(warnings: &[TileSourceWarning]) -> String {
warnings
.iter()
.map(|w| format!(" - {w}"))
.collect::<Vec<String>>()
.join("\n")
}
impl OnInvalid {
#[instrument(skip_all, fields(warnings.count = warnings.len()), err(Debug))]
pub fn handle_tile_warnings(self, warnings: &[TileSourceWarning]) -> MartinResult<()> {
if warnings.is_empty() {
return Ok(());
}
match warnings {
[warning] => match self {
Self::Warn => warn!("Tile source resolution warning: {warning}"),
Self::Abort => error!("Tile source resolution warning: {warning}"),
},
warnings => match self {
Self::Warn => warn!("Tile source resolutions:\n{}", fmt_warnings(warnings)),
Self::Abort => error!("Tile source resolutions:\n{}", fmt_warnings(warnings)),
},
}
match self {
Self::Abort => Err(MartinError::TileResolutionWarningsIssued),
Self::Warn => Ok(()),
}
}
}
pub fn parse_base_path(path: &str) -> MartinResult<String> {
if !path.starts_with('/') {
return Err(MartinError::BasePathError(path.to_string()));
}
if let Ok(uri) = path.parse::<actix_web::http::Uri>() {
return Ok(uri.path().trim_end_matches('/').to_string());
}
Err(MartinError::BasePathError(path.to_string()))
}
pub fn init_aws_lc_tls() {
use rustls::crypto::aws_lc_rs;
static INIT_TLS: LazyLock<()> = LazyLock::new(|| {
aws_lc_rs::default_provider()
.install_default()
.expect("Unable to init rustls: {e:?}");
});
*INIT_TLS;
}
#[cfg(test)]
mod tests {
use martin_core::CacheZoomRange;
use super::*;
use crate::MartinError;
use crate::config::file::CachePolicy;
use crate::logging::LogFormat;
#[test]
fn non_spanned_error_renders_as_json_envelope() {
let envelope = MartinError::BasePathError("not-a-path".to_string())
.render_diagnostic_with(LogFormat::Json);
let parsed: serde_json::Value =
serde_json::from_str(&envelope).unwrap_or_else(|e| panic!("not JSON: {e}\n{envelope}"));
let msg = parsed.get("message").and_then(|m| m.as_str()).unwrap_or("");
assert!(
msg.contains("not-a-path"),
"expected envelope to include the error message; got: {envelope}"
);
}
#[test]
fn parse_base_path_accepts_valid_paths() {
assert_eq!("", parse_base_path("/").unwrap());
assert_eq!("", parse_base_path("//").unwrap());
assert_eq!("/foo/bar", parse_base_path("/foo/bar").unwrap());
assert_eq!("/foo/bar", parse_base_path("/foo/bar/").unwrap());
}
#[test]
fn parse_base_path_rejects_invalid_paths() {
parse_base_path("").unwrap_err();
parse_base_path("foo/bar").unwrap_err();
}
#[test]
fn cache_disable_per_source() {
let policy: CachePolicy = serde_yaml::from_str("disable").unwrap();
assert_eq!(policy, CachePolicy::disabled());
for zoom in 0..=u8::MAX {
assert!(
!policy.zoom().contains(zoom),
"A disabled policy should never match any zoom level"
);
}
}
#[test]
fn cache_disable_per_source_ignores_global_defaults() {
let disabled = CachePolicy::disabled();
let defaults = CachePolicy::new(CacheZoomRange::new(Some(0), Some(20)));
let merged = disabled.or(defaults);
for zoom in 0..=u8::MAX {
assert!(!merged.zoom().contains(zoom));
}
}
#[test]
fn cache_disable_global_can_be_overridden_per_source() {
let source = CachePolicy::new(CacheZoomRange::new(Some(0), Some(10)));
let global_disabled = CachePolicy::disabled();
let merged = source.or(global_disabled);
assert!(merged.zoom().contains(0));
assert!(merged.zoom().contains(5));
assert!(merged.zoom().contains(10));
assert!(!merged.zoom().contains(11));
}
}