mirror of
https://github.com/maplibre/martin.git
synced 2024-12-18 20:31:54 +03:00
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:
parent
196df9e806
commit
e377bd62ac
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -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",
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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![
|
||||
|
@ -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
|
||||
|
@ -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
404
martin/src/pg/configurator.rs
Executable file → Normal 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: ~
|
||||
"###);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
143
martin/src/utils/cfg_containers.rs
Normal file
143
martin/src/utils/cfg_containers.rs
Normal 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]);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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]);
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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() {
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user