nbwipers 0.6.1

Wipe clean your Jupyter Notebooks!
Documentation
use std::{fmt::Display, str::FromStr};

use serde::{de, Deserialize, Serialize};
use thiserror::Error;

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ExtraKey {
    CellMeta(StripKey),
    Metadata(StripKey),
}

impl Serialize for ExtraKey {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&self.to_string())
    }
}

impl ExtraKey {
    pub const fn get_parts(&self) -> &Vec<String> {
        match self {
            Self::Metadata(ref c) | Self::CellMeta(ref c) => &c.parts,
        }
    }
}

impl Display for ExtraKey {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::CellMeta(cellmeta) => write!(f, "cell.metadata.{}", cellmeta.parts.join(".")),
            Self::Metadata(meta) => write!(f, "metadata.{}", meta.parts.join(".")),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
pub struct StripKey {
    pub(crate) parts: Vec<String>,
}

#[derive(Debug, PartialEq, Eq, Clone, Copy, Error)]
pub enum ExtraKeyParseError {
    #[error("Key must start with `cell.metadata` or `metadata`")]
    NotCellOrMetadata,
    #[error("Empty Subkey")]
    EmptySubKey,
    #[error("Empty Key")]
    Empty,
}

impl StripKey {
    pub fn try_from_slice(parts: &[&str]) -> Result<Self, ExtraKeyParseError> {
        if parts.is_empty() || parts == [""] {
            Err(ExtraKeyParseError::EmptySubKey)
        } else {
            Ok(Self {
                parts: parts.iter().map(|x| String::from(*x)).collect(),
            })
        }
    }
}

impl FromStr for ExtraKey {
    type Err = ExtraKeyParseError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let parts = s.split('.').collect::<Vec<_>>();
        match parts.split_first() {
            Some((&"cell", tail)) => match tail.split_first() {
                Some((&"metadata", tail2)) => {
                    StripKey::try_from_slice(tail2).map(ExtraKey::CellMeta)
                }
                _ => Err(ExtraKeyParseError::NotCellOrMetadata),
            },

            Some((&"metadata", tail)) => StripKey::try_from_slice(tail).map(ExtraKey::Metadata),
            Some((&"", [])) => Err(ExtraKeyParseError::Empty),
            Some(_) => Err(ExtraKeyParseError::NotCellOrMetadata),
            None => unreachable!(),
        }
    }
}

impl<'de> Deserialize<'de> for ExtraKey {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let str_result = String::deserialize(deserializer)?;
        Self::from_str(str_result.as_str()).map_err(|_| {
            de::Error::invalid_value(
                de::Unexpected::Str(str_result.as_str()),
                &"dot separated json path to key starting with `cell` or `metadata`",
            )
        })
    }
}

pub fn partition_extra_keys<'a, I: IntoIterator<Item = &'a ExtraKey>>(
    extra_keys: I,
) -> (Vec<&'a ExtraKey>, Vec<&'a ExtraKey>) {
    let mut meta_keys = vec![];
    let mut cell_keys = vec![];
    for extra_key in extra_keys {
        match extra_key {
            ExtraKey::CellMeta(ref _cell_key) => cell_keys.push(extra_key),
            ExtraKey::Metadata(ref _meta_key) => meta_keys.push(extra_key),
        };
    }
    (cell_keys, meta_keys)
}

#[allow(clippy::unwrap_used)]
#[cfg(test)]
mod tests {
    use serde_json::json;

    use crate::{
        extra_keys::ExtraKeyParseError,
        schema::Cell,
        utils::{pop_cell_key, pop_value_child},
    };

    use super::ExtraKey;
    use crate::config::EXTRA_KEYS;
    use std::str::FromStr;

    #[test]
    fn test_pop_value_child() {
        let mut x = json!({"hello": {"world": "baby", "banana":"pear"}});
        pop_value_child(&mut x, &"hello.world".split('.').collect::<Vec<_>>());
        assert_eq!(x, json!({"hello": {"banana": "pear"}}));
    }
    #[test]
    fn test_pop_key() {
        let cell_value = json!({
         "cell_type": "code",
         "execution_count": null,
         "metadata": {"banana": "pear"},
         "outputs": [],
         "source": [
          "from ipywidgets import interact"
         ]
        });

        let mut cell: Cell = serde_json::from_value(cell_value).unwrap();
        let extra_key = ExtraKey::from_str("cell.metadata.banana").unwrap();
        pop_cell_key(&mut cell, &extra_key);
        assert_eq!(cell.get_metadata().as_object().unwrap().len(), 0);
    }
    #[test]
    fn test_key_roundtrip() {
        for key in EXTRA_KEYS {
            let parsed_key = ExtraKey::from_str(key).unwrap();
            let key2 = parsed_key.to_string();
            assert!(key == &key2);
        }
    }

    #[test]
    fn test_key_parse_errors() {
        assert!(matches!(
            ExtraKey::from_str("hello.world"),
            Err(ExtraKeyParseError::NotCellOrMetadata)
        ));
        assert!(matches!(
            ExtraKey::from_str("metadata."),
            Err(ExtraKeyParseError::EmptySubKey)
        ));
        assert!(matches!(
            ExtraKey::from_str("metadata"),
            Err(ExtraKeyParseError::EmptySubKey)
        ));
        assert!(matches!(
            ExtraKey::from_str(""),
            Err(ExtraKeyParseError::Empty)
        ));
        assert!(matches!(
            ExtraKey::from_str(".world"),
            Err(ExtraKeyParseError::NotCellOrMetadata)
        ));
        assert!(matches!(
            ExtraKey::from_str("cell.interlinked.world"),
            Err(ExtraKeyParseError::NotCellOrMetadata)
        ));
    }

    #[test]
    fn test_deserialize() {
        let valid_key: serde_json::Result<ExtraKey> =
            serde_json::from_str("\"metadata.hello.world\"");
        let invalid_key: serde_json::Result<ExtraKey> = serde_json::from_str("\"hello.world\"");

        assert!(valid_key.is_ok());
        assert!(invalid_key.is_err());
    }
}