cloud-lite-core-rs 0.1.1

Shared utilities for cloud-lite provider crates
Documentation
//! Base64 serde helpers for `format: "byte"` fields.
//!
//! Some API discovery documents mark certain string fields with `"format": "byte"`,
//! meaning their JSON wire format is base64-encoded. These helpers transparently
//! encode on serialization and decode on deserialization, so Rust code works with
//! raw (unencoded) strings.

// These functions are called by serde `with` attributes in generated types.
// Rustc's dead_code lint doesn't see through serde attribute paths.
#![allow(dead_code)]

use base64::{Engine, engine::general_purpose::STANDARD};
use serde::{self, Deserialize, Deserializer, Serializer};

/// Serialize an `Option<String>` as a base64-encoded string.
///
/// `None` is skipped (via `skip_serializing_if` on the field).
/// `Some(value)` is base64-encoded before being written to JSON.
pub fn serialize_base64_opt<S>(value: &Option<String>, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    match value {
        Some(v) => serializer.serialize_str(&STANDARD.encode(v.as_bytes())),
        None => serializer.serialize_none(),
    }
}

/// Deserialize a base64-encoded JSON string into `Option<String>`.
///
/// Decodes the base64 value and returns the raw UTF-8 string.
pub fn deserialize_base64_opt<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
    D: Deserializer<'de>,
{
    let opt: Option<String> = Option::deserialize(deserializer)?;
    match opt {
        Some(encoded) => {
            let bytes = STANDARD
                .decode(&encoded)
                .map_err(serde::de::Error::custom)?;
            let s = String::from_utf8(bytes).map_err(serde::de::Error::custom)?;
            Ok(Some(s))
        }
        None => Ok(None),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde::{Deserialize, Serialize};

    #[derive(Debug, Serialize, Deserialize, PartialEq)]
    struct TestStruct {
        #[serde(
            default,
            skip_serializing_if = "Option::is_none",
            serialize_with = "serialize_base64_opt",
            deserialize_with = "deserialize_base64_opt"
        )]
        body: Option<String>,
    }

    #[test]
    fn serialize_none_omits_field() {
        let t = TestStruct { body: None };
        let json = serde_json::to_string(&t).unwrap();
        assert_eq!(json, "{}");
    }

    #[test]
    fn serialize_some_base64_encodes() {
        let t = TestStruct {
            body: Some(r#"{"key":"value"}"#.into()),
        };
        let json = serde_json::to_string(&t).unwrap();
        let expected_b64 = STANDARD.encode(r#"{"key":"value"}"#.as_bytes());
        assert_eq!(json, format!(r#"{{"body":"{}"}}"#, expected_b64));
    }

    #[test]
    fn deserialize_base64_decodes() {
        let encoded = STANDARD.encode(r#"{"key":"value"}"#.as_bytes());
        let json = format!(r#"{{"body":"{}"}}"#, encoded);
        let t: TestStruct = serde_json::from_str(&json).unwrap();
        assert_eq!(t.body, Some(r#"{"key":"value"}"#.into()));
    }

    #[test]
    fn roundtrip_preserves_value() {
        let original = TestStruct {
            body: Some("hello world".into()),
        };
        let json = serde_json::to_string(&original).unwrap();
        let deserialized: TestStruct = serde_json::from_str(&json).unwrap();
        assert_eq!(original, deserialized);
    }

    #[test]
    fn deserialize_missing_field_gives_none() {
        let json = "{}";
        let t: TestStruct = serde_json::from_str(json).unwrap();
        assert_eq!(t.body, None);
    }

    #[test]
    fn deserialize_invalid_base64_errors() {
        let json = r#"{"body":"not-valid-base64!!!"}"#;
        let result: Result<TestStruct, _> = serde_json::from_str(json);
        assert!(result.is_err());
    }
}