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]] [[package]]
name = "socket2" name = "socket2"
version = "0.5.4" version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.48.0", "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::connections::State::{Ignore, Take};
use crate::args::environment::Env; use crate::args::environment::Env;
use crate::pg::{PgConfig, PgSslCerts, POOL_SIZE_DEFAULT}; use crate::pg::{PgConfig, PgSslCerts, POOL_SIZE_DEFAULT};
use crate::utils::OneOrMany; use crate::utils::{OptBoolObj, OptOneMany};
#[derive(clap::Args, Debug, PartialEq, Default)] #[derive(clap::Args, Debug, PartialEq, Default)]
#[command(about, version)] #[command(about, version)]
@ -30,7 +30,7 @@ impl PgArgs {
self, self,
cli_strings: &mut Arguments, cli_strings: &mut Arguments,
env: &impl Env<'a>, env: &impl Env<'a>,
) -> Option<OneOrMany<PgConfig>> { ) -> OptOneMany<PgConfig> {
let connections = Self::extract_conn_strings(cli_strings, env); let connections = Self::extract_conn_strings(cli_strings, env);
let default_srid = self.get_default_srid(env); let default_srid = self.get_default_srid(env);
let certs = self.get_certs(env); let certs = self.get_certs(env);
@ -48,20 +48,20 @@ impl PgArgs {
}, },
max_feature_count: self.max_feature_count, max_feature_count: self.max_feature_count,
pool_size: self.pool_size, pool_size: self.pool_size,
auto_publish: None, auto_publish: OptBoolObj::NoValue,
tables: None, tables: None,
functions: None, functions: None,
}) })
.collect(); .collect();
match results.len() { match results.len() {
0 => None, 0 => OptOneMany::NoVals,
1 => Some(OneOrMany::One(results.into_iter().next().unwrap())), 1 => OptOneMany::One(results.into_iter().next().unwrap()),
_ => Some(OneOrMany::Many(results)), _ => 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() { 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()); 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| { pg_config.iter_mut().for_each(|c| {
@ -224,10 +224,10 @@ mod tests {
let config = PgArgs::default().into_config(&mut args, &FauxEnv::default()); let config = PgArgs::default().into_config(&mut args, &FauxEnv::default());
assert_eq!( assert_eq!(
config, config,
Some(OneOrMany::One(PgConfig { OptOneMany::One(PgConfig {
connection_string: some("postgres://localhost:5432"), connection_string: some("postgres://localhost:5432"),
..Default::default() ..Default::default()
})) })
); );
assert!(args.check().is_ok()); assert!(args.check().is_ok());
} }
@ -248,7 +248,7 @@ mod tests {
let config = PgArgs::default().into_config(&mut args, &env); let config = PgArgs::default().into_config(&mut args, &env);
assert_eq!( assert_eq!(
config, config,
Some(OneOrMany::One(PgConfig { OptOneMany::One(PgConfig {
connection_string: some("postgres://localhost:5432"), connection_string: some("postgres://localhost:5432"),
default_srid: Some(10), default_srid: Some(10),
ssl_certificates: PgSslCerts { ssl_certificates: PgSslCerts {
@ -256,7 +256,7 @@ mod tests {
..Default::default() ..Default::default()
}, },
..Default::default() ..Default::default()
})) })
); );
assert!(args.check().is_ok()); assert!(args.check().is_ok());
} }
@ -282,7 +282,7 @@ mod tests {
let config = pg_args.into_config(&mut args, &env); let config = pg_args.into_config(&mut args, &env);
assert_eq!( assert_eq!(
config, config,
Some(OneOrMany::One(PgConfig { OptOneMany::One(PgConfig {
connection_string: some("postgres://localhost:5432"), connection_string: some("postgres://localhost:5432"),
default_srid: Some(20), default_srid: Some(20),
ssl_certificates: PgSslCerts { ssl_certificates: PgSslCerts {
@ -291,7 +291,7 @@ mod tests {
ssl_root_cert: Some(PathBuf::from("root")), ssl_root_cert: Some(PathBuf::from("root")),
}, },
..Default::default() ..Default::default()
})) })
); );
assert!(args.check().is_ok()); assert!(args.check().is_ok());
} }

View File

@ -62,11 +62,11 @@ impl Args {
let mut cli_strings = Arguments::new(self.meta.connection); let mut cli_strings = Arguments::new(self.meta.connection);
let pg_args = self.pg.unwrap_or_default(); let pg_args = self.pg.unwrap_or_default();
if let Some(pg_config) = &mut config.postgres { if config.postgres.is_none() {
// config was loaded from a file, we can only apply a few CLI overrides to it
pg_args.override_config(pg_config, env);
} else {
config.postgres = pg_args.into_config(&mut cli_strings, env); 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() { 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) { let paths = cli_strings.process(|v| match PathBuf::try_from(v) {
Ok(v) => { Ok(v) => {
if v.is_dir() { if v.is_dir() {
@ -107,7 +107,7 @@ mod tests {
use super::*; use super::*;
use crate::pg::PgConfig; use crate::pg::PgConfig;
use crate::test_utils::{some, FauxEnv}; use crate::test_utils::{some, FauxEnv};
use crate::utils::OneOrMany; use crate::utils::OptOneMany;
fn parse(args: &[&str]) -> Result<(Config, MetaArgs)> { fn parse(args: &[&str]) -> Result<(Config, MetaArgs)> {
let args = Args::parse_from(args); let args = Args::parse_from(args);
@ -143,10 +143,10 @@ mod tests {
let args = parse(&["martin", "postgres://connection"]).unwrap(); let args = parse(&["martin", "postgres://connection"]).unwrap();
let cfg = Config { let cfg = Config {
postgres: Some(OneOrMany::One(PgConfig { postgres: OptOneMany::One(PgConfig {
connection_string: some("postgres://connection"), connection_string: some("postgres://connection"),
..Default::default() ..Default::default()
})), }),
..Default::default() ..Default::default()
}; };
let meta = MetaArgs { let meta = MetaArgs {

View File

@ -17,7 +17,7 @@ use crate::source::{TileInfoSources, TileSources};
use crate::sprites::SpriteSources; use crate::sprites::SpriteSources;
use crate::srv::SrvConfig; use crate::srv::SrvConfig;
use crate::Error::{ConfigLoadError, ConfigParseError, NoSources}; use crate::Error::{ConfigLoadError, ConfigParseError, NoSources};
use crate::{IdResolver, OneOrMany, Result}; use crate::{IdResolver, OptOneMany, Result};
pub type UnrecognizedValues = HashMap<String, serde_yaml::Value>; pub type UnrecognizedValues = HashMap<String, serde_yaml::Value>;
@ -31,17 +31,17 @@ pub struct Config {
#[serde(flatten)] #[serde(flatten)]
pub srv: SrvConfig, pub srv: SrvConfig,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "OptOneMany::is_none")]
pub postgres: Option<OneOrMany<PgConfig>>, pub postgres: OptOneMany<PgConfig>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "FileConfigEnum::is_none")]
pub pmtiles: Option<FileConfigEnum>, pub pmtiles: FileConfigEnum,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "FileConfigEnum::is_none")]
pub mbtiles: Option<FileConfigEnum>, pub mbtiles: FileConfigEnum,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "FileConfigEnum::is_none")]
pub sprites: Option<FileConfigEnum>, pub sprites: FileConfigEnum,
#[serde(flatten)] #[serde(flatten)]
pub unrecognized: UnrecognizedValues, pub unrecognized: UnrecognizedValues,
@ -53,40 +53,22 @@ impl Config {
let mut res = UnrecognizedValues::new(); let mut res = UnrecognizedValues::new();
copy_unrecognized_config(&mut res, "", &self.unrecognized); copy_unrecognized_config(&mut res, "", &self.unrecognized);
let mut any = if let Some(pg) = &mut self.postgres { for pg in self.postgres.iter_mut() {
for pg in pg.iter_mut() { res.extend(pg.finalize()?);
res.extend(pg.finalize()?); }
}
!pg.is_empty()
} else {
false
};
any |= if let Some(cfg) = &mut self.pmtiles { res.extend(self.pmtiles.finalize("pmtiles.")?);
res.extend(cfg.finalize("pmtiles.")?); res.extend(self.mbtiles.finalize("mbtiles.")?);
!cfg.is_empty() res.extend(self.sprites.finalize("sprites.")?);
} else {
false
};
any |= if let Some(cfg) = &mut self.mbtiles { if self.postgres.is_empty()
res.extend(cfg.finalize("mbtiles.")?); && self.pmtiles.is_empty()
!cfg.is_empty() && self.mbtiles.is_empty()
} else { && self.sprites.is_empty()
false {
};
any |= if let Some(cfg) = &mut self.sprites {
res.extend(cfg.finalize("sprites.")?);
!cfg.is_empty()
} else {
false
};
if any {
Ok(res)
} else {
Err(NoSources) Err(NoSources)
} else {
Ok(res)
} }
} }
@ -102,18 +84,16 @@ impl Config {
let create_mbt_src = &mut MbtSource::new_box; let create_mbt_src = &mut MbtSource::new_box;
let mut sources: Vec<Pin<Box<dyn Future<Output = Result<TileInfoSources>>>>> = Vec::new(); let mut sources: Vec<Pin<Box<dyn Future<Output = Result<TileInfoSources>>>>> = Vec::new();
if let Some(v) = self.postgres.as_mut() { for s in self.postgres.iter_mut() {
for s in v.iter_mut() { sources.push(Box::pin(s.resolve(idr.clone())));
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); let val = resolve_files(&mut self.pmtiles, idr.clone(), "pmtiles", create_pmt_src);
sources.push(Box::pin(val)); 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); let val = resolve_files(&mut self.mbtiles, idr.clone(), "mbtiles", create_mbt_src);
sources.push(Box::pin(val)); sources.push(Box::pin(val));
} }

View File

@ -10,8 +10,8 @@ use serde::{Deserialize, Serialize};
use crate::config::{copy_unrecognized_config, UnrecognizedValues}; use crate::config::{copy_unrecognized_config, UnrecognizedValues};
use crate::file_config::FileError::{InvalidFilePath, InvalidSourceFilePath, IoError}; use crate::file_config::FileError::{InvalidFilePath, InvalidSourceFilePath, IoError};
use crate::source::{Source, TileInfoSources}; use crate::source::{Source, TileInfoSources};
use crate::utils::{sorted_opt_map, Error, IdResolver, OneOrMany}; use crate::utils::{sorted_opt_map, Error, IdResolver, OptOneMany};
use crate::OneOrMany::{Many, One}; use crate::OptOneMany::{Many, One};
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum FileError { pub enum FileError {
@ -31,9 +31,11 @@ pub enum FileError {
AquireConnError(String), AquireConnError(String),
} }
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum FileConfigEnum { pub enum FileConfigEnum {
#[default]
None,
Path(PathBuf), Path(PathBuf),
Paths(Vec<PathBuf>), Paths(Vec<PathBuf>),
Config(FileConfig), Config(FileConfig),
@ -41,7 +43,7 @@ pub enum FileConfigEnum {
impl FileConfigEnum { impl FileConfigEnum {
#[must_use] #[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()) Self::new_extended(paths, HashMap::new(), UnrecognizedValues::new())
} }
@ -50,46 +52,70 @@ impl FileConfigEnum {
paths: Vec<PathBuf>, paths: Vec<PathBuf>,
configs: HashMap<String, FileConfigSrc>, configs: HashMap<String, FileConfigSrc>,
unrecognized: UnrecognizedValues, unrecognized: UnrecognizedValues,
) -> Option<FileConfigEnum> { ) -> FileConfigEnum {
if configs.is_empty() && unrecognized.is_empty() { if configs.is_empty() && unrecognized.is_empty() {
match paths.len() { match paths.len() {
0 => None, 0 => FileConfigEnum::None,
1 => Some(FileConfigEnum::Path(paths.into_iter().next().unwrap())), 1 => FileConfigEnum::Path(paths.into_iter().next().unwrap()),
_ => Some(FileConfigEnum::Paths(paths)), _ => FileConfigEnum::Paths(paths),
} }
} else { } else {
Some(FileConfigEnum::Config(FileConfig { FileConfigEnum::Config(FileConfig {
paths: OneOrMany::new_opt(paths), paths: OptOneMany::new(paths),
sources: if configs.is_empty() { sources: if configs.is_empty() {
None None
} else { } else {
Some(configs) Some(configs)
}, },
unrecognized, 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 { match self {
FileConfigEnum::Path(path) => FileConfig { Self::None => true,
paths: Some(One(mem::take(path))), Self::Path(_) => false,
..FileConfig::default() Self::Paths(v) => v.is_empty(),
}, Self::Config(c) => c.is_empty(),
FileConfigEnum::Paths(paths) => FileConfig {
paths: Some(Many(mem::take(paths))),
..Default::default()
},
FileConfigEnum::Config(cfg) => mem::take(cfg),
} }
} }
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)] #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct FileConfig { pub struct FileConfig {
/// A list of file paths /// A list of file paths
#[serde(skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "OptOneMany::is_none")]
pub paths: Option<OneOrMany<PathBuf>>, pub paths: OptOneMany<PathBuf>,
/// A map of source IDs to file paths or config objects /// A map of source IDs to file paths or config objects
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
#[serde(serialize_with = "sorted_opt_map")] #[serde(serialize_with = "sorted_opt_map")]
@ -128,27 +154,8 @@ pub struct FileConfigSource {
pub path: PathBuf, 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>( pub async fn resolve_files<Fut>(
config: &mut Option<FileConfigEnum>, config: &mut FileConfigEnum,
idr: IdResolver, idr: IdResolver,
extension: &str, extension: &str,
create_source: &mut impl FnMut(String, PathBuf) -> Fut, create_source: &mut impl FnMut(String, PathBuf) -> Fut,
@ -162,7 +169,7 @@ where
} }
async fn resolve_int<Fut>( async fn resolve_int<Fut>(
config: &mut Option<FileConfigEnum>, config: &mut FileConfigEnum,
idr: IdResolver, idr: IdResolver,
extension: &str, extension: &str,
create_source: &mut impl FnMut(String, PathBuf) -> Fut, create_source: &mut impl FnMut(String, PathBuf) -> Fut,
@ -170,10 +177,9 @@ async fn resolve_int<Fut>(
where where
Fut: Future<Output = Result<Box<dyn Source>, FileError>>, 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()); return Ok(TileInfoSources::default());
}; };
let cfg = cfg.extract_file_config();
let mut results = TileInfoSources::default(); let mut results = TileInfoSources::default();
let mut configs = HashMap::new(); let mut configs = HashMap::new();
@ -202,50 +208,47 @@ where
} }
} }
if let Some(paths) = cfg.paths { for path in cfg.paths {
for path in paths { let is_dir = path.is_dir();
let is_dir = path.is_dir(); let dir_files = if is_dir {
let dir_files = if is_dir { // directories will be kept in the config just in case there are new files
// directories will be kept in the config just in case there are new files directories.push(path.clone());
directories.push(path.clone()); path.read_dir()
path.read_dir() .map_err(|e| IoError(e, path.clone()))?
.map_err(|e| IoError(e, path.clone()))? .filter_map(Result::ok)
.filter_map(Result::ok) .filter(|f| {
.filter(|f| { f.path().extension().filter(|e| *e == extension).is_some() && f.path().is_file()
f.path().extension().filter(|e| *e == extension).is_some() })
&& f.path().is_file() .map(|f| f.path())
}) .collect()
.map(|f| f.path()) } else if path.is_file() {
.collect() vec![path]
} else if path.is_file() { } else {
vec![path] return Err(InvalidFilePath(path.canonicalize().unwrap_or(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()))?;
for path in dir_files { if files.contains(&can) {
let can = path.canonicalize().map_err(|e| IoError(e, path.clone()))?; if !is_dir {
if files.contains(&can) { warn!("Ignoring duplicate MBTiles path: {}", can.display());
if !is_dir {
warn!("Ignoring duplicate MBTiles path: {}", can.display());
}
continue;
} }
let id = path.file_stem().map_or_else( continue;
|| "_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?);
} }
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 { let FileConfigEnum::Config(cfg) = cfg else {
panic!(); panic!();
}; };
let paths = cfg.paths.clone().unwrap().into_iter().collect::<Vec<_>>(); let paths = cfg.paths.clone().into_iter().collect::<Vec<_>>();
assert_eq!( assert_eq!(
paths, paths,
vec![ vec![

View File

@ -31,7 +31,7 @@ pub use crate::args::Env;
pub use crate::config::{read_config, Config, ServerState}; pub use crate::config::{read_config, Config, ServerState};
pub use crate::source::Source; pub use crate::source::Source;
pub use crate::utils::{ 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 // 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::configurator::PgBuilder;
use crate::pg::Result; use crate::pg::Result;
use crate::source::TileInfoSources; 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 { pub trait PgInfo {
fn format_id(&self) -> String; fn format_id(&self) -> String;
@ -47,8 +47,8 @@ pub struct PgConfig {
pub max_feature_count: Option<usize>, pub max_feature_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub pool_size: Option<usize>, pub pool_size: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "OptBoolObj::is_none")]
pub auto_publish: Option<BoolOrObject<PgCfgPublish>>, pub auto_publish: OptBoolObj<PgCfgPublish>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
#[serde(serialize_with = "sorted_opt_map")] #[serde(serialize_with = "sorted_opt_map")]
pub tables: Option<TableInfoSources>, pub tables: Option<TableInfoSources>,
@ -59,29 +59,29 @@ pub struct PgConfig {
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct PgCfgPublish { pub struct PgCfgPublish {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "OptOneMany::is_none")]
#[serde(alias = "from_schema")] #[serde(alias = "from_schema")]
pub from_schemas: Option<OneOrMany<String>>, pub from_schemas: OptOneMany<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "OptBoolObj::is_none")]
pub tables: Option<BoolOrObject<PgCfgPublishType>>, pub tables: OptBoolObj<PgCfgPublishTables>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "OptBoolObj::is_none")]
pub functions: Option<BoolOrObject<PgCfgPublishType>>, pub functions: OptBoolObj<PgCfgPublishFuncs>,
} }
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct PgCfgPublishType { pub struct PgCfgPublishTables {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "OptOneMany::is_none")]
#[serde(alias = "from_schema")] #[serde(alias = "from_schema")]
pub from_schemas: Option<OneOrMany<String>>, pub from_schemas: OptOneMany<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "id_format")] #[serde(alias = "id_format")]
pub source_id_format: Option<String>, pub source_id_format: Option<String>,
/// A table column to use as the feature ID /// 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 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. /// 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")] #[serde(alias = "id_column")]
pub id_columns: Option<OneOrMany<String>>, pub id_columns: OptOneMany<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub clip_geom: Option<bool>, pub clip_geom: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@ -90,6 +90,16 @@ pub struct PgCfgPublishType {
pub extent: Option<u32>, 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 { impl PgConfig {
/// Apply defaults to the config, and validate if there is a connection string /// Apply defaults to the config, and validate if there is a connection string
pub fn finalize(&mut self) -> Result<UnrecognizedValues> { 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() { 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) Ok(res)
@ -143,7 +153,7 @@ mod tests {
use crate::pg::config_function::FunctionInfo; use crate::pg::config_function::FunctionInfo;
use crate::pg::config_table::TableInfo; use crate::pg::config_table::TableInfo;
use crate::test_utils::some; use crate::test_utils::some;
use crate::utils::OneOrMany::{Many, One}; use crate::utils::OptOneMany::{Many, One};
#[test] #[test]
fn parse_pg_one() { fn parse_pg_one() {
@ -153,11 +163,11 @@ mod tests {
connection_string: 'postgresql://postgres@localhost/db' connection_string: 'postgresql://postgres@localhost/db'
"}, "},
&Config { &Config {
postgres: Some(One(PgConfig { postgres: One(PgConfig {
connection_string: some("postgresql://postgres@localhost/db"), connection_string: some("postgresql://postgres@localhost/db"),
auto_publish: Some(BoolOrObject::Bool(true)), auto_publish: OptBoolObj::Bool(true),
..Default::default() ..Default::default()
})), }),
..Default::default() ..Default::default()
}, },
); );
@ -172,18 +182,18 @@ mod tests {
- connection_string: 'postgresql://postgres@localhost:5433/db' - connection_string: 'postgresql://postgres@localhost:5433/db'
"}, "},
&Config { &Config {
postgres: Some(Many(vec![ postgres: Many(vec![
PgConfig { PgConfig {
connection_string: some("postgres://postgres@localhost:5432/db"), connection_string: some("postgres://postgres@localhost:5432/db"),
auto_publish: Some(BoolOrObject::Bool(true)), auto_publish: OptBoolObj::Bool(true),
..Default::default() ..Default::default()
}, },
PgConfig { PgConfig {
connection_string: some("postgresql://postgres@localhost:5433/db"), connection_string: some("postgresql://postgres@localhost:5433/db"),
auto_publish: Some(BoolOrObject::Bool(true)), auto_publish: OptBoolObj::Bool(true),
..Default::default() ..Default::default()
}, },
])), ]),
..Default::default() ..Default::default()
}, },
); );
@ -225,7 +235,7 @@ mod tests {
bounds: [-180.0, -90.0, 180.0, 90.0] bounds: [-180.0, -90.0, 180.0, 90.0]
"}, "},
&Config { &Config {
postgres: Some(One(PgConfig { postgres: One(PgConfig {
connection_string: some("postgres://postgres@localhost:5432/db"), connection_string: some("postgres://postgres@localhost:5432/db"),
default_srid: Some(4326), default_srid: Some(4326),
pool_size: Some(20), pool_size: Some(20),
@ -262,7 +272,7 @@ mod tests {
), ),
)])), )])),
..Default::default() ..Default::default()
})), }),
..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::utils::{find_info, find_kv_ignore_case, normalize_key, InfoMap};
use crate::pg::PgError::InvalidTableExtent; use crate::pg::PgError::InvalidTableExtent;
use crate::pg::Result; use crate::pg::{PgCfgPublish, PgCfgPublishFuncs, Result};
use crate::source::TileInfoSources; 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 SqlFuncInfoMapMap = InfoMap<InfoMap<(PgSqlInfo, FunctionInfo)>>;
pub type SqlTableInfoMapMapMap = InfoMap<InfoMap<InfoMap<TableInfo>>>; pub type SqlTableInfoMapMapMap = InfoMap<InfoMap<InfoMap<TableInfo>>>;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub struct PgBuilderAuto { #[cfg_attr(test, derive(serde::Serialize))]
source_id_format: String, pub struct PgBuilderFuncs {
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
schemas: Option<HashSet<String>>, 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>>, id_columns: Option<Vec<String>>,
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
clip_geom: Option<bool>, clip_geom: Option<bool>,
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
buffer: Option<u32>, buffer: Option<u32>,
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
extent: Option<u32>, extent: Option<u32>,
} }
@ -39,17 +61,39 @@ pub struct PgBuilder {
default_srid: Option<i32>, default_srid: Option<i32>,
disable_bounds: bool, disable_bounds: bool,
max_feature_count: Option<usize>, max_feature_count: Option<usize>,
auto_functions: Option<PgBuilderAuto>, auto_functions: Option<PgBuilderFuncs>,
auto_tables: Option<PgBuilderAuto>, auto_tables: Option<PgBuilderTables>,
id_resolver: IdResolver, id_resolver: IdResolver,
tables: TableInfoSources, tables: TableInfoSources,
functions: FuncInfoSources, 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 { impl PgBuilder {
pub async fn new(config: &PgConfig, id_resolver: IdResolver) -> Result<Self> { pub async fn new(config: &PgConfig, id_resolver: IdResolver) -> Result<Self> {
let pool = PgPool::new(config).await?; let pool = PgPool::new(config).await?;
let (auto_tables, auto_functions) = calc_auto(config);
Ok(Self { Ok(Self {
pool, pool,
default_srid: config.default_srid, default_srid: config.default_srid,
@ -58,8 +102,8 @@ impl PgBuilder {
id_resolver, id_resolver,
tables: config.tables.clone().unwrap_or_default(), tables: config.tables.clone().unwrap_or_default(),
functions: config.functions.clone().unwrap_or_default(), functions: config.functions.clone().unwrap_or_default(),
auto_functions: new_auto_publish(config, true), auto_functions,
auto_tables: new_auto_publish(config, false), 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() { if inf.clip_geom.is_none() {
inf.clip_geom = auto_tables.clip_geom; 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> { fn calc_auto(config: &PgConfig) -> (Option<PgBuilderTables>, Option<PgBuilderFuncs>) {
let default_id_fmt = |is_func| (if is_func { "{function}" } else { "{table}" }).to_string(); let auto_tables = if use_auto_publish(config, false) {
let default = |schemas| { let schemas = get_auto_schemas!(config, tables);
Some(PgBuilderAuto { let bld = if let Object(PgCfgPublish {
source_id_format: default_id_fmt(is_function), tables: Object(v), ..
schemas, }) = &config.auto_publish
id_columns: None, {
clip_geom: None, PgBuilderTables {
buffer: None, schemas,
extent: None, 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 { let auto_functions = if use_auto_publish(config, true) {
match bo_a { Some(PgBuilderFuncs {
BoolOrObject::Object(a) => match if is_function { &a.functions } else { &a.tables } { schemas: get_auto_schemas!(config, functions),
Some(bo_i) => match bo_i { source_id_format: if let Object(PgCfgPublish {
BoolOrObject::Object(item) => Some(PgBuilderAuto { functions:
source_id_format: item Object(PgCfgPublishFuncs {
.source_id_format source_id_format: Some(v),
.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
}
},
}), }),
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, // 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) // .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 } { match &funcs.functions {
Some(bo_i) => match bo_i { NoValue => matches!(funcs.tables, NoValue | Bool(false)),
BoolOrObject::Object(_) | BoolOrObject::Bool(true) => None, Object(_) => true,
BoolOrObject::Bool(false) => default(merge_opt_hs(&a.from_schemas, &None)), Bool(v) => *v,
}, }
None => default(merge_opt_hs(&a.from_schemas, &None)), } else {
}, match &funcs.tables {
}, NoValue => matches!(funcs.functions, NoValue | Bool(false)),
BoolOrObject::Bool(true) => default(None), Object(_) => true,
BoolOrObject::Bool(false) => None, Bool(v) => *v,
}
}
} }
} else if config.tables.is_some() || config.functions.is_some() { Bool(v) => *v,
None
} else {
default(None)
} }
} }
@ -442,142 +485,167 @@ fn by_key<T>(a: &(String, T), b: &(String, T)) -> Ordering {
a.0.cmp(&b.0) 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)] #[cfg(test)]
mod tests { mod tests {
use indoc::indoc; use indoc::indoc;
use insta::assert_yaml_snapshot;
use super::*; use super::*;
#[allow(clippy::unnecessary_wraps)] #[derive(serde::Serialize)]
fn builder(source_id_format: &str, schemas: Option<&[&str]>) -> Option<PgBuilderAuto> { struct AutoCfg {
Some(PgBuilderAuto { auto_table: Option<PgBuilderTables>,
source_id_format: source_id_format.to_string(), auto_funcs: Option<PgBuilderFuncs>,
schemas: schemas.map(|s| s.iter().map(|s| (*s).to_string()).collect()),
id_columns: None,
clip_geom: None,
buffer: None,
extent: None,
})
} }
fn auto(content: &str) -> AutoCfg {
fn parse_yaml(content: &str) -> PgConfig { let cfg: PgConfig = serde_yaml::from_str(content).unwrap();
serde_yaml::from_str(content).unwrap() let (auto_table, auto_funcs) = calc_auto(&cfg);
AutoCfg {
auto_table,
auto_funcs,
}
} }
#[test] #[test]
#[allow(clippy::too_many_lines)]
fn test_auto_publish_no_auto() { fn test_auto_publish_no_auto() {
let config = parse_yaml("{}"); let cfg = auto("{}");
let res = new_auto_publish(&config, false); assert_yaml_snapshot!(cfg, @r###"
assert_eq!(res, builder("{table}", None)); ---
let res = new_auto_publish(&config, true); auto_table:
assert_eq!(res, builder("{function}", None)); source_id_format: "{table}"
auto_funcs:
source_id_format: "{function}"
"###);
let config = parse_yaml("tables: {}"); let cfg = auto("tables: {}");
assert_eq!(new_auto_publish(&config, false), None); assert_yaml_snapshot!(cfg, @r###"
assert_eq!(new_auto_publish(&config, true), None); ---
auto_table: ~
auto_funcs: ~
"###);
let config = parse_yaml("functions: {}"); let cfg = auto("functions: {}");
assert_eq!(new_auto_publish(&config, false), None); assert_yaml_snapshot!(cfg, @r###"
assert_eq!(new_auto_publish(&config, true), None); ---
} auto_table: ~
auto_funcs: ~
"###);
#[test] let cfg = auto("auto_publish: true");
fn test_auto_publish_bool() { assert_yaml_snapshot!(cfg, @r###"
let config = parse_yaml("auto_publish: true"); ---
let res = new_auto_publish(&config, false); auto_table:
assert_eq!(res, builder("{table}", None)); source_id_format: "{table}"
let res = new_auto_publish(&config, true); auto_funcs:
assert_eq!(res, builder("{function}", None)); source_id_format: "{function}"
"###);
let config = parse_yaml("auto_publish: false"); let cfg = auto("auto_publish: false");
assert_eq!(new_auto_publish(&config, false), None); assert_yaml_snapshot!(cfg, @r###"
assert_eq!(new_auto_publish(&config, true), None); ---
} auto_table: ~
auto_funcs: ~
"###);
#[test] let cfg = auto(indoc! {"
fn test_auto_publish_obj_bool() {
let config = parse_yaml(indoc! {"
auto_publish: auto_publish:
from_schemas: public from_schemas: public
tables: true"}); tables: true"});
let res = new_auto_publish(&config, false); assert_yaml_snapshot!(cfg, @r###"
assert_eq!(res, builder("{table}", Some(&["public"]))); ---
assert_eq!(new_auto_publish(&config, true), None); auto_table:
schemas:
- public
source_id_format: "{table}"
auto_funcs: ~
"###);
let config = parse_yaml(indoc! {" let cfg = auto(indoc! {"
auto_publish: auto_publish:
from_schemas: public from_schemas: public
functions: true"}); functions: true"});
assert_eq!(new_auto_publish(&config, false), None); assert_yaml_snapshot!(cfg, @r###"
let res = new_auto_publish(&config, true); ---
assert_eq!(res, builder("{function}", Some(&["public"]))); auto_table: ~
auto_funcs:
schemas:
- public
source_id_format: "{function}"
"###);
let config = parse_yaml(indoc! {" let cfg = auto(indoc! {"
auto_publish: auto_publish:
from_schemas: public from_schemas: public
tables: false"}); tables: false"});
assert_eq!(new_auto_publish(&config, false), None); assert_yaml_snapshot!(cfg, @r###"
let res = new_auto_publish(&config, true); ---
assert_eq!(res, builder("{function}", Some(&["public"]))); auto_table: ~
auto_funcs:
schemas:
- public
source_id_format: "{function}"
"###);
let config = parse_yaml(indoc! {" let cfg = auto(indoc! {"
auto_publish: auto_publish:
from_schemas: public from_schemas: public
functions: false"}); functions: false"});
let res = new_auto_publish(&config, false); assert_yaml_snapshot!(cfg, @r###"
assert_eq!(res, builder("{table}", Some(&["public"]))); ---
assert_eq!(new_auto_publish(&config, true), None); auto_table:
} schemas:
- public
source_id_format: "{table}"
auto_funcs: ~
"###);
#[test] let cfg = auto(indoc! {"
fn test_auto_publish_obj_obj() {
let config = parse_yaml(indoc! {"
auto_publish: auto_publish:
from_schemas: public from_schemas: public
tables: tables:
from_schemas: osm from_schemas: osm
id_format: 'foo_{schema}.{table}_bar'"}); id_format: 'foo_{schema}.{table}_bar'"});
let res = new_auto_publish(&config, false); assert_yaml_snapshot!(cfg, @r###"
assert_eq!( ---
res, auto_table:
builder("foo_{schema}.{table}_bar", Some(&["public", "osm"])) schemas:
); - osm
assert_eq!(new_auto_publish(&config, true), None); - public
source_id_format: "foo_{schema}.{table}_bar"
auto_funcs: ~
"###);
let config = parse_yaml(indoc! {" let cfg = auto(indoc! {"
auto_publish: auto_publish:
from_schemas: public from_schemas: public
tables: tables:
from_schemas: osm from_schemas: osm
source_id_format: '{schema}.{table}'"}); source_id_format: '{schema}.{table}'"});
let res = new_auto_publish(&config, false); assert_yaml_snapshot!(cfg, @r###"
assert_eq!(res, builder("{schema}.{table}", Some(&["public", "osm"]))); ---
assert_eq!(new_auto_publish(&config, true), None); auto_table:
schemas:
- osm
- public
source_id_format: "{schema}.{table}"
auto_funcs: ~
"###);
let config = parse_yaml(indoc! {" let cfg = auto(indoc! {"
auto_publish: auto_publish:
tables: tables:
from_schemas: from_schemas:
- osm - osm
- public"}); - public"});
let res = new_auto_publish(&config, false); assert_yaml_snapshot!(cfg, @r###"
assert_eq!(res, builder("{table}", Some(&["public", "osm"]))); ---
assert_eq!(new_auto_publish(&config, true), None); auto_table:
schemas:
- osm
- public
source_id_format: "{table}"
auto_funcs: ~
"###);
} }
} }

View File

@ -10,11 +10,9 @@ mod table_source;
mod tls; mod tls;
mod utils; 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_function::FunctionInfo;
pub use config_table::TableInfo; pub use config_table::TableInfo;
pub use errors::{PgError, Result}; pub use errors::{PgError, Result};
pub use function_source::query_available_function; pub use function_source::query_available_function;
pub use pool::{PgPool, POOL_SIZE_DEFAULT}; 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>); pub struct SpriteSources(HashMap<String, SpriteSource>);
impl SpriteSources { impl SpriteSources {
pub fn resolve(config: &mut Option<FileConfigEnum>) -> Result<Self, FileError> { pub fn resolve(config: &mut FileConfigEnum) -> Result<Self, FileError> {
let Some(cfg) = config else { let Some(cfg) = config.extract_file_config() else {
return Ok(Self::default()); return Ok(Self::default());
}; };
let cfg = cfg.extract_file_config();
let mut results = Self::default(); let mut results = Self::default();
let mut directories = Vec::new(); let mut directories = Vec::new();
let mut configs = HashMap::new(); let mut configs = HashMap::new();
@ -72,18 +71,16 @@ impl SpriteSources {
} }
}; };
if let Some(paths) = cfg.paths { for path in cfg.paths {
for path in paths { let Some(name) = path.file_name() else {
let Some(name) = path.file_name() else { warn!(
warn!( "Ignoring sprite source with no name from {}",
"Ignoring sprite source with no name from {}", path.display()
path.display() );
); continue;
continue; };
}; directories.push(path.clone());
directories.push(path.clone()); results.add_source(name.to_string_lossy().to_string(), path);
results.add_source(name.to_string_lossy().to_string(), path);
}
} }
*config = FileConfigEnum::new_extended(directories, configs, cfg.unrecognized); *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 error;
mod id_resolver; mod id_resolver;
mod one_or_many;
mod utilities; mod utilities;
mod xyz; mod xyz;
pub use cfg_containers::{OptBoolObj, OptOneMany};
pub use error::*; pub use error::*;
pub use id_resolver::IdResolver; pub use id_resolver::IdResolver;
pub use one_or_many::OneOrMany;
pub use utilities::*; pub use utilities::*;
pub use xyz::Xyz; 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::read::GzDecoder;
use flate2::write::GzEncoder; use flate2::write::GzEncoder;
use futures::pin_mut; use futures::pin_mut;
use serde::{Deserialize, Serialize, Serializer}; use serde::{Serialize, Serializer};
use tokio::time::timeout; 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 /// Sort an optional hashmap by key, case-insensitive first, then case-sensitive
pub fn sorted_opt_map<S: Serializer, T: Serialize>( pub fn sorted_opt_map<S: Serializer, T: Serialize>(
value: &Option<HashMap<String, T>>, 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) 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> { pub fn decode_gzip(data: &[u8]) -> Result<Vec<u8>, std::io::Error> {
let mut decoder = GzDecoder::new(data); let mut decoder = GzDecoder::new(data);
let mut decompressed = Vec::new(); 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 ctor::ctor;
use indoc::indoc; use indoc::indoc;
use insta::assert_yaml_snapshot; use insta::assert_yaml_snapshot;
use martin::OneOrMany; use martin::OptOneMany;
use tilejson::TileJSON; use tilejson::TileJSON;
pub mod utils; pub mod utils;
@ -1092,7 +1092,7 @@ tables:
) )
.await; .await;
let OneOrMany::One(cfg) = cfg.postgres.unwrap() else { let OptOneMany::One(cfg) = cfg.postgres else {
panic!() panic!()
}; };
for (name, _) in cfg.tables.unwrap_or_default() { 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 (_, config) = mock;
let vals: Vec<&TableInfo> = config let vals: Vec<&TableInfo> = config
.postgres .postgres
.as_ref()
.unwrap()
.iter() .iter()
.flat_map(|v| v.tables.iter().map(|vv| vv.get(name))) .flat_map(|v| v.tables.iter().map(|vv| vv.get(name)))
.flatten() .flatten()