/* * This file is licensed under the Affero General Public License (AGPL) version 3. * * Copyright (C) 2026 Element Creations Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * See the GNU Affero General Public License for more details: * . * */ /// A wrapper type that represents a value that may be missing. /// /// We can't necessarily use `Option` for this, as we want to distinguish /// between a missing value and a value that is present but null (e.g. /// `{"field": null}` vs `{}`). Serde by default treats missing fields as /// `None`, so we need a custom type to capture this distinction. /// /// A plain `AllowMissing` is used for fields that are either present and of /// type `T`, or absent. An `AllowMissing>` is used for fields that /// are of type `T`, null, or absent. /// /// Note, to use this type correctly, the field **MUST** be annotated with: /// /// ```rust /// #[serde( /// default, /// with = "crate::json::allow_missing", /// skip_serializing_if = "AllowMissing::is_absent" /// )] /// ``` /// #[derive(Default, Debug, Clone)] pub enum AllowMissing { Some(T), #[default] Absent, } impl AllowMissing { /// Returns `true` if the value is present, even if it is null. pub fn is_some(&self) -> bool { matches!(self, AllowMissing::Some(_)) } /// Returns `true` if the value is absent. pub fn is_absent(&self) -> bool { matches!(self, AllowMissing::Absent) } /// Converts to `Option`. /// /// Useful for converting e.g. `AllowMissing` to `Option<&str>`. pub fn as_deref_opt(&self) -> Option<&T::Target> where T: std::ops::Deref, { match self { AllowMissing::Some(inner) => Some(inner.deref()), AllowMissing::Absent => None, } } /// Converts to `Option<&T>`. pub fn as_ref_opt(&self) -> Option<&T> { match self { AllowMissing::Some(inner) => Some(inner), AllowMissing::Absent => None, } } } /// A module that provides the serialization and deserialization logic for /// `AllowMissing`. pub mod allow_missing { use serde::ser::Error as _; use super::AllowMissing; pub fn deserialize<'de, T, D>(deserializer: D) -> Result, D::Error> where T: serde::Deserialize<'de>, D: serde::Deserializer<'de>, { Ok(AllowMissing::Some(T::deserialize(deserializer)?)) } pub fn serialize(value: &AllowMissing, serializer: S) -> Result where T: serde::Serialize, S: serde::Serializer, { match value { AllowMissing::Some(inner) => inner.serialize(serializer), // We should never attempt to serialize an `AllowMissing::Absent`, as we // should have skipped it with `skip_serializing_if`. AllowMissing::Absent => Err(S::Error::custom("cannot serialize AllowMissing::Absent")), } } } #[cfg(test)] mod tests { use std::assert_matches; use serde::{Deserialize, Serialize}; use super::*; #[derive(Serialize, Deserialize)] struct TestStruct { #[serde( default, with = "crate::json::allow_missing", skip_serializing_if = "AllowMissing::is_absent" )] value: AllowMissing, } #[test] fn test_deserialize() { let json = r#"{"value":42}"#; let deserialized: TestStruct = serde_json::from_str(json).unwrap(); assert!(deserialized.value.is_some()); assert_matches!(deserialized.value, AllowMissing::Some(42)); let json = r#"{}"#; let deserialized: TestStruct = serde_json::from_str(json).unwrap(); assert!(deserialized.value.is_absent()); assert_matches!(deserialized.value, AllowMissing::Absent); } #[test] fn test_serialize() { let value = TestStruct { value: AllowMissing::Some(42), }; let serialized = serde_json::to_string(&value).unwrap(); assert_eq!(serialized, r#"{"value":42}"#); let value = TestStruct { value: AllowMissing::Absent, }; let serialized = serde_json::to_string(&value).unwrap(); assert_eq!(serialized, r#"{}"#); } /// Test that we get an error if we attempt to serialize an /// `AllowMissing::Absent` without the skip_serializing_if annotation. #[test] fn test_serialize_absent_error() { #[derive(Serialize)] struct TestStructWithoutSkip { #[serde(default, with = "crate::json::allow_missing")] value: AllowMissing, } let value = TestStructWithoutSkip { value: AllowMissing::Absent, }; let err = serde_json::to_string(&value).unwrap_err(); assert_eq!(err.to_string(), "cannot serialize AllowMissing::Absent"); } #[derive(Serialize, Deserialize)] struct TestStructOption { #[serde( default, with = "crate::json::allow_missing", skip_serializing_if = "AllowMissing::is_absent" )] value: AllowMissing>, } #[test] fn test_serialize_option() { let value = TestStructOption { value: AllowMissing::Some(Some(42)), }; let serialized = serde_json::to_string(&value).unwrap(); assert_eq!(serialized, r#"{"value":42}"#); let value = TestStructOption { value: AllowMissing::Some(None), }; let serialized = serde_json::to_string(&value).unwrap(); assert_eq!(serialized, r#"{"value":null}"#); let value = TestStructOption { value: AllowMissing::Absent, }; let serialized = serde_json::to_string(&value).unwrap(); assert_eq!(serialized, r#"{}"#); } #[test] fn test_deserialize_option() { let json = r#"{"value":42}"#; let deserialized: TestStructOption = serde_json::from_str(json).unwrap(); assert!(deserialized.value.is_some()); assert_matches!(deserialized.value, AllowMissing::Some(Some(42))); let json = r#"{"value":null}"#; let deserialized: TestStructOption = serde_json::from_str(json).unwrap(); assert!(deserialized.value.is_some()); assert_matches!(deserialized.value, AllowMissing::Some(None)); let json = r#"{}"#; let deserialized: TestStructOption = serde_json::from_str(json).unwrap(); assert!(deserialized.value.is_absent()); assert_matches!(deserialized.value, AllowMissing::Absent); } }