use std::collections::{BTreeMap, BTreeSet};
use futures::stream::{self, StreamExt as _};
use martin_core::tiles::BoxedSource;
#[cfg(feature = "_file_kinds")]
use crate::config::file::FileConfigSrc;
use crate::config::file::discovery::BuiltSource;
#[cfg(feature = "postgres")]
use crate::config::file::postgres::SourceSpec;
use crate::config::file::{MAX_CONCURRENT_SOURCE_INITS, ResolvedProcess, SourceBuildResult};
#[cfg(feature = "_file_kinds")]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum FileKind {
#[cfg(feature = "mbtiles")]
Mbtiles,
#[cfg(feature = "pmtiles")]
Pmtiles,
#[cfg(feature = "unstable-cog")]
Cog,
#[cfg(feature = "geojson")]
GeoJson,
}
#[derive(Clone, Debug)]
pub enum SourceProvenance {
#[cfg(feature = "postgres")]
Postgres {
connection_string: String,
spec: Box<SourceSpec>,
},
#[cfg(feature = "_file_kinds")]
File {
kind: FileKind,
src: FileConfigSrc,
},
}
#[derive(Debug)]
pub struct NewSource {
pub id: String,
pub source: SourceBuildResult<BoxedSource>,
pub process: ResolvedProcess,
pub provenance: Option<SourceProvenance>,
}
impl NewSource {
fn new(id: String, built: SourceBuildResult<BuiltSource>, process: ResolvedProcess) -> Self {
match built {
Ok(built) => Self {
id,
source: Ok(built.source),
process: built.process.unwrap_or(process),
provenance: built.provenance,
},
Err(e) => Self {
id,
source: Err(e),
process,
provenance: None,
},
}
}
}
#[derive(Debug, Ord, PartialOrd, PartialEq, Eq)]
pub struct DeletedSource {
pub id: String,
}
#[derive(Default, Debug)]
pub struct ReloadAdvisory {
pub additions: Vec<NewSource>,
pub updates: Vec<NewSource>,
pub removals: BTreeSet<DeletedSource>,
}
impl ReloadAdvisory {
pub async fn from_sets<F>(
previous_ids: &BTreeSet<String>,
next_ids: &BTreeSet<String>,
initializer: F,
process: ResolvedProcess,
) -> Self
where
F: AsyncFn(String) -> SourceBuildResult<BuiltSource>,
{
let removals = previous_ids
.difference(next_ids)
.map(|id| DeletedSource { id: id.clone() })
.collect();
let additions = build_all(
next_ids.difference(previous_ids).cloned(),
&initializer,
&process,
)
.await;
Self {
additions,
updates: Vec::new(),
removals,
}
}
pub async fn from_maps<F, V: Eq + Copy>(
previous_map: &BTreeMap<String, V>,
next_map: &BTreeMap<String, V>,
initializer: F,
process: ResolvedProcess,
) -> Self
where
F: AsyncFn(String) -> SourceBuildResult<BuiltSource>,
{
let removals = previous_map
.keys()
.filter(|id| !next_map.contains_key(*id))
.map(|id| DeletedSource { id: id.clone() })
.collect();
let mut to_update = Vec::new();
let mut to_add = Vec::new();
for (id, &next_version) in next_map {
match previous_map.get(id) {
Some(&prev_version) if next_version != prev_version => to_update.push(id.clone()),
None => to_add.push(id.clone()),
_ => {} }
}
Self {
updates: build_all(to_update, &initializer, &process).await,
additions: build_all(to_add, &initializer, &process).await,
removals,
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.additions.is_empty() && self.updates.is_empty() && self.removals.is_empty()
}
}
async fn build_all<F>(
ids: impl IntoIterator<Item = String>,
initializer: &F,
process: &ResolvedProcess,
) -> Vec<NewSource>
where
F: AsyncFn(String) -> SourceBuildResult<BuiltSource>,
{
stream::iter(ids)
.map(|id| async move {
let built = initializer(id.clone()).await;
NewSource::new(id, built, process.clone())
})
.buffered(MAX_CONCURRENT_SOURCE_INITS)
.collect()
.await
}
#[cfg(test)]
mod tests {
use async_trait::async_trait;
use insta::assert_yaml_snapshot;
use martin_core::CacheZoomRange;
use martin_core::tiles::{BoxedSource, MartinCoreResult, Source, UrlQuery};
use martin_tile_utils::{Encoding, Format, TileCoord, TileData, TileInfo};
use tilejson::{TileJSON, tilejson};
use super::*;
#[derive(Debug, Clone)]
struct TestSource {
id: String,
tj: TileJSON,
}
#[async_trait]
impl Source for TestSource {
fn get_id(&self) -> &str {
&self.id
}
fn get_tilejson(&self) -> &TileJSON {
&self.tj
}
fn get_tile_info(&self) -> TileInfo {
TileInfo::new(Format::Mvt, Encoding::Uncompressed)
}
fn clone_source(&self) -> BoxedSource {
Box::new(self.clone())
}
fn cache_zoom(&self) -> CacheZoomRange {
CacheZoomRange::default()
}
async fn get_tile(
&self,
_xyz: TileCoord,
_url_query: Option<&UrlQuery>,
) -> MartinCoreResult<TileData> {
Ok(vec![1, 2, 3])
}
}
async fn make_source(id: String) -> SourceBuildResult<BuiltSource> {
let source: BoxedSource = Box::new(TestSource {
id,
tj: tilejson! {
tilejson: "3.0.0".to_owned(),
tiles: vec!["https://maplibre.org".to_owned()],
attribution: String::new(),
name: "test_json".to_owned(),
scheme: "xyz".to_owned(),
},
});
Ok(source.into())
}
#[derive(serde::Serialize)]
struct AdvisorySnapshot {
additions: Vec<String>,
updates: Vec<String>,
removals: Vec<String>,
}
impl From<&ReloadAdvisory> for AdvisorySnapshot {
fn from(advisory: &ReloadAdvisory) -> Self {
Self {
additions: advisory.additions.iter().map(|s| s.id.clone()).collect(),
updates: advisory.updates.iter().map(|s| s.id.clone()).collect(),
removals: advisory.removals.iter().map(|s| s.id.clone()).collect(),
}
}
}
#[tokio::test]
async fn from_sets_empty_to_empty() {
let prev = BTreeSet::new();
let next = BTreeSet::new();
let advisory =
ReloadAdvisory::from_sets(&prev, &next, make_source, ResolvedProcess::default()).await;
assert_yaml_snapshot!(AdvisorySnapshot::from(&advisory), @"
additions: []
updates: []
removals: []
");
}
#[tokio::test]
async fn from_sets_additions_only() {
let prev = BTreeSet::new();
let next: BTreeSet<String> = ["a", "b"].into_iter().map(String::from).collect();
let advisory =
ReloadAdvisory::from_sets(&prev, &next, make_source, ResolvedProcess::default()).await;
assert_yaml_snapshot!(AdvisorySnapshot::from(&advisory), @"
additions:
- a
- b
updates: []
removals: []
");
}
#[tokio::test]
async fn from_sets_removals_only() {
let prev: BTreeSet<String> = ["a", "b"].into_iter().map(String::from).collect();
let next = BTreeSet::new();
let advisory =
ReloadAdvisory::from_sets(&prev, &next, make_source, ResolvedProcess::default()).await;
assert_yaml_snapshot!(AdvisorySnapshot::from(&advisory), @"
additions: []
updates: []
removals:
- a
- b
");
}
#[tokio::test]
async fn from_sets_mixed() {
let prev: BTreeSet<String> = ["a", "b", "c"].into_iter().map(String::from).collect();
let next: BTreeSet<String> = ["b", "c", "d"].into_iter().map(String::from).collect();
let advisory =
ReloadAdvisory::from_sets(&prev, &next, make_source, ResolvedProcess::default()).await;
assert_yaml_snapshot!(AdvisorySnapshot::from(&advisory), @"
additions:
- d
updates: []
removals:
- a
");
}
#[tokio::test]
async fn from_maps_additions_and_removals() {
let prev: BTreeMap<String, u64> = [("a".into(), 1), ("b".into(), 2)].into_iter().collect();
let next: BTreeMap<String, u64> = [("b".into(), 2), ("c".into(), 3)].into_iter().collect();
let advisory =
ReloadAdvisory::from_maps(&prev, &next, make_source, ResolvedProcess::default()).await;
assert_yaml_snapshot!(AdvisorySnapshot::from(&advisory), @"
additions:
- c
updates: []
removals:
- a
");
}
#[tokio::test]
async fn from_maps_version_update() {
let prev: BTreeMap<String, u64> = [("a".into(), 1), ("b".into(), 2)].into_iter().collect();
let next: BTreeMap<String, u64> = [("a".into(), 1), ("b".into(), 5)].into_iter().collect();
let advisory =
ReloadAdvisory::from_maps(&prev, &next, make_source, ResolvedProcess::default()).await;
assert_yaml_snapshot!(AdvisorySnapshot::from(&advisory), @"
additions: []
updates:
- b
removals: []
");
}
#[tokio::test]
async fn from_maps_all_changes() {
let prev: BTreeMap<String, u64> = [("a".into(), 1), ("b".into(), 2), ("c".into(), 3)]
.into_iter()
.collect();
let next: BTreeMap<String, u64> = [("b".into(), 9), ("c".into(), 3), ("d".into(), 4)]
.into_iter()
.collect();
let advisory =
ReloadAdvisory::from_maps(&prev, &next, make_source, ResolvedProcess::default()).await;
assert_yaml_snapshot!(AdvisorySnapshot::from(&advisory), @"
additions:
- d
updates:
- b
removals:
- a
");
}
}