saddle-framework 0.3.0-alpha.7

The single business-facing facade for Saddle applications
use serde::Serialize;
use serde::ser::{Error as _, SerializeStruct, Serializer};
use std::fmt;

pub const MAX_FAILURE_MESSAGE_BYTES: usize = 256;

/// Open runtime strings cannot act as registered codes:
///
/// ```compile_fail
/// use saddle::ingress::{FailureMessage, ProfuseGwFailure, ProfuseGwResponse};
/// let response = ProfuseGwResponse::<(), String>::failure(ProfuseGwFailure::new(
///     "dynamic".to_owned(),
///     FailureMessage::new("display only").unwrap(),
/// ));
/// let _ = serde_json::to_string(&response);
/// ```
pub trait ProfuseGwCode {
    const REGISTERED_CODES: &'static [&'static str];
    fn stable_code(&self) -> &'static str;
}

/// Framework-owned bounded display text. It is not machine-readable.
///
/// ```compile_fail
/// use saddle::ingress::{ProfuseGwFailure, ProfuseGwResponse};
/// enum Code { Rejected }
/// let _ = ProfuseGwResponse::<(), Code>::failure(
///     ProfuseGwFailure::new(Code::Rejected, String::new()),
/// );
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FailureMessage(String);

impl FailureMessage {
    pub fn new(value: impl AsRef<str>) -> Result<Self, FailureMessageError> {
        let value = value.as_ref();
        if value.len() > MAX_FAILURE_MESSAGE_BYTES {
            return Err(FailureMessageError::TooLong);
        }
        Ok(Self(value.to_owned()))
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FailureMessageError {
    TooLong,
}

impl fmt::Display for FailureMessageError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str("Profusegw failure message exceeds its byte limit")
    }
}

impl std::error::Error for FailureMessageError {}

pub struct ProfuseGwFailure<Code> {
    code: Code,
    message: FailureMessage,
}

impl<Code> ProfuseGwFailure<Code> {
    pub fn new(code: Code, message: FailureMessage) -> Self {
        Self { code, message }
    }

    pub fn code(&self) -> &Code {
        &self.code
    }

    pub fn message(&self) -> &FailureMessage {
        &self.message
    }
}

impl<Code: ProfuseGwCode> Serialize for ProfuseGwFailure<Code> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let code = self.code.stable_code();
        if !Code::REGISTERED_CODES.contains(&code) {
            return Err(S::Error::custom("Profusegw code is not registered"));
        }
        let mut state = serializer.serialize_struct("ProfuseGwFailure", 2)?;
        state.serialize_field("code", code)?;
        state.serialize_field("message", self.message.as_str())?;
        state.end()
    }
}

/// The transport-independent response has exactly two legal branches.
///
/// ```compile_fail
/// use saddle::ingress::ProfuseGwResponse;
/// let _ = ProfuseGwResponse::<(), ()> { branch: unreachable!() };
/// ```
///
/// ```compile_fail
/// use saddle::ingress::ProfuseGwResponse;
/// use std::future::Future;
/// enum Code { Rejected }
/// async fn bad_handler() -> Result<ProfuseGwResponse<(), Code>, ()> { unreachable!() }
/// fn require_response<F: Future<Output = ProfuseGwResponse<(), Code>>>(_: F) {}
/// require_response(bad_handler());
/// ```
pub struct ProfuseGwResponse<Data, Code> {
    branch: ResponseBranch<Data, Code>,
}

enum ResponseBranch<Data, Code> {
    Success { data: Data },
    Failure { failure: ProfuseGwFailure<Code> },
}

impl<Data, Code> ProfuseGwResponse<Data, Code> {
    pub fn success(data: Data) -> Self {
        Self {
            branch: ResponseBranch::Success { data },
        }
    }

    pub fn failure(failure: ProfuseGwFailure<Code>) -> Self {
        Self {
            branch: ResponseBranch::Failure { failure },
        }
    }

    pub fn success_data(&self) -> Option<&Data> {
        match &self.branch {
            ResponseBranch::Success { data } => Some(data),
            ResponseBranch::Failure { .. } => None,
        }
    }

    pub fn failure_value(&self) -> Option<&ProfuseGwFailure<Code>> {
        match &self.branch {
            ResponseBranch::Success { .. } => None,
            ResponseBranch::Failure { failure } => Some(failure),
        }
    }
}

impl<Data: Serialize, Code: ProfuseGwCode> Serialize for ProfuseGwResponse<Data, Code> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match &self.branch {
            ResponseBranch::Success { data } => {
                let mut state = serializer.serialize_struct("ProfuseGwResponse", 2)?;
                state.serialize_field("success", &true)?;
                state.serialize_field("data", data)?;
                state.end()
            }
            ResponseBranch::Failure { failure } => {
                let mut state = serializer.serialize_struct("ProfuseGwResponse", 2)?;
                state.serialize_field("success", &false)?;
                state.serialize_field("failure", failure)?;
                state.end()
            }
        }
    }
}

const fn strings_equal(left: &str, right: &str) -> bool {
    let left = left.as_bytes();
    let right = right.as_bytes();
    if left.len() != right.len() {
        return false;
    }
    let mut index = 0;
    while index < left.len() {
        if left[index] != right[index] {
            return false;
        }
        index += 1;
    }
    true
}

#[doc(hidden)]
pub const fn assert_unique_code_registries(registries: &[&[&str]]) {
    let mut group = 0;
    while group < registries.len() {
        let mut item = 0;
        while item < registries[group].len() {
            let code = registries[group][item];
            assert!(!code.is_empty(), "Profusegw code must not be empty");
            let mut other_group = group;
            let mut other_item = item + 1;
            while other_group < registries.len() {
                while other_item < registries[other_group].len() {
                    assert!(
                        !strings_equal(code, registries[other_group][other_item]),
                        "duplicate Profusegw code"
                    );
                    other_item += 1;
                }
                other_group += 1;
                other_item = 0;
            }
            item += 1;
        }
        group += 1;
    }
}

/// Duplicate combined codes fail during application compilation:
///
/// ```compile_fail
/// const _: () = saddle::__private::assert_unique_code_registries(&[
///     &["COLLISION"],
///     &["COLLISION"],
/// ]);
/// ```
const _: () = ();

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

    #[derive(Serialize)]
    struct Data {
        count: u64,
    }
    enum Code {
        Rejected,
    }
    impl ProfuseGwCode for Code {
        const REGISTERED_CODES: &'static [&'static str] = &["ACCOUNT_REJECTED"];
        fn stable_code(&self) -> &'static str {
            match self {
                Self::Rejected => "ACCOUNT_REJECTED",
            }
        }
    }

    #[test]
    fn exact_success_and_nested_failure_json_are_mutually_exclusive() {
        let success = ProfuseGwResponse::<_, Code>::success(Data { count: 7 });
        assert_eq!(
            serde_json::to_string(&success).unwrap(),
            r#"{"success":true,"data":{"count":7}}"#
        );
        let failure = ProfuseGwResponse::<Data, _>::failure(ProfuseGwFailure::new(
            Code::Rejected,
            FailureMessage::new("Account was rejected").unwrap(),
        ));
        assert_eq!(
            serde_json::to_string(&failure).unwrap(),
            r#"{"success":false,"failure":{"code":"ACCOUNT_REJECTED","message":"Account was rejected"}}"#
        );
    }

    #[test]
    fn message_is_bounded_and_registry_is_unique() {
        assert!(FailureMessage::new("a".repeat(MAX_FAILURE_MESSAGE_BYTES)).is_ok());
        assert_eq!(
            FailureMessage::new("".repeat(MAX_FAILURE_MESSAGE_BYTES)).unwrap_err(),
            FailureMessageError::TooLong
        );
        assert_unique_code_registries(&[Code::REGISTERED_CODES]);
    }

    #[test]
    #[should_panic(expected = "duplicate Profusegw code")]
    fn duplicate_combined_code_is_rejected() {
        assert_unique_code_registries(&[&["DUPLICATE"], &["DUPLICATE"]]);
    }
}