use std::{
any,
borrow::Cow,
collections::{BTreeMap, BTreeSet, HashMap},
iter,
};
use anyhow::Context;
use self::mount::{MountingPoint, MountingPoints};
use crate::{
metadata::{
AliasOptions, BasicTypes, ConfigMetadata, ConfigVariant, NestedConfigMetadata,
ParamMetadata,
},
utils::EnumVariant,
value::Pointer,
};
mod mount;
#[cfg(test)]
mod tests;
#[derive(Debug, Clone, Copy)]
struct ParentLink {
parent_ty: any::TypeId,
this_ref: &'static NestedConfigMetadata,
}
#[derive(Debug, Clone)]
pub(crate) struct ConfigData {
pub(crate) metadata: &'static ConfigMetadata,
parent_link: Option<ParentLink>,
pub(crate) is_top_level: bool,
pub(crate) coerce_serde_enums: bool,
all_paths: Vec<(Cow<'static, str>, AliasOptions)>,
}
impl ConfigData {
pub(crate) fn prefix(&self) -> Pointer<'_> {
Pointer(self.all_paths[0].0.as_ref())
}
pub(crate) fn aliases(&self) -> impl Iterator<Item = (&str, AliasOptions)> + '_ {
self.all_paths
.iter()
.skip(1)
.map(|(path, options)| (path.as_ref(), *options))
}
pub(crate) fn all_paths_for_param(
&self,
param: &'static ParamMetadata,
) -> impl Iterator<Item = (String, AliasOptions)> + '_ {
self.all_paths_for_child(param.name, param.aliases, param.tag_variant)
}
fn all_paths_for_child(
&self,
name: &'static str,
aliases: &'static [(&'static str, AliasOptions)],
tag_variant: Option<&'static ConfigVariant>,
) -> impl Iterator<Item = (String, AliasOptions)> + '_ {
let local_names =
iter::once((name, AliasOptions::default())).chain(aliases.iter().copied());
let enum_names = if let (true, Some(variant)) = (self.coerce_serde_enums, tag_variant) {
let variant_names = iter::once(variant.name)
.chain(variant.aliases.iter().copied())
.filter_map(|name| Some(EnumVariant::new(name)?.to_snake_case()));
let local_names_ = local_names.clone();
let paths = variant_names.flat_map(move |variant_name| {
local_names_
.clone()
.filter_map(move |(name_or_path, options)| {
if name_or_path.starts_with('.') {
return None;
}
let full_path = Pointer(&variant_name).join(name_or_path);
Some((Cow::Owned(full_path), options))
})
});
Some(paths)
} else {
None
};
let enum_names = enum_names.into_iter().flatten();
let local_names = local_names
.map(|(name, options)| (Cow::Borrowed(name), options))
.chain(enum_names);
self.all_paths
.iter()
.flat_map(move |(alias, config_options)| {
local_names
.clone()
.filter_map(move |(name_or_path, options)| {
let full_path = Pointer(alias).join_path(Pointer(&name_or_path))?;
Some((full_path, options.combine(*config_options)))
})
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct ConfigRef<'a> {
schema: &'a ConfigSchema,
prefix: &'a str,
pub(crate) data: &'a ConfigData,
}
impl<'a> ConfigRef<'a> {
pub fn prefix(&self) -> &'a str {
self.prefix
}
pub fn metadata(&self) -> &'static ConfigMetadata {
self.data.metadata
}
pub fn is_top_level(&self) -> bool {
self.data.parent_link.is_none()
}
#[doc(hidden)] pub fn parent_link(&self) -> Option<(Self, &'static NestedConfigMetadata)> {
let link = self.data.parent_link?;
let parent_prefix = if link.this_ref.name.is_empty() {
self.prefix
} else {
let (parent, _) = Pointer(self.prefix).split_last().unwrap();
parent.0
};
let parent_ref = Self {
schema: self.schema,
prefix: parent_prefix,
data: self.schema.get_ll(parent_prefix, link.parent_ty)?,
};
Some((parent_ref, link.this_ref))
}
pub fn aliases(&self) -> impl Iterator<Item = (&'a str, AliasOptions)> + '_ {
self.data.aliases()
}
#[doc(hidden)] pub fn all_paths_for_param(
&self,
param: &'static ParamMetadata,
) -> impl Iterator<Item = (String, AliasOptions)> + '_ {
self.data.all_paths_for_param(param)
}
}
#[derive(Debug)]
pub struct ConfigMut<'a> {
schema: &'a mut ConfigSchema,
prefix: String,
type_id: any::TypeId,
}
impl ConfigMut<'_> {
pub fn prefix(&self) -> &str {
&self.prefix
}
pub fn aliases(&self) -> impl Iterator<Item = (&str, AliasOptions)> + '_ {
let data = &self.schema.configs[self.prefix.as_str()].inner[&self.type_id];
data.aliases()
}
pub fn push_alias(self, alias: &'static str) -> anyhow::Result<Self> {
self.push_alias_inner(alias, AliasOptions::new())
}
pub fn push_deprecated_alias(self, alias: &'static str) -> anyhow::Result<Self> {
self.push_alias_inner(
alias,
AliasOptions {
is_deprecated: true,
},
)
}
fn push_alias_inner(self, alias: &'static str, options: AliasOptions) -> anyhow::Result<Self> {
let mut patched = PatchedSchema::new(self.schema);
patched.insert_alias(self.prefix.clone(), self.type_id, Pointer(alias), options)?;
patched.commit();
Ok(self)
}
}
#[derive(Debug, Clone, Default)]
struct ConfigsForPrefix {
inner: HashMap<any::TypeId, ConfigData>,
by_depth: BTreeSet<(usize, any::TypeId)>,
}
impl ConfigsForPrefix {
fn by_depth(&self) -> impl Iterator<Item = &ConfigData> + '_ {
self.by_depth.iter().map(|(_, ty)| &self.inner[ty])
}
fn insert(&mut self, ty: any::TypeId, depth: Option<usize>, data: ConfigData) {
self.inner.insert(ty, data);
if let Some(depth) = depth {
self.by_depth.insert((depth, ty));
}
}
fn extend(&mut self, other: Self) {
self.inner.extend(other.inner);
self.by_depth.extend(other.by_depth);
}
}
#[derive(Debug, Clone, Default)]
pub struct ConfigSchema {
configs: BTreeMap<Cow<'static, str>, ConfigsForPrefix>,
mounting_points: MountingPoints,
coerce_serde_enums: bool,
}
impl ConfigSchema {
#[allow(clippy::missing_panics_doc)]
pub fn new(metadata: &'static ConfigMetadata, prefix: &'static str) -> Self {
let mut this = Self::default();
this.insert(metadata, prefix)
.expect("internal error: failed inserting first config to the schema");
this
}
pub fn coerce_serde_enums(&mut self, coerce: bool) -> &mut Self {
self.coerce_serde_enums = coerce;
self
}
pub(crate) fn iter_ll(&self) -> impl Iterator<Item = (Pointer<'_>, &ConfigData)> + '_ {
self.configs
.iter()
.flat_map(|(prefix, data)| data.inner.values().map(move |data| (Pointer(prefix), data)))
}
pub(crate) fn contains_canonical_param(&self, at: Pointer<'_>) -> bool {
self.mounting_points.get(at.0).is_some_and(|mount| {
matches!(
mount,
MountingPoint::Param {
is_canonical: true,
..
}
)
})
}
pub(crate) fn params_with_kv_path<'s>(
&'s self,
kv_path: &'s str,
) -> impl Iterator<Item = (Pointer<'s>, BasicTypes)> + 's {
self.mounting_points
.by_kv_path(kv_path)
.filter_map(|(path, mount)| {
let expecting = match mount {
MountingPoint::Param { expecting, .. } => *expecting,
MountingPoint::Config => return None,
};
Some((path, expecting))
})
}
pub fn iter(&self) -> impl Iterator<Item = ConfigRef<'_>> + '_ {
self.configs.iter().flat_map(move |(prefix, data)| {
data.by_depth().map(move |data| ConfigRef {
schema: self,
prefix: prefix.as_ref(),
data,
})
})
}
pub fn locate(&self, metadata: &'static ConfigMetadata) -> impl Iterator<Item = &str> + '_ {
let config_type_id = metadata.ty.id();
self.configs.iter().filter_map(move |(prefix, data)| {
data.inner
.contains_key(&config_type_id)
.then_some(prefix.as_ref())
})
}
pub fn get<'s>(
&'s self,
metadata: &'static ConfigMetadata,
prefix: &'s str,
) -> Option<ConfigRef<'s>> {
let data = self.get_ll(prefix, metadata.ty.id())?;
Some(ConfigRef {
schema: self,
prefix,
data,
})
}
fn get_ll(&self, prefix: &str, ty: any::TypeId) -> Option<&ConfigData> {
self.configs.get(prefix)?.inner.get(&ty)
}
pub fn get_mut(
&mut self,
metadata: &'static ConfigMetadata,
prefix: &str,
) -> Option<ConfigMut<'_>> {
let ty = metadata.ty.id();
if !self.configs.get(prefix)?.inner.contains_key(&ty) {
return None;
}
Some(ConfigMut {
schema: self,
prefix: prefix.to_owned(),
type_id: ty,
})
}
#[allow(clippy::missing_panics_doc)] pub fn single(&self, metadata: &'static ConfigMetadata) -> anyhow::Result<ConfigRef<'_>> {
let prefixes: Vec<_> = self.locate(metadata).take(2).collect();
match prefixes.as_slice() {
[] => anyhow::bail!(
"configuration `{}` is not registered in schema",
metadata.ty.name_in_code()
),
&[prefix] => Ok(ConfigRef {
schema: self,
prefix,
data: &self.configs[prefix].inner[&metadata.ty.id()],
}),
[first, second] => anyhow::bail!(
"configuration `{}` is registered in at least 2 locations: {first:?}, {second:?}",
metadata.ty.name_in_code()
),
_ => unreachable!(),
}
}
#[allow(clippy::missing_panics_doc)] pub fn single_mut(
&mut self,
metadata: &'static ConfigMetadata,
) -> anyhow::Result<ConfigMut<'_>> {
let mut it = self.locate(metadata);
let first_prefix = it.next().with_context(|| {
format!(
"configuration `{}` is not registered in schema",
metadata.ty.name_in_code()
)
})?;
if let Some(second_prefix) = it.next() {
anyhow::bail!(
"configuration `{}` is registered in at least 2 locations: {first_prefix:?}, {second_prefix:?}",
metadata.ty.name_in_code()
);
}
drop(it);
let prefix = first_prefix.to_owned();
Ok(ConfigMut {
schema: self,
type_id: metadata.ty.id(),
prefix,
})
}
pub fn insert(
&mut self,
metadata: &'static ConfigMetadata,
prefix: &'static str,
) -> anyhow::Result<ConfigMut<'_>> {
let coerce_serde_enums = self.coerce_serde_enums;
let mut patched = PatchedSchema::new(self);
patched.insert_config(prefix, metadata, coerce_serde_enums)?;
patched.commit();
Ok(ConfigMut {
schema: self,
type_id: metadata.ty.id(),
prefix: prefix.to_owned(),
})
}
}
#[derive(Debug)]
#[must_use = "Should be `commit()`ted"]
struct PatchedSchema<'a> {
base: &'a mut ConfigSchema,
patch: ConfigSchema,
}
impl<'a> PatchedSchema<'a> {
fn new(base: &'a mut ConfigSchema) -> Self {
Self {
base,
patch: ConfigSchema::default(),
}
}
fn mount(&self, path: &str) -> Option<&MountingPoint> {
self.patch
.mounting_points
.get(path)
.or_else(|| self.base.mounting_points.get(path))
}
fn insert_config(
&mut self,
prefix: &'static str,
metadata: &'static ConfigMetadata,
coerce_serde_enums: bool,
) -> anyhow::Result<()> {
self.insert_recursively(
prefix.into(),
true,
ConfigData {
metadata,
parent_link: None,
is_top_level: true,
coerce_serde_enums,
all_paths: vec![(prefix.into(), AliasOptions::new())],
},
)
}
fn insert_recursively(
&mut self,
prefix: Cow<'static, str>,
is_new: bool,
data: ConfigData,
) -> anyhow::Result<()> {
let depth = is_new.then_some(0_usize);
let mut pending_configs = vec![(prefix, data, depth)];
while let Some((prefix, data, depth)) = pending_configs.pop() {
if is_new && self.base.get_ll(&prefix, data.metadata.ty.id()).is_some() {
continue;
}
let child_depth = depth.map(|d| d + 1);
let new_configs = Self::list_nested_configs(Pointer(&prefix), &data)
.map(|(prefix, data)| (prefix.into(), data, child_depth));
pending_configs.extend(new_configs);
self.insert_inner(prefix, depth, data)?;
}
Ok(())
}
fn insert_alias(
&mut self,
prefix: String,
config_id: any::TypeId,
alias: Pointer<'static>,
options: AliasOptions,
) -> anyhow::Result<()> {
let config_data = &self.base.configs[prefix.as_str()].inner[&config_id];
if config_data
.all_paths
.iter()
.any(|(name, _)| name == alias.0)
{
return Ok(()); }
let metadata = config_data.metadata;
self.insert_recursively(
prefix.into(),
false,
ConfigData {
metadata,
parent_link: config_data.parent_link,
is_top_level: config_data.is_top_level,
coerce_serde_enums: config_data.coerce_serde_enums,
all_paths: vec![(alias.0.into(), options)],
},
)
}
fn list_nested_configs<'i>(
prefix: Pointer<'i>,
data: &'i ConfigData,
) -> impl Iterator<Item = (String, ConfigData)> + 'i {
data.metadata.nested_configs.iter().map(move |nested| {
let all_paths =
data.all_paths_for_child(nested.name, nested.aliases, nested.tag_variant);
let all_paths = all_paths
.map(|(path, options)| (Cow::Owned(path), options))
.collect();
let config_data = ConfigData {
metadata: nested.meta,
parent_link: Some(ParentLink {
parent_ty: data.metadata.ty.id(),
this_ref: nested,
}),
is_top_level: false,
coerce_serde_enums: data.coerce_serde_enums,
all_paths,
};
(prefix.join(nested.name), config_data)
})
}
fn insert_inner(
&mut self,
prefix: Cow<'static, str>,
depth: Option<usize>,
mut data: ConfigData,
) -> anyhow::Result<()> {
let config_name = data.metadata.ty.name_in_code();
let config_paths = data.all_paths.iter().map(|(name, _)| name.as_ref());
let config_paths = iter::once(prefix.as_ref()).chain(config_paths);
for path in config_paths {
if let Some(mount) = self.mount(path) {
match mount {
MountingPoint::Config => { }
MountingPoint::Param { .. } => {
anyhow::bail!(
"Cannot mount config `{}` at `{path}` because parameter(s) are already mounted at this path",
data.metadata.ty.name_in_code()
);
}
}
}
self.patch
.mounting_points
.insert(path.to_owned(), MountingPoint::Config);
}
for param in data.metadata.params {
let all_paths = data.all_paths_for_param(param);
for (name_i, (full_name, _)) in all_paths.enumerate() {
let mut was_canonical = false;
if let Some(mount) = self.mount(&full_name) {
let prev_expecting = match mount {
MountingPoint::Param {
expecting,
is_canonical,
} => {
was_canonical = *is_canonical;
*expecting
}
MountingPoint::Config => {
anyhow::bail!(
"Cannot insert param `{name}` [Rust field: `{field}`] from config `{config_name}` at `{full_name}`: \
config(s) are already mounted at this path",
name = param.name,
field = param.rust_field_name
);
}
};
if prev_expecting != param.expecting {
anyhow::bail!(
"Cannot insert param `{name}` [Rust field: `{field}`] from config `{config_name}` at `{full_name}`: \
it expects {expecting}, while the existing param(s) mounted at this path expect {prev_expecting}",
name = param.name,
field = param.rust_field_name,
expecting = param.expecting
);
}
}
let is_canonical = was_canonical || name_i == 0;
self.patch.mounting_points.insert(
full_name,
MountingPoint::Param {
expecting: param.expecting,
is_canonical,
},
);
}
}
let config_id = data.metadata.ty.id();
let prev_data = self.base.get_ll(&prefix, config_id);
if let Some(prev_data) = prev_data {
let mut all_paths = prev_data.all_paths.clone();
all_paths.extend_from_slice(&data.all_paths);
data.all_paths = all_paths;
}
self.patch
.configs
.entry(prefix)
.or_default()
.insert(config_id, depth, data);
Ok(())
}
fn commit(self) {
for (prefix, data) in self.patch.configs {
let prev_data = self.base.configs.entry(prefix).or_default();
prev_data.extend(data);
}
self.base.mounting_points.extend(self.patch.mounting_points);
}
}