diff --git a/Cargo.lock b/Cargo.lock index 0ae418ec..75ed2c21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2842,9 +2842,9 @@ checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "socket2" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", "windows-sys 0.48.0", diff --git a/martin/src/args/pg.rs b/martin/src/args/pg.rs index f77c0671..0ca12136 100644 --- a/martin/src/args/pg.rs +++ b/martin/src/args/pg.rs @@ -4,7 +4,7 @@ use crate::args::connections::Arguments; use crate::args::connections::State::{Ignore, Take}; use crate::args::environment::Env; use crate::pg::{PgConfig, PgSslCerts, POOL_SIZE_DEFAULT}; -use crate::utils::OneOrMany; +use crate::utils::{OptBoolObj, OptOneMany}; #[derive(clap::Args, Debug, PartialEq, Default)] #[command(about, version)] @@ -30,7 +30,7 @@ impl PgArgs { self, cli_strings: &mut Arguments, env: &impl Env<'a>, - ) -> Option> { + ) -> OptOneMany { let connections = Self::extract_conn_strings(cli_strings, env); let default_srid = self.get_default_srid(env); let certs = self.get_certs(env); @@ -48,20 +48,20 @@ impl PgArgs { }, max_feature_count: self.max_feature_count, pool_size: self.pool_size, - auto_publish: None, + auto_publish: OptBoolObj::NoValue, tables: None, functions: None, }) .collect(); match results.len() { - 0 => None, - 1 => Some(OneOrMany::One(results.into_iter().next().unwrap())), - _ => Some(OneOrMany::Many(results)), + 0 => OptOneMany::NoVals, + 1 => OptOneMany::One(results.into_iter().next().unwrap()), + _ => OptOneMany::Many(results), } } - pub fn override_config<'a>(self, pg_config: &mut OneOrMany, env: &impl Env<'a>) { + pub fn override_config<'a>(self, pg_config: &mut OptOneMany, env: &impl Env<'a>) { if self.default_srid.is_some() { info!("Overriding configured default SRID to {} on all Postgres connections because of a CLI parameter", self.default_srid.unwrap()); pg_config.iter_mut().for_each(|c| { @@ -224,10 +224,10 @@ mod tests { let config = PgArgs::default().into_config(&mut args, &FauxEnv::default()); assert_eq!( config, - Some(OneOrMany::One(PgConfig { + OptOneMany::One(PgConfig { connection_string: some("postgres://localhost:5432"), ..Default::default() - })) + }) ); assert!(args.check().is_ok()); } @@ -248,7 +248,7 @@ mod tests { let config = PgArgs::default().into_config(&mut args, &env); assert_eq!( config, - Some(OneOrMany::One(PgConfig { + OptOneMany::One(PgConfig { connection_string: some("postgres://localhost:5432"), default_srid: Some(10), ssl_certificates: PgSslCerts { @@ -256,7 +256,7 @@ mod tests { ..Default::default() }, ..Default::default() - })) + }) ); assert!(args.check().is_ok()); } @@ -282,7 +282,7 @@ mod tests { let config = pg_args.into_config(&mut args, &env); assert_eq!( config, - Some(OneOrMany::One(PgConfig { + OptOneMany::One(PgConfig { connection_string: some("postgres://localhost:5432"), default_srid: Some(20), ssl_certificates: PgSslCerts { @@ -291,7 +291,7 @@ mod tests { ssl_root_cert: Some(PathBuf::from("root")), }, ..Default::default() - })) + }) ); assert!(args.check().is_ok()); } diff --git a/martin/src/args/root.rs b/martin/src/args/root.rs index 1201279b..79a4846d 100644 --- a/martin/src/args/root.rs +++ b/martin/src/args/root.rs @@ -62,11 +62,11 @@ impl Args { let mut cli_strings = Arguments::new(self.meta.connection); let pg_args = self.pg.unwrap_or_default(); - if let Some(pg_config) = &mut config.postgres { - // config was loaded from a file, we can only apply a few CLI overrides to it - pg_args.override_config(pg_config, env); - } else { + if config.postgres.is_none() { config.postgres = pg_args.into_config(&mut cli_strings, env); + } else { + // config was loaded from a file, we can only apply a few CLI overrides to it + pg_args.override_config(&mut config.postgres, env); } if !cli_strings.is_empty() { @@ -85,7 +85,7 @@ impl Args { } } -pub fn parse_file_args(cli_strings: &mut Arguments, extension: &str) -> Option { +pub fn parse_file_args(cli_strings: &mut Arguments, extension: &str) -> FileConfigEnum { let paths = cli_strings.process(|v| match PathBuf::try_from(v) { Ok(v) => { if v.is_dir() { @@ -107,7 +107,7 @@ mod tests { use super::*; use crate::pg::PgConfig; use crate::test_utils::{some, FauxEnv}; - use crate::utils::OneOrMany; + use crate::utils::OptOneMany; fn parse(args: &[&str]) -> Result<(Config, MetaArgs)> { let args = Args::parse_from(args); @@ -143,10 +143,10 @@ mod tests { let args = parse(&["martin", "postgres://connection"]).unwrap(); let cfg = Config { - postgres: Some(OneOrMany::One(PgConfig { + postgres: OptOneMany::One(PgConfig { connection_string: some("postgres://connection"), ..Default::default() - })), + }), ..Default::default() }; let meta = MetaArgs { diff --git a/martin/src/config.rs b/martin/src/config.rs index 5856bed2..90005d03 100644 --- a/martin/src/config.rs +++ b/martin/src/config.rs @@ -17,7 +17,7 @@ use crate::source::{TileInfoSources, TileSources}; use crate::sprites::SpriteSources; use crate::srv::SrvConfig; use crate::Error::{ConfigLoadError, ConfigParseError, NoSources}; -use crate::{IdResolver, OneOrMany, Result}; +use crate::{IdResolver, OptOneMany, Result}; pub type UnrecognizedValues = HashMap; @@ -31,17 +31,17 @@ pub struct Config { #[serde(flatten)] pub srv: SrvConfig, - #[serde(skip_serializing_if = "Option::is_none")] - pub postgres: Option>, + #[serde(default, skip_serializing_if = "OptOneMany::is_none")] + pub postgres: OptOneMany, - #[serde(skip_serializing_if = "Option::is_none")] - pub pmtiles: Option, + #[serde(default, skip_serializing_if = "FileConfigEnum::is_none")] + pub pmtiles: FileConfigEnum, - #[serde(skip_serializing_if = "Option::is_none")] - pub mbtiles: Option, + #[serde(default, skip_serializing_if = "FileConfigEnum::is_none")] + pub mbtiles: FileConfigEnum, - #[serde(skip_serializing_if = "Option::is_none")] - pub sprites: Option, + #[serde(default, skip_serializing_if = "FileConfigEnum::is_none")] + pub sprites: FileConfigEnum, #[serde(flatten)] pub unrecognized: UnrecognizedValues, @@ -53,40 +53,22 @@ impl Config { let mut res = UnrecognizedValues::new(); copy_unrecognized_config(&mut res, "", &self.unrecognized); - let mut any = if let Some(pg) = &mut self.postgres { - for pg in pg.iter_mut() { - res.extend(pg.finalize()?); - } - !pg.is_empty() - } else { - false - }; + for pg in self.postgres.iter_mut() { + res.extend(pg.finalize()?); + } - any |= if let Some(cfg) = &mut self.pmtiles { - res.extend(cfg.finalize("pmtiles.")?); - !cfg.is_empty() - } else { - false - }; + res.extend(self.pmtiles.finalize("pmtiles.")?); + res.extend(self.mbtiles.finalize("mbtiles.")?); + res.extend(self.sprites.finalize("sprites.")?); - any |= if let Some(cfg) = &mut self.mbtiles { - res.extend(cfg.finalize("mbtiles.")?); - !cfg.is_empty() - } else { - false - }; - - any |= if let Some(cfg) = &mut self.sprites { - res.extend(cfg.finalize("sprites.")?); - !cfg.is_empty() - } else { - false - }; - - if any { - Ok(res) - } else { + if self.postgres.is_empty() + && self.pmtiles.is_empty() + && self.mbtiles.is_empty() + && self.sprites.is_empty() + { Err(NoSources) + } else { + Ok(res) } } @@ -102,18 +84,16 @@ impl Config { let create_mbt_src = &mut MbtSource::new_box; let mut sources: Vec>>>> = Vec::new(); - if let Some(v) = self.postgres.as_mut() { - for s in v.iter_mut() { - sources.push(Box::pin(s.resolve(idr.clone()))); - } + for s in self.postgres.iter_mut() { + sources.push(Box::pin(s.resolve(idr.clone()))); } - if self.pmtiles.is_some() { + if !self.pmtiles.is_empty() { let val = resolve_files(&mut self.pmtiles, idr.clone(), "pmtiles", create_pmt_src); sources.push(Box::pin(val)); } - if self.mbtiles.is_some() { + if !self.mbtiles.is_empty() { let val = resolve_files(&mut self.mbtiles, idr.clone(), "mbtiles", create_mbt_src); sources.push(Box::pin(val)); } diff --git a/martin/src/file_config.rs b/martin/src/file_config.rs index 9cb55a35..62d8ab30 100644 --- a/martin/src/file_config.rs +++ b/martin/src/file_config.rs @@ -10,8 +10,8 @@ use serde::{Deserialize, Serialize}; use crate::config::{copy_unrecognized_config, UnrecognizedValues}; use crate::file_config::FileError::{InvalidFilePath, InvalidSourceFilePath, IoError}; use crate::source::{Source, TileInfoSources}; -use crate::utils::{sorted_opt_map, Error, IdResolver, OneOrMany}; -use crate::OneOrMany::{Many, One}; +use crate::utils::{sorted_opt_map, Error, IdResolver, OptOneMany}; +use crate::OptOneMany::{Many, One}; #[derive(thiserror::Error, Debug)] pub enum FileError { @@ -31,9 +31,11 @@ pub enum FileError { AquireConnError(String), } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] #[serde(untagged)] pub enum FileConfigEnum { + #[default] + None, Path(PathBuf), Paths(Vec), Config(FileConfig), @@ -41,7 +43,7 @@ pub enum FileConfigEnum { impl FileConfigEnum { #[must_use] - pub fn new(paths: Vec) -> Option { + pub fn new(paths: Vec) -> FileConfigEnum { Self::new_extended(paths, HashMap::new(), UnrecognizedValues::new()) } @@ -50,46 +52,70 @@ impl FileConfigEnum { paths: Vec, configs: HashMap, unrecognized: UnrecognizedValues, - ) -> Option { + ) -> FileConfigEnum { if configs.is_empty() && unrecognized.is_empty() { match paths.len() { - 0 => None, - 1 => Some(FileConfigEnum::Path(paths.into_iter().next().unwrap())), - _ => Some(FileConfigEnum::Paths(paths)), + 0 => FileConfigEnum::None, + 1 => FileConfigEnum::Path(paths.into_iter().next().unwrap()), + _ => FileConfigEnum::Paths(paths), } } else { - Some(FileConfigEnum::Config(FileConfig { - paths: OneOrMany::new_opt(paths), + FileConfigEnum::Config(FileConfig { + paths: OptOneMany::new(paths), sources: if configs.is_empty() { None } else { Some(configs) }, unrecognized, - })) + }) } } - pub fn extract_file_config(&mut self) -> FileConfig { + #[must_use] + pub fn is_none(&self) -> bool { + matches!(self, Self::None) + } + + #[must_use] + pub fn is_empty(&self) -> bool { match self { - FileConfigEnum::Path(path) => FileConfig { - paths: Some(One(mem::take(path))), - ..FileConfig::default() - }, - FileConfigEnum::Paths(paths) => FileConfig { - paths: Some(Many(mem::take(paths))), - ..Default::default() - }, - FileConfigEnum::Config(cfg) => mem::take(cfg), + Self::None => true, + Self::Path(_) => false, + Self::Paths(v) => v.is_empty(), + Self::Config(c) => c.is_empty(), } } + + pub fn extract_file_config(&mut self) -> Option { + match self { + FileConfigEnum::None => None, + FileConfigEnum::Path(path) => Some(FileConfig { + paths: One(mem::take(path)), + ..FileConfig::default() + }), + FileConfigEnum::Paths(paths) => Some(FileConfig { + paths: Many(mem::take(paths)), + ..Default::default() + }), + FileConfigEnum::Config(cfg) => Some(mem::take(cfg)), + } + } + + pub fn finalize(&self, prefix: &str) -> Result { + let mut res = UnrecognizedValues::new(); + if let Self::Config(cfg) = self { + copy_unrecognized_config(&mut res, prefix, &cfg.unrecognized); + } + Ok(res) + } } #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct FileConfig { /// A list of file paths - #[serde(skip_serializing_if = "Option::is_none")] - pub paths: Option>, + #[serde(default, skip_serializing_if = "OptOneMany::is_none")] + pub paths: OptOneMany, /// A map of source IDs to file paths or config objects #[serde(skip_serializing_if = "Option::is_none")] #[serde(serialize_with = "sorted_opt_map")] @@ -128,27 +154,8 @@ pub struct FileConfigSource { pub path: PathBuf, } -impl FileConfigEnum { - pub fn finalize(&self, prefix: &str) -> Result { - let mut res = UnrecognizedValues::new(); - if let Self::Config(cfg) = self { - copy_unrecognized_config(&mut res, prefix, &cfg.unrecognized); - } - Ok(res) - } - - #[must_use] - pub fn is_empty(&self) -> bool { - match self { - Self::Path(_) => false, - Self::Paths(v) => v.is_empty(), - Self::Config(c) => c.is_empty(), - } - } -} - pub async fn resolve_files( - config: &mut Option, + config: &mut FileConfigEnum, idr: IdResolver, extension: &str, create_source: &mut impl FnMut(String, PathBuf) -> Fut, @@ -162,7 +169,7 @@ where } async fn resolve_int( - config: &mut Option, + config: &mut FileConfigEnum, idr: IdResolver, extension: &str, create_source: &mut impl FnMut(String, PathBuf) -> Fut, @@ -170,10 +177,9 @@ async fn resolve_int( where Fut: Future, FileError>>, { - let Some(cfg) = config else { + let Some(cfg) = config.extract_file_config() else { return Ok(TileInfoSources::default()); }; - let cfg = cfg.extract_file_config(); let mut results = TileInfoSources::default(); let mut configs = HashMap::new(); @@ -202,50 +208,47 @@ where } } - if let Some(paths) = cfg.paths { - for path in paths { - let is_dir = path.is_dir(); - let dir_files = if is_dir { - // directories will be kept in the config just in case there are new files - directories.push(path.clone()); - path.read_dir() - .map_err(|e| IoError(e, path.clone()))? - .filter_map(Result::ok) - .filter(|f| { - f.path().extension().filter(|e| *e == extension).is_some() - && f.path().is_file() - }) - .map(|f| f.path()) - .collect() - } else if path.is_file() { - vec![path] - } else { - return Err(InvalidFilePath(path.canonicalize().unwrap_or(path))); - }; - for path in dir_files { - let can = path.canonicalize().map_err(|e| IoError(e, path.clone()))?; - if files.contains(&can) { - if !is_dir { - warn!("Ignoring duplicate MBTiles path: {}", can.display()); - } - continue; + for path in cfg.paths { + let is_dir = path.is_dir(); + let dir_files = if is_dir { + // directories will be kept in the config just in case there are new files + directories.push(path.clone()); + path.read_dir() + .map_err(|e| IoError(e, path.clone()))? + .filter_map(Result::ok) + .filter(|f| { + f.path().extension().filter(|e| *e == extension).is_some() && f.path().is_file() + }) + .map(|f| f.path()) + .collect() + } else if path.is_file() { + vec![path] + } else { + return Err(InvalidFilePath(path.canonicalize().unwrap_or(path))); + }; + for path in dir_files { + let can = path.canonicalize().map_err(|e| IoError(e, path.clone()))?; + if files.contains(&can) { + if !is_dir { + warn!("Ignoring duplicate MBTiles path: {}", can.display()); } - let id = path.file_stem().map_or_else( - || "_unknown".to_string(), - |s| s.to_string_lossy().to_string(), - ); - let source = FileConfigSrc::Path(path); - let id = idr.resolve(&id, can.to_string_lossy().to_string()); - info!("Configured source {id} from {}", can.display()); - files.insert(can); - configs.insert(id.clone(), source.clone()); - - let path = match source { - FileConfigSrc::Obj(pmt) => pmt.path, - FileConfigSrc::Path(path) => path, - }; - results.push(create_source(id, path).await?); + continue; } + let id = path.file_stem().map_or_else( + || "_unknown".to_string(), + |s| s.to_string_lossy().to_string(), + ); + let source = FileConfigSrc::Path(path); + let id = idr.resolve(&id, can.to_string_lossy().to_string()); + info!("Configured source {id} from {}", can.display()); + files.insert(can); + configs.insert(id.clone(), source.clone()); + + let path = match source { + FileConfigSrc::Obj(pmt) => pmt.path, + FileConfigSrc::Path(path) => path, + }; + results.push(create_source(id, path).await?); } } @@ -280,7 +283,7 @@ mod tests { let FileConfigEnum::Config(cfg) = cfg else { panic!(); }; - let paths = cfg.paths.clone().unwrap().into_iter().collect::>(); + let paths = cfg.paths.clone().into_iter().collect::>(); assert_eq!( paths, vec![ diff --git a/martin/src/lib.rs b/martin/src/lib.rs index 6915d051..8903799d 100644 --- a/martin/src/lib.rs +++ b/martin/src/lib.rs @@ -31,7 +31,7 @@ pub use crate::args::Env; pub use crate::config::{read_config, Config, ServerState}; pub use crate::source::Source; pub use crate::utils::{ - decode_brotli, decode_gzip, BoolOrObject, Error, IdResolver, OneOrMany, Result, + decode_brotli, decode_gzip, Error, IdResolver, OptBoolObj, OptOneMany, Result, }; // Ensure README.md contains valid code diff --git a/martin/src/pg/config.rs b/martin/src/pg/config.rs index 1934756c..51de0a39 100644 --- a/martin/src/pg/config.rs +++ b/martin/src/pg/config.rs @@ -11,7 +11,7 @@ use crate::pg::config_table::TableInfoSources; use crate::pg::configurator::PgBuilder; use crate::pg::Result; use crate::source::TileInfoSources; -use crate::utils::{on_slow, sorted_opt_map, BoolOrObject, IdResolver, OneOrMany}; +use crate::utils::{on_slow, sorted_opt_map, IdResolver, OptBoolObj, OptOneMany}; pub trait PgInfo { fn format_id(&self) -> String; @@ -47,8 +47,8 @@ pub struct PgConfig { pub max_feature_count: Option, #[serde(skip_serializing_if = "Option::is_none")] pub pool_size: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub auto_publish: Option>, + #[serde(default, skip_serializing_if = "OptBoolObj::is_none")] + pub auto_publish: OptBoolObj, #[serde(skip_serializing_if = "Option::is_none")] #[serde(serialize_with = "sorted_opt_map")] pub tables: Option, @@ -59,29 +59,29 @@ pub struct PgConfig { #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct PgCfgPublish { - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "OptOneMany::is_none")] #[serde(alias = "from_schema")] - pub from_schemas: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub tables: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub functions: Option>, + pub from_schemas: OptOneMany, + #[serde(default, skip_serializing_if = "OptBoolObj::is_none")] + pub tables: OptBoolObj, + #[serde(default, skip_serializing_if = "OptBoolObj::is_none")] + pub functions: OptBoolObj, } #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -pub struct PgCfgPublishType { - #[serde(skip_serializing_if = "Option::is_none")] +pub struct PgCfgPublishTables { + #[serde(default, skip_serializing_if = "OptOneMany::is_none")] #[serde(alias = "from_schema")] - pub from_schemas: Option>, + pub from_schemas: OptOneMany, #[serde(skip_serializing_if = "Option::is_none")] #[serde(alias = "id_format")] pub source_id_format: Option, /// A table column to use as the feature ID /// If a table has no column with this name, `id_column` will not be set for that table. /// If a list of strings is given, the first found column will be treated as a feature ID. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "OptOneMany::is_none")] #[serde(alias = "id_column")] - pub id_columns: Option>, + pub id_columns: OptOneMany, #[serde(skip_serializing_if = "Option::is_none")] pub clip_geom: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -90,6 +90,16 @@ pub struct PgCfgPublishType { pub extent: Option, } +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct PgCfgPublishFuncs { + #[serde(default, skip_serializing_if = "OptOneMany::is_none")] + #[serde(alias = "from_schema")] + pub from_schemas: OptOneMany, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(alias = "id_format")] + pub source_id_format: Option, +} + impl PgConfig { /// Apply defaults to the config, and validate if there is a connection string pub fn finalize(&mut self) -> Result { @@ -105,7 +115,7 @@ impl PgConfig { } } if self.tables.is_none() && self.functions.is_none() && self.auto_publish.is_none() { - self.auto_publish = Some(BoolOrObject::Bool(true)); + self.auto_publish = OptBoolObj::Bool(true); } Ok(res) @@ -143,7 +153,7 @@ mod tests { use crate::pg::config_function::FunctionInfo; use crate::pg::config_table::TableInfo; use crate::test_utils::some; - use crate::utils::OneOrMany::{Many, One}; + use crate::utils::OptOneMany::{Many, One}; #[test] fn parse_pg_one() { @@ -153,11 +163,11 @@ mod tests { connection_string: 'postgresql://postgres@localhost/db' "}, &Config { - postgres: Some(One(PgConfig { + postgres: One(PgConfig { connection_string: some("postgresql://postgres@localhost/db"), - auto_publish: Some(BoolOrObject::Bool(true)), + auto_publish: OptBoolObj::Bool(true), ..Default::default() - })), + }), ..Default::default() }, ); @@ -172,18 +182,18 @@ mod tests { - connection_string: 'postgresql://postgres@localhost:5433/db' "}, &Config { - postgres: Some(Many(vec![ + postgres: Many(vec![ PgConfig { connection_string: some("postgres://postgres@localhost:5432/db"), - auto_publish: Some(BoolOrObject::Bool(true)), + auto_publish: OptBoolObj::Bool(true), ..Default::default() }, PgConfig { connection_string: some("postgresql://postgres@localhost:5433/db"), - auto_publish: Some(BoolOrObject::Bool(true)), + auto_publish: OptBoolObj::Bool(true), ..Default::default() }, - ])), + ]), ..Default::default() }, ); @@ -225,7 +235,7 @@ mod tests { bounds: [-180.0, -90.0, 180.0, 90.0] "}, &Config { - postgres: Some(One(PgConfig { + postgres: One(PgConfig { connection_string: some("postgres://postgres@localhost:5432/db"), default_srid: Some(4326), pool_size: Some(20), @@ -262,7 +272,7 @@ mod tests { ), )])), ..Default::default() - })), + }), ..Default::default() }, ); diff --git a/martin/src/pg/configurator.rs b/martin/src/pg/configurator.rs old mode 100755 new mode 100644 index 0cd51d61..7a67a1fc --- a/martin/src/pg/configurator.rs +++ b/martin/src/pg/configurator.rs @@ -16,20 +16,42 @@ use crate::pg::table_source::{ }; use crate::pg::utils::{find_info, find_kv_ignore_case, normalize_key, InfoMap}; use crate::pg::PgError::InvalidTableExtent; -use crate::pg::Result; +use crate::pg::{PgCfgPublish, PgCfgPublishFuncs, Result}; use crate::source::TileInfoSources; -use crate::utils::{BoolOrObject, IdResolver, OneOrMany}; +use crate::utils::IdResolver; +use crate::utils::OptOneMany::NoVals; +use crate::OptBoolObj::{Bool, NoValue, Object}; pub type SqlFuncInfoMapMap = InfoMap>; pub type SqlTableInfoMapMapMap = InfoMap>>; #[derive(Debug, PartialEq)] -pub struct PgBuilderAuto { - source_id_format: String, +#[cfg_attr(test, derive(serde::Serialize))] +pub struct PgBuilderFuncs { + #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))] schemas: Option>, + source_id_format: String, +} + +#[derive(Debug, Default, PartialEq)] +#[cfg_attr(test, derive(serde::Serialize))] +pub struct PgBuilderTables { + #[cfg_attr( + test, + serde( + skip_serializing_if = "Option::is_none", + serialize_with = "crate::utils::sorted_opt_set" + ) + )] + schemas: Option>, + source_id_format: String, + #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))] id_columns: Option>, + #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))] clip_geom: Option, + #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))] buffer: Option, + #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))] extent: Option, } @@ -39,17 +61,39 @@ pub struct PgBuilder { default_srid: Option, disable_bounds: bool, max_feature_count: Option, - auto_functions: Option, - auto_tables: Option, + auto_functions: Option, + auto_tables: Option, id_resolver: IdResolver, tables: TableInfoSources, functions: FuncInfoSources, } +/// Combine `from_schema` field from the `config.auto_publish` and `config.auto_publish.tables/functions` +macro_rules! get_auto_schemas { + ($config:expr, $typ:ident) => { + if let Object(v) = &$config.auto_publish { + match (&v.from_schemas, &v.$typ) { + (NoVals, NoValue | Bool(_)) => None, + (v, NoValue | Bool(_)) => v.opt_iter().map(|v| v.cloned().collect()), + (NoVals, Object(v)) => v.from_schemas.opt_iter().map(|v| v.cloned().collect()), + (v, Object(v2)) => { + let mut vals: HashSet<_> = v.iter().cloned().collect(); + vals.extend(v2.from_schemas.iter().cloned()); + Some(vals) + } + } + } else { + None + } + }; +} + impl PgBuilder { pub async fn new(config: &PgConfig, id_resolver: IdResolver) -> Result { let pool = PgPool::new(config).await?; + let (auto_tables, auto_functions) = calc_auto(config); + Ok(Self { pool, default_srid: config.default_srid, @@ -58,8 +102,8 @@ impl PgBuilder { id_resolver, tables: config.tables.clone().unwrap_or_default(), functions: config.functions.clone().unwrap_or_default(), - auto_functions: new_auto_publish(config, true), - auto_tables: new_auto_publish(config, false), + auto_functions, + auto_tables, }) } @@ -275,7 +319,7 @@ impl PgBuilder { } } -fn update_auto_fields(id: &str, inf: &mut TableInfo, auto_tables: &PgBuilderAuto) { +fn update_auto_fields(id: &str, inf: &mut TableInfo, auto_tables: &PgBuilderTables) { if inf.clip_geom.is_none() { inf.clip_geom = auto_tables.clip_geom; } @@ -333,83 +377,82 @@ fn update_auto_fields(id: &str, inf: &mut TableInfo, auto_tables: &PgBuilderAuto ); } -fn new_auto_publish(config: &PgConfig, is_function: bool) -> Option { - let default_id_fmt = |is_func| (if is_func { "{function}" } else { "{table}" }).to_string(); - let default = |schemas| { - Some(PgBuilderAuto { - source_id_format: default_id_fmt(is_function), - schemas, - id_columns: None, - clip_geom: None, - buffer: None, - extent: None, - }) +fn calc_auto(config: &PgConfig) -> (Option, Option) { + let auto_tables = if use_auto_publish(config, false) { + let schemas = get_auto_schemas!(config, tables); + let bld = if let Object(PgCfgPublish { + tables: Object(v), .. + }) = &config.auto_publish + { + PgBuilderTables { + schemas, + source_id_format: v + .source_id_format + .as_deref() + .unwrap_or("{table}") + .to_string(), + id_columns: v.id_columns.opt_iter().map(|v| v.cloned().collect()), + clip_geom: v.clip_geom, + buffer: v.buffer, + extent: v.extent, + } + } else { + PgBuilderTables { + schemas, + source_id_format: "{table}".to_string(), + ..Default::default() + } + }; + Some(bld) + } else { + None }; - if let Some(bo_a) = &config.auto_publish { - match bo_a { - BoolOrObject::Object(a) => match if is_function { &a.functions } else { &a.tables } { - Some(bo_i) => match bo_i { - BoolOrObject::Object(item) => Some(PgBuilderAuto { - source_id_format: item - .source_id_format - .as_ref() - .cloned() - .unwrap_or_else(|| default_id_fmt(is_function)), - schemas: merge_opt_hs(&a.from_schemas, &item.from_schemas), - id_columns: item.id_columns.as_ref().and_then(|ids| { - if is_function { - error!("Configuration parameter auto_publish.functions.id_columns is not supported"); - None - } else { - Some(ids.iter().cloned().collect()) - } - }), - clip_geom: { - if is_function { - error!("Configuration parameter auto_publish.functions.clip_geom is not supported"); - None - } else { - item.clip_geom - } - }, - buffer: { - if is_function { - error!("Configuration parameter auto_publish.functions.buffer is not supported"); - None - } else { - item.buffer - } - }, - extent: { - if is_function { - error!("Configuration parameter auto_publish.functions.extent is not supported"); - None - } else { - item.extent - } - }, + let auto_functions = if use_auto_publish(config, true) { + Some(PgBuilderFuncs { + schemas: get_auto_schemas!(config, functions), + source_id_format: if let Object(PgCfgPublish { + functions: + Object(PgCfgPublishFuncs { + source_id_format: Some(v), + .. }), - BoolOrObject::Bool(true) => default(merge_opt_hs(&a.from_schemas, &None)), - BoolOrObject::Bool(false) => None, - }, + .. + }) = &config.auto_publish + { + v.clone() + } else { + "{function}".to_string() + }, + }) + } else { + None + }; + + (auto_tables, auto_functions) +} + +fn use_auto_publish(config: &PgConfig, for_functions: bool) -> bool { + match &config.auto_publish { + NoValue => config.tables.is_none() && config.functions.is_none(), + Object(funcs) => { + if for_functions { // If auto_publish.functions is set, and currently asking for .tables which is missing, // .tables becomes the inverse of functions (i.e. an obj or true in tables means false in functions) - None => match if is_function { &a.tables } else { &a.functions } { - Some(bo_i) => match bo_i { - BoolOrObject::Object(_) | BoolOrObject::Bool(true) => None, - BoolOrObject::Bool(false) => default(merge_opt_hs(&a.from_schemas, &None)), - }, - None => default(merge_opt_hs(&a.from_schemas, &None)), - }, - }, - BoolOrObject::Bool(true) => default(None), - BoolOrObject::Bool(false) => None, + match &funcs.functions { + NoValue => matches!(funcs.tables, NoValue | Bool(false)), + Object(_) => true, + Bool(v) => *v, + } + } else { + match &funcs.tables { + NoValue => matches!(funcs.functions, NoValue | Bool(false)), + Object(_) => true, + Bool(v) => *v, + } + } } - } else if config.tables.is_some() || config.functions.is_some() { - None - } else { - default(None) + Bool(v) => *v, } } @@ -442,142 +485,167 @@ fn by_key(a: &(String, T), b: &(String, T)) -> Ordering { a.0.cmp(&b.0) } -/// Merge two optional list of strings into a hashset -fn merge_opt_hs( - a: &Option>, - b: &Option>, -) -> Option> { - if let Some(a) = a { - let mut res: HashSet<_> = a.iter().cloned().collect(); - if let Some(b) = b { - res.extend(b.iter().cloned()); - } - Some(res) - } else { - b.as_ref().map(|b| b.iter().cloned().collect()) - } -} - #[cfg(test)] mod tests { use indoc::indoc; + use insta::assert_yaml_snapshot; use super::*; - #[allow(clippy::unnecessary_wraps)] - fn builder(source_id_format: &str, schemas: Option<&[&str]>) -> Option { - Some(PgBuilderAuto { - source_id_format: source_id_format.to_string(), - schemas: schemas.map(|s| s.iter().map(|s| (*s).to_string()).collect()), - id_columns: None, - clip_geom: None, - buffer: None, - extent: None, - }) + #[derive(serde::Serialize)] + struct AutoCfg { + auto_table: Option, + auto_funcs: Option, } - - fn parse_yaml(content: &str) -> PgConfig { - serde_yaml::from_str(content).unwrap() + fn auto(content: &str) -> AutoCfg { + let cfg: PgConfig = serde_yaml::from_str(content).unwrap(); + let (auto_table, auto_funcs) = calc_auto(&cfg); + AutoCfg { + auto_table, + auto_funcs, + } } #[test] + #[allow(clippy::too_many_lines)] fn test_auto_publish_no_auto() { - let config = parse_yaml("{}"); - let res = new_auto_publish(&config, false); - assert_eq!(res, builder("{table}", None)); - let res = new_auto_publish(&config, true); - assert_eq!(res, builder("{function}", None)); + let cfg = auto("{}"); + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: + source_id_format: "{table}" + auto_funcs: + source_id_format: "{function}" + "###); - let config = parse_yaml("tables: {}"); - assert_eq!(new_auto_publish(&config, false), None); - assert_eq!(new_auto_publish(&config, true), None); + let cfg = auto("tables: {}"); + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: ~ + auto_funcs: ~ + "###); - let config = parse_yaml("functions: {}"); - assert_eq!(new_auto_publish(&config, false), None); - assert_eq!(new_auto_publish(&config, true), None); - } + let cfg = auto("functions: {}"); + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: ~ + auto_funcs: ~ + "###); - #[test] - fn test_auto_publish_bool() { - let config = parse_yaml("auto_publish: true"); - let res = new_auto_publish(&config, false); - assert_eq!(res, builder("{table}", None)); - let res = new_auto_publish(&config, true); - assert_eq!(res, builder("{function}", None)); + let cfg = auto("auto_publish: true"); + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: + source_id_format: "{table}" + auto_funcs: + source_id_format: "{function}" + "###); - let config = parse_yaml("auto_publish: false"); - assert_eq!(new_auto_publish(&config, false), None); - assert_eq!(new_auto_publish(&config, true), None); - } + let cfg = auto("auto_publish: false"); + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: ~ + auto_funcs: ~ + "###); - #[test] - fn test_auto_publish_obj_bool() { - let config = parse_yaml(indoc! {" + let cfg = auto(indoc! {" auto_publish: from_schemas: public tables: true"}); - let res = new_auto_publish(&config, false); - assert_eq!(res, builder("{table}", Some(&["public"]))); - assert_eq!(new_auto_publish(&config, true), None); + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: + schemas: + - public + source_id_format: "{table}" + auto_funcs: ~ + "###); - let config = parse_yaml(indoc! {" + let cfg = auto(indoc! {" auto_publish: from_schemas: public functions: true"}); - assert_eq!(new_auto_publish(&config, false), None); - let res = new_auto_publish(&config, true); - assert_eq!(res, builder("{function}", Some(&["public"]))); + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: ~ + auto_funcs: + schemas: + - public + source_id_format: "{function}" + "###); - let config = parse_yaml(indoc! {" + let cfg = auto(indoc! {" auto_publish: from_schemas: public tables: false"}); - assert_eq!(new_auto_publish(&config, false), None); - let res = new_auto_publish(&config, true); - assert_eq!(res, builder("{function}", Some(&["public"]))); + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: ~ + auto_funcs: + schemas: + - public + source_id_format: "{function}" + "###); - let config = parse_yaml(indoc! {" + let cfg = auto(indoc! {" auto_publish: from_schemas: public functions: false"}); - let res = new_auto_publish(&config, false); - assert_eq!(res, builder("{table}", Some(&["public"]))); - assert_eq!(new_auto_publish(&config, true), None); - } + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: + schemas: + - public + source_id_format: "{table}" + auto_funcs: ~ + "###); - #[test] - fn test_auto_publish_obj_obj() { - let config = parse_yaml(indoc! {" + let cfg = auto(indoc! {" auto_publish: from_schemas: public tables: from_schemas: osm id_format: 'foo_{schema}.{table}_bar'"}); - let res = new_auto_publish(&config, false); - assert_eq!( - res, - builder("foo_{schema}.{table}_bar", Some(&["public", "osm"])) - ); - assert_eq!(new_auto_publish(&config, true), None); + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: + schemas: + - osm + - public + source_id_format: "foo_{schema}.{table}_bar" + auto_funcs: ~ + "###); - let config = parse_yaml(indoc! {" + let cfg = auto(indoc! {" auto_publish: from_schemas: public tables: from_schemas: osm source_id_format: '{schema}.{table}'"}); - let res = new_auto_publish(&config, false); - assert_eq!(res, builder("{schema}.{table}", Some(&["public", "osm"]))); - assert_eq!(new_auto_publish(&config, true), None); + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: + schemas: + - osm + - public + source_id_format: "{schema}.{table}" + auto_funcs: ~ + "###); - let config = parse_yaml(indoc! {" + let cfg = auto(indoc! {" auto_publish: tables: from_schemas: - osm - public"}); - let res = new_auto_publish(&config, false); - assert_eq!(res, builder("{table}", Some(&["public", "osm"]))); - assert_eq!(new_auto_publish(&config, true), None); + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: + schemas: + - osm + - public + source_id_format: "{table}" + auto_funcs: ~ + "###); } } diff --git a/martin/src/pg/mod.rs b/martin/src/pg/mod.rs index 3437322e..35204208 100644 --- a/martin/src/pg/mod.rs +++ b/martin/src/pg/mod.rs @@ -10,11 +10,9 @@ mod table_source; mod tls; mod utils; -pub use config::{PgCfgPublish, PgCfgPublishType, PgConfig, PgSslCerts}; +pub use config::{PgCfgPublish, PgCfgPublishFuncs, PgCfgPublishTables, PgConfig, PgSslCerts}; pub use config_function::FunctionInfo; pub use config_table::TableInfo; pub use errors::{PgError, Result}; pub use function_source::query_available_function; pub use pool::{PgPool, POOL_SIZE_DEFAULT}; - -pub use crate::utils::BoolOrObject; diff --git a/martin/src/sprites/mod.rs b/martin/src/sprites/mod.rs index cb120d4b..ac2b3f4a 100644 --- a/martin/src/sprites/mod.rs +++ b/martin/src/sprites/mod.rs @@ -55,12 +55,11 @@ pub type SpriteCatalog = BTreeMap; pub struct SpriteSources(HashMap); impl SpriteSources { - pub fn resolve(config: &mut Option) -> Result { - let Some(cfg) = config else { + pub fn resolve(config: &mut FileConfigEnum) -> Result { + let Some(cfg) = config.extract_file_config() else { return Ok(Self::default()); }; - let cfg = cfg.extract_file_config(); let mut results = Self::default(); let mut directories = Vec::new(); let mut configs = HashMap::new(); @@ -72,18 +71,16 @@ impl SpriteSources { } }; - if let Some(paths) = cfg.paths { - for path in paths { - let Some(name) = path.file_name() else { - warn!( - "Ignoring sprite source with no name from {}", - path.display() - ); - continue; - }; - directories.push(path.clone()); - results.add_source(name.to_string_lossy().to_string(), path); - } + for path in cfg.paths { + let Some(name) = path.file_name() else { + warn!( + "Ignoring sprite source with no name from {}", + path.display() + ); + continue; + }; + directories.push(path.clone()); + results.add_source(name.to_string_lossy().to_string(), path); } *config = FileConfigEnum::new_extended(directories, configs, cfg.unrecognized); diff --git a/martin/src/utils/cfg_containers.rs b/martin/src/utils/cfg_containers.rs new file mode 100644 index 00000000..6f7c9c2e --- /dev/null +++ b/martin/src/utils/cfg_containers.rs @@ -0,0 +1,143 @@ +use std::vec::IntoIter; + +use serde::{Deserialize, Serialize}; + +/// A serde helper to store a boolean as an object. +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum OptBoolObj { + #[default] + #[serde(skip)] + NoValue, + Bool(bool), + Object(T), +} + +impl OptBoolObj { + pub fn is_none(&self) -> bool { + matches!(self, Self::NoValue) + } +} + +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum OptOneMany { + #[default] + NoVals, + One(T), + Many(Vec), +} + +impl IntoIterator for OptOneMany { + type Item = T; + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + match self { + Self::NoVals => Vec::new().into_iter(), + Self::One(v) => vec![v].into_iter(), + Self::Many(v) => v.into_iter(), + } + } +} + +impl OptOneMany { + pub fn new>(iter: I) -> Self { + let mut iter = iter.into_iter(); + match (iter.next(), iter.next()) { + (Some(first), Some(second)) => { + let mut vec = Vec::with_capacity(iter.size_hint().0 + 2); + vec.push(first); + vec.push(second); + vec.extend(iter); + Self::Many(vec) + } + (Some(first), None) => Self::One(first), + (None, _) => Self::NoVals, + } + } + + pub fn is_none(&self) -> bool { + matches!(self, Self::NoVals) + } + + pub fn is_empty(&self) -> bool { + match self { + Self::NoVals => true, + Self::One(_) => false, + Self::Many(v) => v.is_empty(), + } + } + + pub fn iter(&self) -> impl Iterator { + match self { + Self::NoVals => [].iter(), + Self::One(v) => std::slice::from_ref(v).iter(), + Self::Many(v) => v.iter(), + } + } + + pub fn opt_iter(&self) -> Option> { + match self { + Self::NoVals => None, + Self::One(v) => Some(std::slice::from_ref(v).iter()), + Self::Many(v) => Some(v.iter()), + } + } + + pub fn iter_mut(&mut self) -> impl Iterator { + match self { + Self::NoVals => [].iter_mut(), + Self::One(v) => std::slice::from_mut(v).iter_mut(), + Self::Many(v) => v.iter_mut(), + } + } + + pub fn as_slice(&self) -> &[T] { + match self { + Self::NoVals => &[], + Self::One(item) => std::slice::from_ref(item), + Self::Many(v) => v.as_slice(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::OptOneMany::{Many, NoVals, One}; + + #[test] + fn test_one_or_many() { + let mut noval: OptOneMany = NoVals; + let mut one = One(1); + let mut many = Many(vec![1, 2, 3]); + + assert_eq!(OptOneMany::new(vec![1, 2, 3]), Many(vec![1, 2, 3])); + assert_eq!(OptOneMany::new(vec![1]), One(1)); + assert_eq!(OptOneMany::new(Vec::::new()), NoVals); + + assert_eq!(noval.iter_mut().collect::>(), Vec::<&i32>::new()); + assert_eq!(one.iter_mut().collect::>(), vec![&1]); + assert_eq!(many.iter_mut().collect::>(), vec![&1, &2, &3]); + + assert_eq!(noval.iter().collect::>(), Vec::<&i32>::new()); + assert_eq!(one.iter().collect::>(), vec![&1]); + assert_eq!(many.iter().collect::>(), vec![&1, &2, &3]); + + assert_eq!(noval.opt_iter().map(Iterator::collect::>), None); + assert_eq!(one.opt_iter().map(Iterator::collect), Some(vec![&1])); + assert_eq!( + many.opt_iter().map(Iterator::collect), + Some(vec![&1, &2, &3]) + ); + + assert_eq!(noval.as_slice(), Vec::::new().as_slice()); + assert_eq!(one.as_slice(), &[1]); + assert_eq!(many.as_slice(), &[1, 2, 3]); + + assert_eq!(noval.into_iter().collect::>(), Vec::::new()); + assert_eq!(one.into_iter().collect::>(), vec![1]); + assert_eq!(many.into_iter().collect::>(), vec![1, 2, 3]); + } +} diff --git a/martin/src/utils/mod.rs b/martin/src/utils/mod.rs index 85534e50..81ab8ec9 100644 --- a/martin/src/utils/mod.rs +++ b/martin/src/utils/mod.rs @@ -1,11 +1,11 @@ +mod cfg_containers; mod error; mod id_resolver; -mod one_or_many; mod utilities; mod xyz; +pub use cfg_containers::{OptBoolObj, OptOneMany}; pub use error::*; pub use id_resolver::IdResolver; -pub use one_or_many::OneOrMany; pub use utilities::*; pub use xyz::Xyz; diff --git a/martin/src/utils/one_or_many.rs b/martin/src/utils/one_or_many.rs deleted file mode 100644 index c7cdd780..00000000 --- a/martin/src/utils/one_or_many.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::vec::IntoIter; - -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum OneOrMany { - One(T), - Many(Vec), -} - -impl IntoIterator for OneOrMany { - type Item = T; - type IntoIter = IntoIter; - - fn into_iter(self) -> Self::IntoIter { - match self { - Self::One(v) => vec![v].into_iter(), - Self::Many(v) => v.into_iter(), - } - } -} - -impl OneOrMany { - pub fn new_opt>(iter: I) -> Option { - let mut iter = iter.into_iter(); - match (iter.next(), iter.next()) { - (Some(first), Some(second)) => { - let mut vec = Vec::with_capacity(iter.size_hint().0 + 2); - vec.push(first); - vec.push(second); - vec.extend(iter); - Some(Self::Many(vec)) - } - (Some(first), None) => Some(Self::One(first)), - (None, _) => None, - } - } - - pub fn is_empty(&self) -> bool { - match self { - Self::One(_) => false, - Self::Many(v) => v.is_empty(), - } - } - - pub fn iter(&self) -> impl Iterator { - match self { - OneOrMany::Many(v) => v.iter(), - OneOrMany::One(v) => std::slice::from_ref(v).iter(), - } - } - - pub fn iter_mut(&mut self) -> impl Iterator { - match self { - Self::Many(v) => v.iter_mut(), - Self::One(v) => std::slice::from_mut(v).iter_mut(), - } - } - - pub fn as_slice(&self) -> &[T] { - match self { - Self::One(item) => std::slice::from_ref(item), - Self::Many(v) => v.as_slice(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::OneOrMany::{Many, One}; - - #[test] - fn test_one_or_many() { - let mut one = One(1); - let mut many = Many(vec![1, 2, 3]); - - assert_eq!(OneOrMany::new_opt(vec![1, 2, 3]), Some(Many(vec![1, 2, 3]))); - assert_eq!(OneOrMany::new_opt(vec![1]), Some(One(1))); - assert_eq!(OneOrMany::new_opt(Vec::::new()), None); - - assert_eq!(one.iter_mut().collect::>(), vec![&1]); - assert_eq!(many.iter_mut().collect::>(), vec![&1, &2, &3]); - - assert_eq!(one.iter().collect::>(), vec![&1]); - assert_eq!(many.iter().collect::>(), vec![&1, &2, &3]); - - assert_eq!(one.as_slice(), &[1]); - assert_eq!(many.as_slice(), &[1, 2, 3]); - - assert_eq!(one.into_iter().collect::>(), vec![1]); - assert_eq!(many.into_iter().collect::>(), vec![1, 2, 3]); - } -} diff --git a/martin/src/utils/utilities.rs b/martin/src/utils/utilities.rs index e05e7927..e3a83f35 100644 --- a/martin/src/utils/utilities.rs +++ b/martin/src/utils/utilities.rs @@ -6,17 +6,9 @@ use std::time::Duration; use flate2::read::GzDecoder; use flate2::write::GzEncoder; use futures::pin_mut; -use serde::{Deserialize, Serialize, Serializer}; +use serde::{Serialize, Serializer}; use tokio::time::timeout; -/// A serde helper to store a boolean as an object. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum BoolOrObject { - Bool(bool), - Object(T), -} - /// Sort an optional hashmap by key, case-insensitive first, then case-sensitive pub fn sorted_opt_map( value: &Option>, @@ -31,6 +23,21 @@ pub fn sorted_btree_map(value: &HashMap) -> BTreeMa BTreeMap::from_iter(items) } +#[cfg(test)] +pub fn sorted_opt_set( + value: &Option>, + serializer: S, +) -> Result { + value + .as_ref() + .map(|v| { + let mut v: Vec<_> = v.iter().collect(); + v.sort(); + v + }) + .serialize(serializer) +} + pub fn decode_gzip(data: &[u8]) -> Result, std::io::Error> { let mut decoder = GzDecoder::new(data); let mut decompressed = Vec::new(); diff --git a/martin/tests/pg_server_test.rs b/martin/tests/pg_server_test.rs index c814f554..341e9aab 100644 --- a/martin/tests/pg_server_test.rs +++ b/martin/tests/pg_server_test.rs @@ -4,7 +4,7 @@ use actix_web::test::{call_and_read_body_json, call_service, read_body, TestRequ use ctor::ctor; use indoc::indoc; use insta::assert_yaml_snapshot; -use martin::OneOrMany; +use martin::OptOneMany; use tilejson::TileJSON; pub mod utils; @@ -1092,7 +1092,7 @@ tables: ) .await; - let OneOrMany::One(cfg) = cfg.postgres.unwrap() else { + let OptOneMany::One(cfg) = cfg.postgres else { panic!() }; for (name, _) in cfg.tables.unwrap_or_default() { diff --git a/martin/tests/utils/pg_utils.rs b/martin/tests/utils/pg_utils.rs index da5da7d3..27b68942 100644 --- a/martin/tests/utils/pg_utils.rs +++ b/martin/tests/utils/pg_utils.rs @@ -34,8 +34,6 @@ pub fn table<'a>(mock: &'a MockSource, name: &str) -> &'a TableInfo { let (_, config) = mock; let vals: Vec<&TableInfo> = config .postgres - .as_ref() - .unwrap() .iter() .flat_map(|v| v.tables.iter().map(|vv| vv.get(name))) .flatten()