use serde::Serialize;
use serde::ser::{Error as _, SerializeStruct, Serializer};
use std::fmt;
pub const MAX_FAILURE_MESSAGE_BYTES: usize = 256;
pub trait ProfuseGwCode {
const REGISTERED_CODES: &'static [&'static str];
fn stable_code(&self) -> &'static str;
}
#[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()
}
}
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;
}
}
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"]]);
}
}