znote 1.0.0

A lightweight desktop editor for Typst with live preview, built with Tauri
use std::sync::LazyLock;

use serde::{Deserialize, Serialize};

use crate::knowledge::STRICT;

const BUILT_IN: &str = include_str!("../resources/policy.json");

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum Rule {
    Optional,
    Required,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum Web {
    Allowed,
    Blocked,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum Collection {
    Allow,
    Deny,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Policy {
    #[serde(rename = "$schema", default, skip_serializing)]
    schema: Option<String>,
    pub hide_names: Rule,
    pub find_names: Rule,
    pub hide_amounts: Rule,
    pub minimum_level: u8,
    pub web: Web,
    pub providers: Vec<String>,
    pub data_collection: Collection,
    pub zero_retention: bool,
}

impl Policy {
    fn strictest() -> Self {
        Self {
            schema: None,
            hide_names: Rule::Required,
            find_names: Rule::Optional,
            hide_amounts: Rule::Required,
            minimum_level: STRICT,
            web: Web::Blocked,
            providers: Vec::new(),
            data_collection: Collection::Deny,
            zero_retention: true,
        }
    }

    pub fn parse(text: &str) -> Result<Self, String> {
        let policy: Self = serde_json::from_str(text).map_err(|error| error.to_string())?;
        if policy.minimum_level > STRICT {
            return Err("minimumLevel is above the strictest level".into());
        }
        Ok(policy)
    }

    pub fn allows(&self, provider: &str) -> bool {
        self.providers.iter().any(|allowed| allowed == provider)
    }

    pub fn web(&self) -> bool {
        self.web == Web::Allowed
    }
}

static POLICY: LazyLock<Policy> =
    LazyLock::new(|| Policy::parse(BUILT_IN).unwrap_or_else(|_| Policy::strictest()));

pub fn get() -> &'static Policy {
    &POLICY
}

#[tauri::command]
pub fn privacy_policy() -> Policy {
    get().clone()
}

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

    #[test]
    fn the_built_in_policy_is_read_and_leaves_hiding_to_the_user() {
        let policy = Policy::parse(BUILT_IN).unwrap();
        assert_eq!(policy.hide_names, Rule::Optional);
        assert_eq!(policy.data_collection, Collection::Deny);
        assert!(policy.allows("claude") && policy.web());
        assert_eq!(get(), &policy);
    }

    #[test]
    fn a_broken_policy_is_refused_and_an_unreadable_one_hides_everything() {
        for broken in [
            "{}",
            r#"{"hideNames":"maybe"}"#,
            &BUILT_IN.replace("\"minimumLevel\": 0", "\"minimumLevel\": 7"),
            &BUILT_IN.replace("\"web\"", "\"wbe\""),
        ] {
            assert!(Policy::parse(broken).is_err(), "{broken}");
        }
        let fallback = Policy::strictest();
        assert_eq!(fallback.hide_names, Rule::Required);
        assert!(!fallback.web() && !fallback.allows("claude"));
    }
}