#[cfg(all(feature = "mlt", feature = "_tiles"))]
use mlt_core::encoder::EncoderConfig;
#[cfg(all(feature = "mlt", feature = "_tiles"))]
use serde::{Deserialize, Serialize};
#[cfg(all(feature = "mlt", feature = "_tiles"))]
use crate::config::file::UnrecognizedValues;
#[cfg(all(feature = "mlt", feature = "_tiles"))]
use crate::config::primitives::AutoOption;
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ProcessConfig {
#[cfg(all(feature = "mlt", feature = "_tiles"))]
pub convert_to_mlt: Option<MltProcessConfig>,
#[cfg(all(feature = "mlt", feature = "_tiles"))]
pub convert_to_mvt: Option<MvtProcessConfig>,
}
#[cfg(all(feature = "mlt", feature = "_tiles"))]
pub type MltProcessConfig = AutoOption<MltEncoderConfig>;
#[cfg(all(feature = "mlt", feature = "_tiles"))]
pub type MvtProcessConfig = AutoOption<MvtEncoderConfig>;
#[cfg(all(feature = "mlt", feature = "_tiles"))]
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "unstable-schemas", derive(schemars::JsonSchema))]
#[serde(transparent)]
pub struct MvtEncoderConfig(pub serde_json::Map<String, serde_json::Value>);
#[cfg(all(feature = "mlt", feature = "_tiles"))]
impl MvtEncoderConfig {
pub(crate) fn unrecognized_keys(&self) -> impl Iterator<Item = &str> {
self.0.keys().map(String::as_str)
}
}
#[cfg(all(feature = "mlt", feature = "_tiles"))]
#[serde_with::skip_serializing_none]
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "unstable-schemas", derive(schemars::JsonSchema))]
pub struct MltEncoderConfig {
pub tessellate: Option<bool>,
pub try_spatial_morton_sort: Option<bool>,
pub try_spatial_hilbert_sort: Option<bool>,
pub try_id_sort: Option<bool>,
pub allow_fsst: Option<bool>,
pub allow_fpf: Option<bool>,
pub allow_shared_dict: Option<bool>,
#[serde(flatten, skip_serializing)]
#[cfg_attr(feature = "unstable-schemas", schemars(skip))]
pub unrecognized: UnrecognizedValues,
}
#[cfg(all(feature = "mlt", feature = "_tiles"))]
impl MltEncoderConfig {
pub(crate) fn unrecognized_keys(&self) -> impl Iterator<Item = &str> {
self.unrecognized.keys().map(String::as_str)
}
}
#[cfg(all(feature = "mlt", feature = "_tiles"))]
impl From<MltEncoderConfig> for EncoderConfig {
fn from(src: MltEncoderConfig) -> Self {
let MltEncoderConfig {
tessellate,
try_spatial_morton_sort,
try_spatial_hilbert_sort,
try_id_sort,
allow_fsst,
allow_fpf,
allow_shared_dict,
unrecognized: _,
} = src;
Self {
tessellate: tessellate.unwrap_or(Self::default().tessellate),
try_spatial_morton_sort: try_spatial_morton_sort
.unwrap_or(Self::default().try_spatial_morton_sort),
try_spatial_hilbert_sort: try_spatial_hilbert_sort
.unwrap_or(Self::default().try_spatial_hilbert_sort),
try_id_sort: try_id_sort.unwrap_or(Self::default().try_id_sort),
allow_fsst: allow_fsst.unwrap_or(Self::default().allow_fsst),
allow_fpf: allow_fpf.unwrap_or(Self::default().allow_fpf),
allow_shared_dict: allow_shared_dict.unwrap_or(Self::default().allow_shared_dict),
}
}
}
#[must_use]
pub fn resolve_process_config(
global: &ProcessConfig,
source_type: &ProcessConfig,
per_source: &ProcessConfig,
) -> ProcessConfig {
let default = ProcessConfig::default();
if *per_source != default {
per_source.clone()
} else if *source_type != default {
source_type.clone()
} else {
global.clone()
}
}
#[cfg(test)]
mod tests {
#[cfg(all(feature = "mlt", feature = "_tiles"))]
use indoc::indoc;
use super::*;
#[cfg(all(feature = "mlt", feature = "_tiles"))]
#[test]
fn parse_mlt_auto_string() {
let cfg: MltProcessConfig = serde_yaml::from_str("auto").unwrap();
assert_eq!(cfg, MltProcessConfig::Auto);
}
#[cfg(all(feature = "mlt", feature = "_tiles"))]
#[test]
fn parse_mlt_explicit_empty() {
let cfg: MltProcessConfig = serde_yaml::from_str("{}").unwrap();
assert_eq!(cfg, MltProcessConfig::Explicit(MltEncoderConfig::default()));
}
#[cfg(all(feature = "mlt", feature = "_tiles"))]
#[test]
fn parse_mlt_explicit_with_overrides() {
let cfg: MltProcessConfig = serde_yaml::from_str(indoc! {"
tessellate: true
allow_fsst: false
"})
.unwrap();
assert_eq!(
cfg,
MltProcessConfig::Explicit(MltEncoderConfig {
tessellate: Some(true),
allow_fsst: Some(false),
..Default::default()
})
);
}
#[cfg(all(feature = "mlt", feature = "_tiles"))]
#[test]
fn serde_round_trip_auto() {
let cfg = MltProcessConfig::Auto;
let yaml = serde_yaml::to_string(&cfg).unwrap();
insta::assert_snapshot!(yaml, @"auto");
let parsed: MltProcessConfig = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(cfg, parsed);
}
#[cfg(all(feature = "mlt", feature = "_tiles"))]
#[test]
fn serde_round_trip_disabled() {
let cfg = MltProcessConfig::Disabled;
let yaml = serde_yaml::to_string(&cfg).unwrap();
insta::assert_snapshot!(yaml, @"disabled");
let parsed: MltProcessConfig = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(cfg, parsed);
}
#[cfg(all(feature = "mlt", feature = "_tiles"))]
#[test]
fn serde_round_trip_explicit() {
let cfg = MltProcessConfig::Explicit(MltEncoderConfig {
tessellate: Some(true),
..Default::default()
});
let yaml = serde_yaml::to_string(&cfg).unwrap();
let parsed: MltProcessConfig = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(cfg, parsed);
}
#[cfg(all(feature = "mlt", feature = "_tiles"))]
#[test]
fn parse_mlt_invalid_string() {
let result = serde_yaml::from_str::<MltProcessConfig>("invalid");
result.unwrap_err();
}
#[cfg(all(feature = "mlt", feature = "_tiles"))]
#[test]
fn parse_mlt_invalid_type() {
let result = serde_yaml::from_str::<MltProcessConfig>("123");
result.unwrap_err();
}
#[cfg(all(feature = "mlt", feature = "_tiles"))]
#[test]
fn render_failure_mlt_unknown_string() {
use crate::config::test_helpers::render_failure;
insta::assert_snapshot!(render_failure(indoc! {"
convert_to_mlt: atuo
"}), @r#"
× invalid value: string "atuo", expected a string ("auto", "enabled",
│ "disabled"), a boolean, or a map of settings
â•─[config.yaml:1:1]
1 │ convert_to_mlt: atuo
· ───────┬──────
· ╰── invalid value: string "atuo", expected a string ("auto", "enabled", "disabled"), a boolean, or a map of settings
╰────
"#);
}
#[cfg(all(feature = "mlt", feature = "_tiles"))]
#[test]
fn render_failure_mlt_integer() {
use crate::config::test_helpers::render_failure;
insta::assert_snapshot!(render_failure(indoc! {"
convert_to_mlt: 42
"}), @r#"
× invalid type: integer `42`, expected a string ("auto", "enabled",
│ "disabled"), a boolean, or a map of settings
â•─[config.yaml:1:1]
1 │ convert_to_mlt: 42
· ───────┬──────
· ╰── invalid type: integer `42`, expected a string ("auto", "enabled", "disabled"), a boolean, or a map of settings
╰────
"#);
}
#[cfg(all(feature = "mlt", feature = "_tiles"))]
#[test]
fn render_failure_mlt_nested_field_bad_type() {
use crate::config::test_helpers::render_failure;
insta::assert_snapshot!(render_failure(indoc! {"
convert_to_mlt:
tessellate: yes-please
"}), @r"
× invalid boolean
â•─[config.yaml:2:15]
1 │ convert_to_mlt:
2 │ tessellate: yes-please
· ─────┬────
· ╰── invalid boolean
╰────
");
}
#[cfg(all(feature = "mlt", feature = "_tiles"))]
#[test]
fn resolve_per_source_disabled_overrides_global_auto() {
let global = ProcessConfig {
convert_to_mlt: Some(MltProcessConfig::Auto),
convert_to_mvt: None,
};
let per_source = ProcessConfig {
convert_to_mlt: Some(MltProcessConfig::Disabled),
convert_to_mvt: None,
};
let resolved = resolve_process_config(&global, &ProcessConfig::default(), &per_source);
assert_eq!(resolved.convert_to_mlt, Some(MltProcessConfig::Disabled));
}
#[cfg(all(feature = "mlt", feature = "_tiles"))]
#[test]
fn resolve_per_source_overrides_all() {
let global = ProcessConfig {
convert_to_mlt: Some(MltProcessConfig::Auto),
convert_to_mvt: None,
};
let source_type = ProcessConfig {
convert_to_mlt: None,
convert_to_mvt: Some(MvtProcessConfig::Auto),
};
let per_source = ProcessConfig {
convert_to_mlt: Some(MltProcessConfig::Explicit(MltEncoderConfig {
tessellate: Some(true),
..Default::default()
})),
convert_to_mvt: None,
};
let resolved = resolve_process_config(&global, &source_type, &per_source);
assert_eq!(resolved, per_source);
}
#[cfg(all(feature = "mlt", feature = "_tiles"))]
#[test]
fn resolve_source_type_overrides_global() {
let global = ProcessConfig {
convert_to_mlt: Some(MltProcessConfig::Auto),
convert_to_mvt: None,
};
let source_type = ProcessConfig {
convert_to_mlt: None,
convert_to_mvt: Some(MvtProcessConfig::Auto),
};
let resolved = resolve_process_config(&global, &source_type, &ProcessConfig::default());
assert_eq!(resolved, source_type);
}
#[cfg(all(feature = "mlt", feature = "_tiles"))]
#[test]
fn resolve_global_used_as_fallback() {
let global = ProcessConfig {
convert_to_mlt: Some(MltProcessConfig::Auto),
convert_to_mvt: None,
};
let resolved = resolve_process_config(
&global,
&ProcessConfig::default(),
&ProcessConfig::default(),
);
assert_eq!(resolved, global);
}
#[test]
fn resolve_default_when_all_none() {
let resolved = resolve_process_config(
&ProcessConfig::default(),
&ProcessConfig::default(),
&ProcessConfig::default(),
);
assert_eq!(resolved, ProcessConfig::default());
}
#[cfg(all(feature = "mlt", feature = "_tiles"))]
#[test]
fn mlt_encoder_captures_unrecognized_keys() {
let cfg: MltProcessConfig = serde_yaml::from_str(indoc! {"
tessellate: true
unknown_knob: 42
another_typo: hi
"})
.unwrap();
let MltProcessConfig::Explicit(inner) = cfg else {
panic!("expected explicit MltEncoderConfig");
};
assert_eq!(inner.tessellate, Some(true));
let mut keys: Vec<&str> = inner.unrecognized_keys().collect();
keys.sort_unstable();
assert_eq!(keys, vec!["another_typo", "unknown_knob"]);
}
#[cfg(all(feature = "mlt", feature = "_tiles"))]
#[test]
fn mvt_encoder_captures_all_keys_as_unrecognized() {
let cfg: MvtProcessConfig = serde_yaml::from_str(indoc! {"
anything: 1
else: yes
"})
.unwrap();
let MvtProcessConfig::Explicit(inner) = cfg else {
panic!("expected explicit MvtEncoderConfig");
};
let mut keys: Vec<&str> = inner.unrecognized_keys().collect();
keys.sort_unstable();
assert_eq!(keys, vec!["anything", "else"]);
}
#[cfg(all(feature = "mlt", feature = "_tiles"))]
#[test]
fn parse_mvt_explicit_empty() {
let cfg: MvtProcessConfig = serde_yaml::from_str("{}").unwrap();
assert_eq!(cfg, MvtProcessConfig::Explicit(MvtEncoderConfig::default()));
}
#[cfg(all(feature = "mlt", feature = "pmtiles"))]
#[test]
fn pmt_config_propagates_mlt_unrecognized_keys() {
use crate::config::file::ConfigurationLivecycleHooks as _;
use crate::config::file::pmtiles::PmtConfig;
let cfg: PmtConfig = serde_yaml::from_str(indoc! {"
convert_to_mlt:
tessellate: true
bogus_option: 1
"})
.unwrap();
let keys = cfg.get_unrecognized_keys();
assert!(
keys.contains("convert_to_mlt.bogus_option"),
"expected convert_to_mlt.bogus_option in {keys:?}"
);
}
#[cfg(all(feature = "mlt", feature = "mbtiles"))]
#[test]
fn mbt_config_propagates_mvt_unrecognized_keys() {
use crate::config::file::ConfigurationLivecycleHooks as _;
use crate::config::file::mbtiles::MbtConfig;
let cfg: MbtConfig = serde_yaml::from_str(indoc! {"
convert_to_mvt:
not_a_real_setting: yes
"})
.unwrap();
let keys = cfg.get_unrecognized_keys();
assert!(
keys.contains("convert_to_mvt.not_a_real_setting"),
"expected convert_to_mvt.not_a_real_setting in {keys:?}"
);
}
#[cfg(all(feature = "mlt", feature = "pmtiles"))]
#[test]
fn finalize_collects_global_convert_to_mlt_unrecognized() {
use crate::config::file::Config;
let mut cfg: Config = serde_yaml::from_str(indoc! {"
pmtiles:
paths: /tmp/never-read.pmtiles
convert_to_mlt:
tessellate: true
definitely_a_typo: 1
"})
.unwrap();
let keys = cfg.finalize().expect("finalize should not error");
assert!(
keys.contains("convert_to_mlt.definitely_a_typo"),
"expected convert_to_mlt.definitely_a_typo in {keys:?}"
);
}
#[cfg(all(feature = "mlt", feature = "unstable-schemas"))]
#[test]
fn json_schema_matches_serde_format() {
let schema = serde_json::to_value(schemars::schema_for!(MltProcessConfig)).unwrap();
let one_of = schema
.get("oneOf")
.and_then(|v| v.as_array())
.expect("MltProcessConfig schema should be a `oneOf`");
assert_eq!(one_of.len(), 4, "schema: {schema}");
let mut saw_encoder_ref = false;
for entry in one_of {
if let Some(reference) = entry.get("$ref").and_then(|v| v.as_str())
&& reference.ends_with("/MltEncoderConfig")
{
saw_encoder_ref = true;
}
}
assert!(
saw_encoder_ref,
"expected $ref to MltEncoderConfig: {schema}"
);
}
}