thread-rule-engine 0.1.0

Rule-based scanning and transformation engine for Thread
Documentation
// SPDX-FileCopyrightText: 2022 Herrington Darkholme <2883231+HerringtonDarkholme@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 Knitli Inc. <knitli@knit.li>
// SPDX-FileContributor: Adam Poulemanos <adam@knit.li>
//
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT

use schemars::{JsonSchema, Schema, SchemaGenerator};
use serde::{Deserialize, Serialize, de, ser};
use std::borrow::Cow;

#[derive(Clone, PartialEq, Eq, Copy, Default, Debug)]
pub enum Maybe<T> {
    #[default]
    Absent,
    Present(T),
}

impl<T> Maybe<T> {
    pub fn is_present(&self) -> bool {
        matches!(self, Maybe::Present(_))
    }
    pub fn is_absent(&self) -> bool {
        matches!(self, Maybe::Absent)
    }
    pub fn unwrap(self) -> T {
        match self {
            Maybe::Absent => panic!("called `Maybe::unwrap()` on an `Absent` value"),
            Maybe::Present(t) => t,
        }
    }
}

impl<T> From<Maybe<T>> for Option<T> {
    fn from(maybe: Maybe<T>) -> Self {
        match maybe {
            Maybe::Present(v) => Some(v),
            Maybe::Absent => None,
        }
    }
}

impl<T> From<Option<T>> for Maybe<T> {
    fn from(opt: Option<T>) -> Maybe<T> {
        match opt {
            Some(v) => Maybe::Present(v),
            None => Maybe::Absent,
        }
    }
}

const ERROR_STR: &str = r#"Maybe fields need to be annotated with:
  #[serde(default, skip_serializing_if = "Maybe::is_absent")]"#;

impl<T: Serialize> Serialize for Maybe<T> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        match self {
            Maybe::Absent => Err(ser::Error::custom(ERROR_STR)),
            Maybe::Present(t) => T::serialize(t, serializer),
        }
    }
}

impl<'de, T: Deserialize<'de>> Deserialize<'de> for Maybe<T> {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        match Option::deserialize(deserializer)? {
            Some(t) => Ok(Maybe::Present(t)),
            None => Err(de::Error::custom("Maybe field cannot be null.")),
        }
    }
}

impl<T: JsonSchema> JsonSchema for Maybe<T> {
    fn schema_name() -> Cow<'static, str> {
        Cow::Owned(format!("Maybe<{}>", T::schema_name()))
    }
    fn schema_id() -> Cow<'static, str> {
        Cow::Owned(format!("Maybe<{}>", T::schema_id()))
    }
    fn json_schema(generator: &mut SchemaGenerator) -> Schema {
        generator.subschema_for::<T>()
    }

    fn _schemars_private_non_optional_json_schema(generator: &mut SchemaGenerator) -> Schema {
        T::_schemars_private_non_optional_json_schema(generator)
    }

    fn _schemars_private_is_option() -> bool {
        true
    }
}

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

    #[derive(Serialize, Deserialize, Debug, Clone)]
    struct Correct {
        #[serde(default, skip_serializing_if = "Maybe::is_absent")]
        a: Maybe<i32>,
    }
    #[derive(Serialize, Deserialize, Debug, Clone)]
    struct Wrong {
        #[serde(skip_serializing_if = "Maybe::is_absent")]
        a: Maybe<i32>,
    }

    #[test]
    fn test_de_correct_ok() {
        let correct: Correct = from_str("a: 123").expect("should ok");
        assert!(matches!(correct.a, Maybe::Present(123)));
        let correct: Correct = from_str("").expect("should ok");
        assert!(matches!(correct.a, Maybe::Absent));
    }
    #[test]
    fn test_de_correct_err() {
        let ret: Result<Correct, _> = from_str("a:");
        assert!(ret.is_err());
        let err = ret.unwrap_err().to_string();
        assert!(err.contains("cannot be null"));
    }
    #[test]
    fn test_de_wrong_err() {
        let wrong: Wrong = from_str("a: 123").expect("should ok");
        assert!(matches!(wrong.a, Maybe::Present(123)));
        let wrong: Result<Wrong, _> = from_str("a:");
        assert!(wrong.is_err());
        let wrong: Result<Wrong, _> = from_str("");
        assert!(wrong.is_err());
    }

    #[test]
    #[should_panic]
    fn test_unwrap_absent() {
        let nothing: Maybe<()> = Maybe::Absent;
        nothing.unwrap();
    }

    #[test]
    fn test_from_option() {
        let mut maybe = Maybe::from(None);
        assert!(maybe.is_absent());
        maybe = Maybe::from(Some(123));
        assert!(maybe.is_present());
    }
}