entry 0.2.1

An interactive CLI for generating JSON-formatted data from a local schema
use chrono::{
    prelude::{NaiveDate, NaiveDateTime},
    DateTime,
};
use serde::{Deserialize, Serialize};
use serde_json::{Number, Value};
use std::{
    collections::{btree_map::IntoIter, BTreeMap},
    fs::{self, File, OpenOptions},
    io::BufReader,
    str::FromStr,
};

use crate::{config::Config, errors::EntryError};

#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
pub enum SchemaCount {
    #[serde(rename = "one")]
    One,
    #[serde(rename = "many")]
    Many,
}

#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub enum SchemaDataType {
    #[serde(rename = "string")]
    String,
    #[serde(rename = "number")]
    Number,
    #[serde(rename = "date")]
    Date,
    #[serde(rename = "datetime")]
    DateTime,
}

impl std::fmt::Display for SchemaDataType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&format_args!("{:?}", self).to_string().to_lowercase())
    }
}

impl FromStr for SchemaDataType {
    type Err = EntryError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "date" => Ok(SchemaDataType::Date),
            "number" => Ok(SchemaDataType::Number),
            "string" => Ok(SchemaDataType::String),
            _ => Err(EntryError::SchemaParseError),
        }
    }
}

#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub struct SchemaType {
    pub count: SchemaCount,
    pub data_type: SchemaDataType,
}

impl std::fmt::Display for SchemaType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self.count {
            SchemaCount::One => f.write_fmt(format_args!("{}", self.data_type)),
            SchemaCount::Many => f.write_fmt(format_args!("array of {}s", self.data_type)),
        }
    }
}

impl SchemaType {
    pub fn parse(&self, value: &str) -> Option<Value> {
        let trimmed = value.trim();
        if trimmed.len() == 0 {
            return match self.count {
                SchemaCount::Many => Some(Value::Array(vec![])),
                SchemaCount::One => None,
            };
        }
        match self.count {
            SchemaCount::Many => {
                let split = trimmed.split(",");
                let mut vec: Vec<Value> = Vec::new();
                for v in split {
                    if let Some(parsed) = self.parse_individual(v) {
                        vec.push(parsed);
                    } else {
                        return None;
                    }
                }
                Some(Value::Array(vec))
            }
            SchemaCount::One => self.parse_individual(trimmed),
        }
    }

    fn parse_individual(&self, value: &str) -> Option<Value> {
        match self.data_type {
            SchemaDataType::Date => {
                let custom_date_formats = ["%Y-%m-%d", "%Y/%m/%d"];

                for &format in custom_date_formats.iter() {
                    if let Ok(_) = NaiveDate::parse_from_str(value, format) {
                        return Some(Value::String(value.to_string()));
                    }
                }
                None
            }
            SchemaDataType::DateTime => {
                if let Ok(_) = DateTime::parse_from_rfc2822(value) {
                    return Some(Value::String(value.to_string()));
                }

                if let Ok(_) = DateTime::parse_from_rfc3339(value) {
                    return Some(Value::String(value.to_string()));
                }

                let custom_datetime_formats = [
                    "%Y-%m-%d %H:%M:%S",
                    "%Y/%m/%d %H:%M:%S",
                    "%Y-%m-%d %H:%M",
                    "%Y/%m/%d %H:%M",
                ];

                for &format in custom_datetime_formats.iter() {
                    if let Ok(_) = NaiveDateTime::parse_from_str(value, format) {
                        return Some(Value::String(value.to_string()));
                    }
                }
                None
            }
            SchemaDataType::Number => Number::from_str(value).map(|v| Value::Number(v)).ok(),
            SchemaDataType::String => Some(Value::String(value.to_string())),
        }
    }
}

#[derive(Deserialize, Serialize)]
pub struct Schema {
    shape: BTreeMap<String, SchemaType>,
}

impl Default for Schema {
    fn default() -> Self {
        Schema {
            shape: BTreeMap::new(),
        }
    }
}

impl IntoIterator for Schema {
    type Item = (String, SchemaType);
    type IntoIter = IntoIter<String, SchemaType>;

    fn into_iter(self) -> Self::IntoIter {
        self.shape.into_iter()
    }
}

impl Schema {
    pub fn insert(&mut self, key: String, value: SchemaType) -> () {
        self.shape.insert(key, value);
    }

    pub fn is_empty(&self) -> bool {
        return self.shape.is_empty();
    }

    pub fn list(config: &Config) -> Vec<String> {
        if let Ok(dir) = fs::read_dir(format!("{}/schema", &config.data_directory())) {
            let names: Vec<String> = dir
                .filter_map(|f| {
                    if let Ok(entry) = f {
                        let file_name = entry.file_name().to_string_lossy().to_string();
                        if file_name.ends_with(".json") {
                            Some(file_name[..file_name.len() - 5].to_string())
                        } else {
                            None
                        }
                    } else {
                        None
                    }
                })
                .collect();
            return names;
        }
        return Vec::new();
    }

    pub fn print(&self) -> () {
        if let Ok(json_str) = serde_json::to_string(&self) {
            println!("{}", json_str);
        }
    }

    pub fn save(&self, path: &str, name: &str) -> Result<(), EntryError> {
        let full_path = format!("{}/{}.json", path, name);
        let result = OpenOptions::new()
            .create(true)
            .write(true)
            .truncate(true)
            .open(full_path);
        match result {
            Ok(file) => {
                let write_result = serde_json::to_writer_pretty(file, &self);
                match write_result {
                    Ok(_) => Ok(()),
                    Err(_) => Err(EntryError::SchemaSaveError),
                }
            }
            Err(_) => Err(EntryError::SchemaSaveError),
        }
    }

    pub fn load(path: &str, name: &str) -> Result<Schema, EntryError> {
        let full_path = format!("{}/{}.json", path, name);
        let result = File::open(full_path);
        return match result {
            Ok(file) => {
                let reader = BufReader::new(file);
                let schema_result = serde_json::from_reader(reader);
                match schema_result {
                    Ok(schema) => Ok(schema),
                    Err(_) => Err(EntryError::SchemaParseError),
                }
            }
            Err(_) => Err(EntryError::SchemaLoadError),
        };
    }

    pub fn remove(path: &str, name: &str) -> Result<(), EntryError> {
        let full_path = format!("{}/{}.json", path, name);
        match fs::remove_file(full_path) {
            Ok(_) => Ok(()),
            Err(_) => Err(EntryError::SchemaRemoveError),
        }
    }
}

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

    #[test]
    fn schema_types_can_parse_individual_data_types() {
        let data_types = [
            (
                SchemaDataType::Date,
                "2021/04/25",
                Value::String("2021/04/25".to_string()),
            ),
            (
                SchemaDataType::DateTime,
                "2021/04/25 11:17:00",
                Value::String("2021/04/25 11:17:00".to_string()),
            ),
            (
                SchemaDataType::Number,
                "20.5",
                Value::Number(Number::from_f64(20.5).unwrap()),
            ),
            (
                SchemaDataType::String,
                "anything531",
                Value::String("anything531".to_string()),
            ),
        ];

        for (data_type, input, value) in data_types.iter() {
            let schema_type = SchemaType {
                count: SchemaCount::One,
                data_type: data_type.clone(),
            };
            assert_eq!(schema_type.parse(*input).unwrap(), value.clone());
        }
    }
}