use super::compass_configuration_error::CompassConfigurationError;
use super::compass_configuration_field::CompassConfigurationField;
use serde::de;
use std::{
path::{Path, PathBuf},
str::FromStr,
};
fn looks_like_file_path(s: &str) -> bool {
let common_extensions = [
".json",
".toml",
".yaml",
".yml",
".csv",
".txt",
".xml",
".ini",
".conf",
".config",
".properties",
".parquet",
".arrow",
".geojson",
".osm",
".pbf",
".db",
".sqlite",
".bin",
".dat",
".log",
".vrt",
".tif",
".tiff",
".shp",
".gpkg",
".gz",
".zip",
".tar",
];
common_extensions
.iter()
.any(|ext| s.to_lowercase().ends_with(ext))
}
pub trait ConfigJsonExtensions {
fn get_config_section(
&self,
section: CompassConfigurationField,
parent_key: &dyn AsRef<str>,
) -> Result<serde_json::Value, CompassConfigurationError>;
fn get_config_path(
&self,
key: &dyn AsRef<str>,
parent_key: &dyn AsRef<str>,
) -> Result<PathBuf, CompassConfigurationError>;
fn get_config_path_optional(
&self,
key: &dyn AsRef<str>,
parent_key: &dyn AsRef<str>,
) -> Result<Option<PathBuf>, CompassConfigurationError>;
fn get_config_string(
&self,
key: &dyn AsRef<str>,
parent_key: &dyn AsRef<str>,
) -> Result<String, CompassConfigurationError>;
fn get_config_string_optional(
&self,
key: &dyn AsRef<str>,
) -> Result<Option<String>, CompassConfigurationError>;
fn get_config_array(
&self,
key: &dyn AsRef<str>,
parent_key: &dyn AsRef<str>,
) -> Result<Vec<serde_json::Value>, CompassConfigurationError>;
fn get_config_i64(
&self,
key: &dyn AsRef<str>,
parent_key: &dyn AsRef<str>,
) -> Result<i64, CompassConfigurationError>;
fn get_config_f64(
&self,
key: &dyn AsRef<str>,
parent_key: &dyn AsRef<str>,
) -> Result<f64, CompassConfigurationError>;
fn get_config_from_str<T: FromStr>(
&self,
key: &dyn AsRef<str>,
parent_key: &dyn AsRef<str>,
) -> Result<T, CompassConfigurationError>;
fn get_config_serde<T: de::DeserializeOwned>(
&self,
key: &dyn AsRef<str>,
parent_key: &dyn AsRef<str>,
) -> Result<T, CompassConfigurationError>;
fn get_config_serde_optional<T: de::DeserializeOwned>(
&self,
key: &dyn AsRef<str>,
parent_key: &dyn AsRef<str>,
) -> Result<Option<T>, CompassConfigurationError>;
fn normalize_file_paths(
&self,
parent_key: &dyn AsRef<str>,
root_config_path: &Path,
) -> Result<serde_json::Value, CompassConfigurationError>;
}
impl ConfigJsonExtensions for serde_json::Value {
fn get_config_section(
&self,
section: CompassConfigurationField,
parent_key: &dyn AsRef<str>,
) -> Result<serde_json::Value, CompassConfigurationError> {
let section = self
.get(section.to_str())
.ok_or_else(|| {
CompassConfigurationError::ExpectedFieldForComponent(
section.to_string(),
String::from(parent_key.as_ref()),
)
})?
.clone();
Ok(section)
}
fn get_config_path_optional(
&self,
key: &dyn AsRef<str>,
parent_key: &dyn AsRef<str>,
) -> Result<Option<PathBuf>, CompassConfigurationError> {
match self.get(key.as_ref()) {
None => Ok(None),
Some(_) => {
let config_path = self.get_config_path(key, parent_key)?;
Ok(Some(config_path))
}
}
}
fn get_config_path(
&self,
key: &dyn AsRef<str>,
parent_key: &dyn AsRef<str>,
) -> Result<PathBuf, CompassConfigurationError> {
let path_string = self.get_config_string(key, parent_key)?;
let path = PathBuf::from(&path_string);
if path.is_file() {
Ok(path)
} else {
Err(CompassConfigurationError::FileNotFoundForComponent(
path_string,
String::from(key.as_ref()),
String::from(parent_key.as_ref()),
))
}
}
fn get_config_string(
&self,
key: &dyn AsRef<str>,
parent_key: &dyn AsRef<str>,
) -> Result<String, CompassConfigurationError> {
let value = self
.get(key.as_ref())
.ok_or_else(|| {
CompassConfigurationError::ExpectedFieldForComponent(
String::from(key.as_ref()),
String::from(parent_key.as_ref()),
)
})?
.as_str()
.map(String::from)
.ok_or_else(|| {
CompassConfigurationError::ExpectedFieldWithType(
String::from(key.as_ref()),
String::from("String"),
)
})?;
Ok(value)
}
fn get_config_string_optional(
&self,
key: &dyn AsRef<str>,
) -> Result<Option<String>, CompassConfigurationError> {
let key_path = key.as_ref();
match self.get(key_path) {
None => Ok(None),
Some(value) => value
.as_str()
.map(|v| Some(String::from(v)))
.ok_or_else(|| {
CompassConfigurationError::ExpectedFieldWithType(
String::from(key_path),
String::from("String"),
)
}),
}
}
fn get_config_array(
&self,
key: &dyn AsRef<str>,
parent_key: &dyn AsRef<str>,
) -> Result<Vec<serde_json::Value>, CompassConfigurationError> {
let array = self
.get(key.as_ref())
.ok_or_else(|| {
CompassConfigurationError::ExpectedFieldForComponent(
String::from(key.as_ref()),
String::from(parent_key.as_ref()),
)
})?
.as_array()
.ok_or_else(|| {
CompassConfigurationError::ExpectedFieldWithType(
String::from(key.as_ref()),
String::from("Array"),
)
})?
.to_owned();
Ok(array)
}
fn get_config_i64(
&self,
key: &dyn AsRef<str>,
parent_key: &dyn AsRef<str>,
) -> Result<i64, CompassConfigurationError> {
let value = self
.get(key.as_ref())
.ok_or_else(|| {
CompassConfigurationError::ExpectedFieldForComponent(
String::from(key.as_ref()),
String::from(parent_key.as_ref()),
)
})?
.as_i64()
.ok_or_else(|| {
CompassConfigurationError::ExpectedFieldWithType(
String::from(key.as_ref()),
String::from("64-bit signed integer"),
)
})?;
Ok(value)
}
fn get_config_f64(
&self,
key: &dyn AsRef<str>,
parent_key: &dyn AsRef<str>,
) -> Result<f64, CompassConfigurationError> {
let value = self
.get(key.as_ref())
.ok_or_else(|| {
CompassConfigurationError::ExpectedFieldForComponent(
String::from(key.as_ref()),
String::from(parent_key.as_ref()),
)
})?
.as_f64()
.ok_or_else(|| {
CompassConfigurationError::ExpectedFieldWithType(
String::from(key.as_ref()),
String::from("64-bit floating point"),
)
})?;
Ok(value)
}
fn get_config_from_str<T: FromStr>(
&self,
key: &dyn AsRef<str>,
parent_key: &dyn AsRef<str>,
) -> Result<T, CompassConfigurationError> {
let value = self
.get(key.as_ref())
.ok_or_else(|| {
CompassConfigurationError::ExpectedFieldForComponent(
String::from(key.as_ref()),
String::from(parent_key.as_ref()),
)
})?
.as_str()
.ok_or_else(|| {
CompassConfigurationError::ExpectedFieldWithType(
String::from(key.as_ref()),
String::from("string-parseable"),
)
})?;
let result = T::from_str(value).map_err(|_| {
CompassConfigurationError::ExpectedFieldWithType(
String::from(key.as_ref()),
format!("failed to parse type from string {value}"),
)
})?;
Ok(result)
}
fn get_config_serde<T: de::DeserializeOwned>(
&self,
key: &dyn AsRef<str>,
parent_key: &dyn AsRef<str>,
) -> Result<T, CompassConfigurationError> {
let value = self
.get(key.as_ref())
.ok_or_else(|| {
CompassConfigurationError::ExpectedFieldForComponent(
String::from(key.as_ref()),
String::from(parent_key.as_ref()),
)
})?
.to_owned();
let result: T = serde_json::from_value(value)
.map_err(CompassConfigurationError::SerdeDeserializationError)?;
Ok(result)
}
fn get_config_serde_optional<T: de::DeserializeOwned>(
&self,
key: &dyn AsRef<str>,
_parent_key: &dyn AsRef<str>,
) -> Result<Option<T>, CompassConfigurationError> {
match self.get(key.as_ref()) {
None => Ok(None),
Some(value) => {
let result: T = serde_json::from_value(value.to_owned())
.map_err(CompassConfigurationError::SerdeDeserializationError)?;
Ok(Some(result))
}
}
}
fn normalize_file_paths(
&self,
parent_key: &dyn AsRef<str>,
root_config_path: &Path,
) -> Result<serde_json::Value, CompassConfigurationError> {
match self {
serde_json::Value::String(path_string) => {
if !looks_like_file_path(path_string) {
return Ok(serde_json::Value::String(path_string.clone()));
}
let path = Path::new(path_string);
if path.is_file() {
Ok(serde_json::Value::String(path_string.clone()))
} else {
let root_config_parent = match root_config_path.parent() {
Some(parent) => parent,
None => Path::new(""),
};
let new_path = root_config_parent.join(path);
let new_path_string = new_path
.to_str()
.ok_or_else(|| {
CompassConfigurationError::FileNormalizationError(path_string.clone())
})?
.to_string();
if new_path.is_file() {
Ok(serde_json::Value::String(new_path_string))
} else {
Err(CompassConfigurationError::FileNormalizationNotFound(
String::from(parent_key.as_ref()),
path_string.clone(),
new_path_string,
))
}
}
}
serde_json::Value::Object(obj) => {
let mut new_obj = serde_json::map::Map::new();
for (key, value) in obj.iter() {
if value.is_string() || value.is_object() || value.is_array() {
new_obj.insert(
String::from(key),
value.normalize_file_paths(key, root_config_path)?,
);
} else {
new_obj.insert(String::from(key), value.clone());
}
}
Ok(serde_json::Value::Object(new_obj))
}
serde_json::Value::Array(arr) => {
let mut new_arr = Vec::new();
for value in arr.iter() {
match value {
serde_json::Value::Array(_) => {
new_arr.push(value.normalize_file_paths(parent_key, root_config_path)?)
}
serde_json::Value::Object(_) => {
new_arr.push(value.normalize_file_paths(parent_key, root_config_path)?)
}
serde_json::Value::String(_) => {
new_arr.push(value.normalize_file_paths(parent_key, root_config_path)?)
}
_ => new_arr.push(value.clone()),
}
}
Ok(serde_json::Value::Array(new_arr))
}
_ => Ok(self.clone()),
}
}
}