Simplify conf parsing, separate func & tbl configs (#956)

* Simplify code by adding `None` to the enums we use for configuration
* Separate postgres auto-publish configuration into table and function
structs
This commit is contained in:
Yuri Astrakhan 2023-10-21 14:11:02 -04:00 committed by GitHub
parent 196df9e806
commit e377bd62ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 589 additions and 480 deletions

4
Cargo.lock generated
View File

@ -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",

View File

@ -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<OneOrMany<PgConfig>> {
) -> OptOneMany<PgConfig> {
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<PgConfig>, env: &impl Env<'a>) {
pub fn override_config<'a>(self, pg_config: &mut OptOneMany<PgConfig>, 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());
}

View File

@ -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<FileConfigEnum> {
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 {

View File

@ -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<String, serde_yaml::Value>;
@ -31,17 +31,17 @@ pub struct Config {
#[serde(flatten)]
pub srv: SrvConfig,
#[serde(skip_serializing_if = "Option::is_none")]
pub postgres: Option<OneOrMany<PgConfig>>,
#[serde(default, skip_serializing_if = "OptOneMany::is_none")]
pub postgres: OptOneMany<PgConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pmtiles: Option<FileConfigEnum>,
#[serde(default, skip_serializing_if = "FileConfigEnum::is_none")]
pub pmtiles: FileConfigEnum,
#[serde(skip_serializing_if = "Option::is_none")]
pub mbtiles: Option<FileConfigEnum>,
#[serde(default, skip_serializing_if = "FileConfigEnum::is_none")]
pub mbtiles: FileConfigEnum,
#[serde(skip_serializing_if = "Option::is_none")]
pub sprites: Option<FileConfigEnum>,
#[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<Pin<Box<dyn Future<Output = Result<TileInfoSources>>>>> = 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));
}

View File

@ -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<PathBuf>),
Config(FileConfig),
@ -41,7 +43,7 @@ pub enum FileConfigEnum {
impl FileConfigEnum {
#[must_use]
pub fn new(paths: Vec<PathBuf>) -> Option<FileConfigEnum> {
pub fn new(paths: Vec<PathBuf>) -> FileConfigEnum {
Self::new_extended(paths, HashMap::new(), UnrecognizedValues::new())
}
@ -50,46 +52,70 @@ impl FileConfigEnum {
paths: Vec<PathBuf>,
configs: HashMap<String, FileConfigSrc>,
unrecognized: UnrecognizedValues,
) -> Option<FileConfigEnum> {
) -> 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<FileConfig> {
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<UnrecognizedValues, Error> {
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<OneOrMany<PathBuf>>,
#[serde(default, skip_serializing_if = "OptOneMany::is_none")]
pub paths: OptOneMany<PathBuf>,
/// 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<UnrecognizedValues, Error> {
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<Fut>(
config: &mut Option<FileConfigEnum>,
config: &mut FileConfigEnum,
idr: IdResolver,
extension: &str,
create_source: &mut impl FnMut(String, PathBuf) -> Fut,
@ -162,7 +169,7 @@ where
}
async fn resolve_int<Fut>(
config: &mut Option<FileConfigEnum>,
config: &mut FileConfigEnum,
idr: IdResolver,
extension: &str,
create_source: &mut impl FnMut(String, PathBuf) -> Fut,
@ -170,10 +177,9 @@ async fn resolve_int<Fut>(
where
Fut: Future<Output = Result<Box<dyn Source>, 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::<Vec<_>>();
let paths = cfg.paths.clone().into_iter().collect::<Vec<_>>();
assert_eq!(
paths,
vec![

View File

@ -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

View File

@ -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<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pool_size: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_publish: Option<BoolOrObject<PgCfgPublish>>,
#[serde(default, skip_serializing_if = "OptBoolObj::is_none")]
pub auto_publish: OptBoolObj<PgCfgPublish>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(serialize_with = "sorted_opt_map")]
pub tables: Option<TableInfoSources>,
@ -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<OneOrMany<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tables: Option<BoolOrObject<PgCfgPublishType>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub functions: Option<BoolOrObject<PgCfgPublishType>>,
pub from_schemas: OptOneMany<String>,
#[serde(default, skip_serializing_if = "OptBoolObj::is_none")]
pub tables: OptBoolObj<PgCfgPublishTables>,
#[serde(default, skip_serializing_if = "OptBoolObj::is_none")]
pub functions: OptBoolObj<PgCfgPublishFuncs>,
}
#[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<OneOrMany<String>>,
pub from_schemas: OptOneMany<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "id_format")]
pub source_id_format: Option<String>,
/// 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<OneOrMany<String>>,
pub id_columns: OptOneMany<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub clip_geom: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
@ -90,6 +90,16 @@ pub struct PgCfgPublishType {
pub extent: Option<u32>,
}
#[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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "id_format")]
pub source_id_format: Option<String>,
}
impl PgConfig {
/// Apply defaults to the config, and validate if there is a connection string
pub fn finalize(&mut self) -> Result<UnrecognizedValues> {
@ -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()
},
);

404
martin/src/pg/configurator.rs Executable file → Normal file
View File

@ -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<InfoMap<(PgSqlInfo, FunctionInfo)>>;
pub type SqlTableInfoMapMapMap = InfoMap<InfoMap<InfoMap<TableInfo>>>;
#[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<HashSet<String>>,
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<HashSet<String>>,
source_id_format: String,
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
id_columns: Option<Vec<String>>,
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
clip_geom: Option<bool>,
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
buffer: Option<u32>,
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
extent: Option<u32>,
}
@ -39,17 +61,39 @@ pub struct PgBuilder {
default_srid: Option<i32>,
disable_bounds: bool,
max_feature_count: Option<usize>,
auto_functions: Option<PgBuilderAuto>,
auto_tables: Option<PgBuilderAuto>,
auto_functions: Option<PgBuilderFuncs>,
auto_tables: Option<PgBuilderTables>,
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<Self> {
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<PgBuilderAuto> {
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<PgBuilderTables>, Option<PgBuilderFuncs>) {
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<T>(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<OneOrMany<String>>,
b: &Option<OneOrMany<String>>,
) -> Option<HashSet<String>> {
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<PgBuilderAuto> {
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<PgBuilderTables>,
auto_funcs: Option<PgBuilderFuncs>,
}
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: ~
"###);
}
}

View File

@ -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;

View File

@ -55,12 +55,11 @@ pub type SpriteCatalog = BTreeMap<String, CatalogSpriteEntry>;
pub struct SpriteSources(HashMap<String, SpriteSource>);
impl SpriteSources {
pub fn resolve(config: &mut Option<FileConfigEnum>) -> Result<Self, FileError> {
let Some(cfg) = config else {
pub fn resolve(config: &mut FileConfigEnum) -> Result<Self, FileError> {
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);

View File

@ -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<T> {
#[default]
#[serde(skip)]
NoValue,
Bool(bool),
Object(T),
}
impl<T> OptBoolObj<T> {
pub fn is_none(&self) -> bool {
matches!(self, Self::NoValue)
}
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum OptOneMany<T> {
#[default]
NoVals,
One(T),
Many(Vec<T>),
}
impl<T> IntoIterator for OptOneMany<T> {
type Item = T;
type IntoIter = IntoIter<T>;
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<T> OptOneMany<T> {
pub fn new<I: IntoIterator<Item = T>>(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<Item = &T> {
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<impl Iterator<Item = &T>> {
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<Item = &mut T> {
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<i32> = 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::<i32>::new()), NoVals);
assert_eq!(noval.iter_mut().collect::<Vec<_>>(), Vec::<&i32>::new());
assert_eq!(one.iter_mut().collect::<Vec<_>>(), vec![&1]);
assert_eq!(many.iter_mut().collect::<Vec<_>>(), vec![&1, &2, &3]);
assert_eq!(noval.iter().collect::<Vec<_>>(), Vec::<&i32>::new());
assert_eq!(one.iter().collect::<Vec<_>>(), vec![&1]);
assert_eq!(many.iter().collect::<Vec<_>>(), vec![&1, &2, &3]);
assert_eq!(noval.opt_iter().map(Iterator::collect::<Vec<_>>), 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::<i32>::new().as_slice());
assert_eq!(one.as_slice(), &[1]);
assert_eq!(many.as_slice(), &[1, 2, 3]);
assert_eq!(noval.into_iter().collect::<Vec<_>>(), Vec::<i32>::new());
assert_eq!(one.into_iter().collect::<Vec<_>>(), vec![1]);
assert_eq!(many.into_iter().collect::<Vec<_>>(), vec![1, 2, 3]);
}
}

View File

@ -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;

View File

@ -1,95 +0,0 @@
use std::vec::IntoIter;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum OneOrMany<T> {
One(T),
Many(Vec<T>),
}
impl<T> IntoIterator for OneOrMany<T> {
type Item = T;
type IntoIter = IntoIter<T>;
fn into_iter(self) -> Self::IntoIter {
match self {
Self::One(v) => vec![v].into_iter(),
Self::Many(v) => v.into_iter(),
}
}
}
impl<T: Clone> OneOrMany<T> {
pub fn new_opt<I: IntoIterator<Item = T>>(iter: I) -> Option<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);
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<Item = &T> {
match self {
OneOrMany::Many(v) => v.iter(),
OneOrMany::One(v) => std::slice::from_ref(v).iter(),
}
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
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::<i32>::new()), None);
assert_eq!(one.iter_mut().collect::<Vec<_>>(), vec![&1]);
assert_eq!(many.iter_mut().collect::<Vec<_>>(), vec![&1, &2, &3]);
assert_eq!(one.iter().collect::<Vec<_>>(), vec![&1]);
assert_eq!(many.iter().collect::<Vec<_>>(), 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<_>>(), vec![1]);
assert_eq!(many.into_iter().collect::<Vec<_>>(), vec![1, 2, 3]);
}
}

View File

@ -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<T> {
Bool(bool),
Object(T),
}
/// Sort an optional hashmap by key, case-insensitive first, then case-sensitive
pub fn sorted_opt_map<S: Serializer, T: Serialize>(
value: &Option<HashMap<String, T>>,
@ -31,6 +23,21 @@ pub fn sorted_btree_map<K: Serialize + Ord, V>(value: &HashMap<K, V>) -> BTreeMa
BTreeMap::from_iter(items)
}
#[cfg(test)]
pub fn sorted_opt_set<S: Serializer>(
value: &Option<std::collections::HashSet<String>>,
serializer: S,
) -> Result<S::Ok, S::Error> {
value
.as_ref()
.map(|v| {
let mut v: Vec<_> = v.iter().collect();
v.sort();
v
})
.serialize(serializer)
}
pub fn decode_gzip(data: &[u8]) -> Result<Vec<u8>, std::io::Error> {
let mut decoder = GzDecoder::new(data);
let mut decompressed = Vec::new();

View File

@ -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() {

View File

@ -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()