hocon-linked 0.5.3-dev

Reads HOCON configuration files
Documentation
use std::cell::RefCell;
use std::collections::HashMap;
use std::ops::Deref;
use std::rc::Rc;

use crate::HoconLoaderConfig;

use super::intermediate::{Child, HoconIntermediate, Node};
use super::value::HoconValue;

pub(crate) enum Include<'a> {
    File(&'a str),
    Url(&'a str),
}
impl<'a> Include<'a> {
    fn included(&self) -> &'a str {
        match self {
            Include::File(s) => s,
            Include::Url(s) => s,
        }
    }
}

#[derive(Debug, PartialEq, Clone)]
pub(crate) struct HoconInternal {
    pub(crate) internal: Hash,
}

impl HoconInternal {
    pub(crate) fn empty() -> Self {
        Self { internal: vec![] }
    }

    pub(crate) fn add(self, mut other: HoconInternal) -> Self {
        let mut elems = self.internal;
        elems.append(&mut other.internal);
        Self { internal: elems }
    }

    pub(crate) fn from_properties(properties: HashMap<String, String>) -> Self {
        Self {
            internal: properties
                .into_iter()
                .map(|(path, value)| {
                    (
                        path.split('.')
                            .map(|s| HoconValue::String(String::from(s)))
                            .collect(),
                        HoconValue::String(value),
                    )
                })
                .collect(),
        }
    }

    pub(crate) fn from_value(v: HoconValue) -> Self {
        Self {
            internal: vec![(vec![], v)],
        }
    }

    pub(crate) fn from_object(h: Hash) -> Self {
        if h.is_empty() {
            Self {
                internal: vec![(vec![], HoconValue::EmptyObject)],
            }
        } else {
            Self {
                internal: h
                    .into_iter()
                    .map(|(k, v)| Self::add_root_to_includes(k, v))
                    .collect(),
            }
        }
    }

    fn add_root_to_includes(k: Vec<HoconValue>, v: HoconValue) -> (Vec<HoconValue>, HoconValue) {
        match v {
            HoconValue::Included {
                value,
                original_path,
                ..
            } => {
                let root = k
                    .iter()
                    .take(k.len() - original_path.len())
                    .cloned()
                    .collect();
                (
                    k,
                    HoconValue::Included {
                        value,
                        include_root: Some(root),
                        original_path,
                    },
                )
            }
            HoconValue::ToConcatToArray {
                value,
                original_path,
                item_id,
                ..
            } => (
                k,
                HoconValue::ToConcatToArray {
                    value,
                    original_path,
                    item_id,
                },
            ),
            _ => (k, v),
        }
    }

    pub(crate) fn from_array(a: Vec<HoconInternal>) -> Self {
        let mut indexer: Box<dyn Fn(i64) -> HoconValue> = Box::new(HoconValue::Integer);
        if !a.is_empty() && a[0].internal.len() == 1 {
            if let HoconValue::PathSubstitutionInParent(_) = a[0].internal[0].1 {
                let index_prefix = uuid::Uuid::new_v4().to_hyphenated().to_string();
                indexer = Box::new(move |i| HoconValue::Null(format!("{}-{}", index_prefix, i)));
            }
        }
        if a.is_empty() {
            Self {
                internal: vec![(vec![], HoconValue::EmptyArray)],
            }
        } else {
            Self {
                internal: a
                    .into_iter()
                    .enumerate()
                    .flat_map(|(i, hw)| {
                        Self {
                            internal: hw.internal,
                        }
                        .add_to_path(vec![indexer(i as i64)])
                        .internal
                        .into_iter()
                    })
                    .map(|(k, v)| Self::add_root_to_includes(k, v))
                    .collect(),
            }
        }
    }

    pub(crate) fn from_include(
        included: Include,
        config: &HoconLoaderConfig,
    ) -> Result<Self, crate::Error> {
        if config.include_depth > config.max_include_depth {
            Ok(Self {
                internal: vec![(
                    vec![HoconValue::String(String::from(included.included()))],
                    bad_value_or_err!(config, crate::Error::TooManyIncludes),
                )],
            })
        } else if config.file_meta.is_none() {
            Ok(Self {
                internal: vec![(
                    vec![HoconValue::String(String::from(included.included()))],
                    bad_value_or_err!(config, crate::Error::IncludeNotAllowedFromStr),
                )],
            })
        } else {
            let included_parsed = match included {
                Include::File(path) => {
                    let include_config = config
                        .included_from()
                        .with_file(std::path::Path::new(path).to_path_buf());
                    include_config
                        .read_file()
                        .map_err(|_| crate::error::Error::Include {
                            path: String::from(path),
                        })
                        .and_then(|s| include_config.parse_str_to_internal(s))
                }
                #[cfg(feature = "url-support")]
                Include::Url(url) => {
                    config
                        .load_url(url)
                        .map_err(|_| crate::error::Error::Include {
                            path: String::from(url),
                        })
                }
                #[cfg(not(feature = "url-support"))]
                _ => Err(crate::error::Error::DisabledExternalUrl),
            };

            match included_parsed {
                Ok(included) => Ok(Self {
                    internal: included
                        .internal
                        .into_iter()
                        .map(|(path, value)| {
                            (
                                path.clone(),
                                HoconValue::Included {
                                    value: Box::new(value),
                                    original_path: path,
                                    include_root: None,
                                },
                            )
                        })
                        .collect(),
                }),
                Err(error) => Ok(Self {
                    internal: vec![(
                        vec![HoconValue::String(String::from(included.included()))],
                        bad_value_or_err!(config, error),
                    )],
                }),
            }
        }
    }

    pub(crate) fn add_include(
        &mut self,
        included: Include,
        config: &HoconLoaderConfig,
    ) -> Result<Self, crate::Error> {
        let mut included = Self::from_include(included, config)?;

        included.internal.append(&mut self.internal);

        Ok(included)
    }

    pub(crate) fn add_to_path(self, p: Path) -> Self {
        self.transform(|mut k, v| {
            let mut new_path = p.clone();
            new_path.append(&mut k);
            (new_path, v)
        })
    }

    pub(crate) fn transform(
        self,
        transform: impl Fn(Vec<HoconValue>, HoconValue) -> (Vec<HoconValue>, HoconValue),
    ) -> Self {
        Self {
            internal: self
                .internal
                .into_iter()
                .map(|(k, v)| (transform(k, v)))
                .collect(),
        }
    }

    pub(crate) fn merge(
        self,
        config: &HoconLoaderConfig,
    ) -> Result<HoconIntermediate, crate::Error> {
        let root = Rc::new(Child {
            key: HoconValue::Temp,
            value: RefCell::new(Node::Node {
                children: vec![],
                key_hint: None,
            }),
        });

        let mut concatenated_arrays: HashMap<Path, HashMap<HoconValue, i64>> = HashMap::new();

        let mut last_path_encoutered = vec![];
        for (raw_path, item) in self.internal {
            if raw_path.is_empty() {
                continue;
            }

            let full_path = raw_path
                .clone()
                .into_iter()
                .flat_map(|path_item| match path_item {
                    HoconValue::UnquotedString(s) => s
                        .trim()
                        .split('.')
                        .map(|s| HoconValue::String(String::from(s)))
                        .collect(),
                    _ => vec![path_item],
                })
                .collect::<Vec<_>>();

            let (leaf_value, path) = match item {
                HoconValue::PathSubstitutionInParent(v) => {
                    let subst = HoconValue::PathSubstitution {
                        target: v,
                        optional: false,
                        original: None,
                    }
                    .substitute(config, &root, &full_path);
                    (subst, full_path.into_iter().rev().skip(1).rev().collect())
                }
                HoconValue::ToConcatToArray {
                    value,
                    original_path,
                    item_id,
                    ..
                } => {
                    let concat_root: Path = full_path
                        .iter()
                        .rev()
                        .skip(original_path.len())
                        .rev()
                        .cloned()
                        .collect();
                    let existing_array = concatenated_arrays
                        .entry(concat_root.clone())
                        .or_insert_with(HashMap::new);
                    let nb_elems = existing_array.keys().len();
                    let idx = existing_array
                        .entry(HoconValue::String(item_id.clone()))
                        .or_insert(nb_elems as i64);
                    (
                        value.substitute(config, &root, &full_path),
                        concat_root
                            .into_iter()
                            .chain(std::iter::once(HoconValue::Integer(*idx)))
                            .chain(original_path.into_iter().flat_map(|path_item| {
                                match path_item {
                                    HoconValue::UnquotedString(s) => s
                                        .trim()
                                        .split('.')
                                        .map(|s| HoconValue::String(String::from(s)))
                                        .collect(),
                                    _ => vec![path_item],
                                }
                            }))
                            .collect(),
                    )
                }
                HoconValue::PathSubstitution { ref target, .. } => {
                    let value = concatenated_arrays
                        .get(&target.to_path())
                        .cloned()
                        .unwrap_or_else(HashMap::new);
                    concatenated_arrays
                        .entry(full_path.clone())
                        .or_insert(value);
                    (item.substitute(config, &root, &full_path), full_path)
                }
                v => {
                    let mut checked_path: Path = vec![];
                    for item in full_path.clone() {
                        if let HoconValue::Integer(idx) = item {
                            concatenated_arrays
                                .entry(checked_path.clone())
                                .or_insert_with(HashMap::new)
                                .entry(HoconValue::Integer(idx))
                                .or_insert(idx);
                        }
                        checked_path.push(item);
                    }
                    (v.substitute(config, &root, &full_path), full_path)
                }
            };

            let mut current_path = vec![];
            let mut current_node = Rc::clone(&root);
            let mut old_node_value_for_optional_substitution = None;
            for path_item in path {
                current_path.push(path_item.clone());
                let (target_child, child_list) = match current_node.value.borrow().deref() {
                    Node::Leaf(old_value) => {
                        let new_child = Rc::new(Child {
                            key: path_item,
                            value: RefCell::new(Node::Leaf(HoconValue::Temp)),
                        });

                        old_node_value_for_optional_substitution = Some(old_value.clone());

                        (Rc::clone(&new_child), vec![Rc::clone(&new_child)])
                    }
                    Node::Node { children, .. } => {
                        let exist = children.iter().find(|child| child.key == path_item);
                        let first_key = children.iter().next().map(|v| Rc::deref(v).key.clone());
                        match (exist, first_key) {
                            (_, Some(HoconValue::Integer(0)))
                                if path_item == HoconValue::Integer(0)
                                    && last_path_encoutered.len() >= current_path.len()
                                    && current_path.as_slice()
                                        != &last_path_encoutered[0..current_path.len()] =>
                            {
                                let mut new_children = vec![];
                                let new_child = Rc::new(Child {
                                    key: path_item.clone(),
                                    value: RefCell::new(Node::Leaf(HoconValue::Temp)),
                                });
                                new_children.push(Rc::clone(&new_child));

                                (new_child, new_children)
                            }
                            (Some(child), _) => {
                                if let Node::Leaf(old_val) = child.value.borrow().deref() {
                                    old_node_value_for_optional_substitution =
                                        Some(old_val.clone());
                                }
                                (Rc::clone(child), children.clone())
                            }
                            (None, _) => {
                                let new_child = Rc::new(Child {
                                    key: path_item.clone(),
                                    value: RefCell::new(Node::Leaf(HoconValue::Null(
                                        String::from("0"),
                                    ))),
                                });
                                let mut new_children = if children.is_empty() {
                                    children.clone()
                                } else {
                                    match (
                                        Rc::deref(
                                            children.iter().next().expect("got an empty iterator"),
                                        ),
                                        path_item,
                                    ) {
                                        (_, HoconValue::Integer(0)) => vec![],
                                        (
                                            Child {
                                                key: HoconValue::Integer(_),
                                                ..
                                            },
                                            HoconValue::String(_),
                                        ) => vec![],
                                        (
                                            Child {
                                                key: HoconValue::String(_),
                                                ..
                                            },
                                            HoconValue::Integer(_),
                                        ) => vec![],
                                        _ => children.clone(),
                                    }
                                };

                                new_children.push(Rc::clone(&new_child));
                                (new_child, new_children)
                            }
                        }
                    }
                };
                current_node.value.replace(Node::Node {
                    children: child_list,
                    key_hint: None,
                });

                current_node = target_child;
            }
            let mut leaf = current_node.value.borrow_mut();

            *leaf = match leaf_value? {
                Node::Leaf(HoconValue::PathSubstitution {
                    target,
                    optional,
                    original: previously_set_original,
                }) => Node::Leaf(HoconValue::PathSubstitution {
                    target,
                    optional,
                    original: previously_set_original
                        .or_else(|| old_node_value_for_optional_substitution.map(Box::new)),
                }),
                v => v,
            };
            last_path_encoutered = current_path;
        }

        Ok(HoconIntermediate {
            tree: Rc::try_unwrap(root)
                .expect("error getting Rc")
                .value
                .into_inner(),
        })
    }
}

pub(crate) type Path = Vec<HoconValue>;
pub(crate) type Hash = Vec<(Path, HoconValue)>;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn max_depth_of_include() {
        let val = dbg!(HoconInternal::from_include(
            Include::File("file.conf"),
            &HoconLoaderConfig {
                include_depth: 15,
                file_meta: Some(crate::ConfFileMeta::from_path(
                    std::path::Path::new("file.conf").to_path_buf()
                )),
                ..Default::default()
            }
        ))
        .expect("during test");

        assert_eq!(
            val,
            HoconInternal {
                internal: vec![(
                    vec![HoconValue::String(String::from("file.conf"))],
                    HoconValue::BadValue(crate::Error::TooManyIncludes)
                )]
            }
        );
    }

    #[test]
    fn missing_file_included() {
        let val = dbg!(HoconInternal::from_include(
            Include::File("file.conf"),
            &HoconLoaderConfig {
                include_depth: 5,
                file_meta: Some(crate::ConfFileMeta::from_path(
                    std::path::Path::new("file.conf").to_path_buf()
                )),
                ..Default::default()
            }
        ))
        .expect("during test");

        assert_eq!(
            val,
            HoconInternal {
                internal: vec![(
                    vec![HoconValue::String(String::from("file.conf"))],
                    HoconValue::BadValue(crate::Error::Include {
                        path: String::from("file.conf")
                    })
                )]
            }
        );
    }
}