libpep 0.12.0

Library for polymorphic encryption and pseudonymization
Documentation
//! Macros for creating PEPJSONValue objects with a JSON-like syntax.

/// Macro for creating PEPJSONValue objects with a JSON-like syntax.
///
/// Supports marking fields as pseudonyms using `pseudonym("value")` syntax.
/// Creates an unencrypted `PEPJSONValue` which can then be encrypted.
///
/// # Example
///
/// ```ignore
/// use libpep::pep_json;
/// use serde_json::json;
///
/// let pep_value = pep_json!({
///     "id": pseudonym("user1@example.com"),
///     "age": 16,
///     "verified": true,
///     "scores": [88, 91, 85]
/// });
///
/// // Then encrypt it
/// let encrypted = encrypt(&pep_value, &keys, &mut rng);
/// let decrypted = decrypt(&encrypted, &keys)?;
/// assert_eq!(decrypted, json!({
///     "id": "user1@example.com",
///     "age": 16,
///     "verified": true,
///     "scores": [88, 91, 85]
/// }));
/// ```
#[macro_export]
macro_rules! pep_json {
    // Entry point for standalone pseudonym
    (pseudonym($value:expr)) => {{
        use $crate::data::padding::Padded;
        let value = $value;
        let s_str: &str = value.as_ref();
        // Always try short first, then fall back to long if needed
        match $crate::data::simple::Pseudonym::from_string_padded(s_str) {
            Ok(pseudo) => $crate::data::json::data::PEPJSONValue::Pseudonym(pseudo),
            Err(_) => $crate::data::json::data::PEPJSONValue::LongPseudonym(
                $crate::data::long::LongPseudonym::from_string_padded(s_str)
            )
        }
    }};

    // Entry point for object
    ({ $($tt:tt)* }) => {{
        let builder = $crate::data::json::builder::PEPJSONBuilder::new();
        pep_json!(@object builder, $($tt)*)
    }};

    // Parse object fields - empty
    (@object $builder:ident, ) => {
        $builder.build()
    };

    // Pseudonym field (last field, no trailing comma)
    (@object $builder:ident, $key:literal : pseudonym($value:expr)) => {{
        let value = $value;
        $builder.pseudonym($key, value.as_ref()).build()
    }};

    // Pseudonym field with more fields following
    (@object $builder:ident, $key:literal : pseudonym($value:expr), $($rest:tt)*) => {{
        let value = $value;
        let builder = $builder.pseudonym($key, value.as_ref());
        pep_json!(@object builder, $($rest)*)
    }};

    // Object field (last field, no trailing comma)
    (@object $builder:ident, $key:literal : { $($inner:tt)* }) => {{
        $builder.attribute($key, serde_json::json!({ $($inner)* })).build()
    }};

    // Object field with more fields following
    (@object $builder:ident, $key:literal : { $($inner:tt)* }, $($rest:tt)*) => {{
        let builder = $builder.attribute($key, serde_json::json!({ $($inner)* }));
        pep_json!(@object builder, $($rest)*)
    }};

    // Array field (last field, no trailing comma)
    (@object $builder:ident, $key:literal : [ $($inner:tt)* ]) => {{
        $builder.attribute($key, serde_json::json!([ $($inner)* ])).build()
    }};

    // Array field with more fields following
    (@object $builder:ident, $key:literal : [ $($inner:tt)* ], $($rest:tt)*) => {{
        let builder = $builder.attribute($key, serde_json::json!([ $($inner)* ]));
        pep_json!(@object builder, $($rest)*)
    }};

    // Regular field (last field, no trailing comma)
    (@object $builder:ident, $key:literal : $value:expr) => {{
        $builder.attribute($key, serde_json::json!($value)).build()
    }};

    // Regular field with more fields following
    (@object $builder:ident, $key:literal : $value:expr, $($rest:tt)*) => {{
        let builder = $builder.attribute($key, serde_json::json!($value));
        pep_json!(@object builder, $($rest)*)
    }};
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
    use crate::client::{decrypt, encrypt};
    use crate::factors::contexts::EncryptionContext;
    use crate::factors::EncryptionSecret;
    use crate::keys::{
        make_attribute_global_keys, make_attribute_session_keys, make_pseudonym_global_keys,
        make_pseudonym_session_keys, AttributeSessionKeys, PseudonymSessionKeys, SessionKeys,
    };
    use serde_json::json;

    fn make_test_keys() -> SessionKeys {
        let mut rng = rand::rng();
        let (_, attr_global_secret) = make_attribute_global_keys(&mut rng);
        let (_, pseudo_global_secret) = make_pseudonym_global_keys(&mut rng);
        let enc_secret = EncryptionSecret::from("test-secret".as_bytes().to_vec());
        let session = EncryptionContext::from("session-1");

        let (attr_public, attr_secret) =
            make_attribute_session_keys(&attr_global_secret, &session, &enc_secret);
        let (pseudo_public, pseudo_secret) =
            make_pseudonym_session_keys(&pseudo_global_secret, &session, &enc_secret);

        SessionKeys {
            attribute: AttributeSessionKeys {
                public: attr_public,
                secret: attr_secret,
            },
            pseudonym: PseudonymSessionKeys {
                public: pseudo_public,
                secret: pseudo_secret,
            },
        }
    }

    #[test]
    fn macro_with_pseudonym_id() {
        let mut rng = rand::rng();
        let keys = make_test_keys();

        let pep_value = pep_json!({
            "id": pseudonym("user1@example.com"),
            "age": 16,
            "verified": true,
            "scores": [88, 91, 85]
        });

        let encrypted = encrypt(&pep_value, &keys, &mut rng);
        #[cfg(feature = "elgamal3")]
        let decrypted = decrypt(&encrypted, &keys).unwrap();

        #[cfg(not(feature = "elgamal3"))]
        let decrypted = decrypt(&encrypted, &keys);

        let expected = json!({
            "id": "user1@example.com",
            "age": 16,
            "verified": true,
            "scores": [88, 91, 85]
        });

        assert_eq!(expected, decrypted.to_value().unwrap());
    }

    #[test]
    fn macro_only_attributes() {
        let mut rng = rand::rng();
        let keys = make_test_keys();

        let pep_value = pep_json!({
            "name": "Alice",
            "age": 30
        });

        let encrypted = encrypt(&pep_value, &keys, &mut rng);
        #[cfg(feature = "elgamal3")]
        let decrypted = decrypt(&encrypted, &keys).unwrap();

        #[cfg(not(feature = "elgamal3"))]
        let decrypted = decrypt(&encrypted, &keys);

        let expected = json!({
            "name": "Alice",
            "age": 30
        });

        assert_eq!(expected, decrypted.to_value().unwrap());
    }

    #[test]
    fn macro_empty_object() {
        let mut rng = rand::rng();
        let keys = make_test_keys();

        let pep_value = pep_json!({});

        let encrypted = encrypt(&pep_value, &keys, &mut rng);
        #[cfg(feature = "elgamal3")]
        let decrypted = decrypt(&encrypted, &keys).unwrap();

        #[cfg(not(feature = "elgamal3"))]
        let decrypted = decrypt(&encrypted, &keys);

        assert_eq!(json!({}), decrypted.to_value().unwrap());
    }

    #[test]
    fn macro_multiple_pseudonyms() {
        let mut rng = rand::rng();
        let keys = make_test_keys();

        let pep_value = pep_json!({
            "id1": pseudonym("user1@example.com"),
            "id2": pseudonym("user2@example.com")
        });

        let encrypted = encrypt(&pep_value, &keys, &mut rng);
        #[cfg(feature = "elgamal3")]
        let decrypted = decrypt(&encrypted, &keys).unwrap();

        #[cfg(not(feature = "elgamal3"))]
        let decrypted = decrypt(&encrypted, &keys);

        let expected = json!({
            "id1": "user1@example.com",
            "id2": "user2@example.com"
        });

        assert_eq!(expected, decrypted.to_value().unwrap());
    }

    #[test]
    fn macro_nested_values() {
        let mut rng = rand::rng();
        let keys = make_test_keys();

        let pep_value = pep_json!({
            "user": {"name": "Alice", "active": true},
            "scores": [1, 2, 3]
        });

        let encrypted = encrypt(&pep_value, &keys, &mut rng);
        #[cfg(feature = "elgamal3")]
        let decrypted = decrypt(&encrypted, &keys).unwrap();

        #[cfg(not(feature = "elgamal3"))]
        let decrypted = decrypt(&encrypted, &keys);

        let expected = json!({
            "user": {
                "name": "Alice",
                "active": true
            },
            "scores": [1, 2, 3]
        });

        assert_eq!(expected, decrypted.to_value().unwrap());
    }

    #[test]
    fn macro_with_string_variables() {
        let mut rng = rand::rng();
        let keys = make_test_keys();

        // Test with String variables (not just string literals)
        let user_id = String::from("user@example.com");
        let pep_value = pep_json!({
            "id": pseudonym(user_id)
        });

        let encrypted = encrypt(&pep_value, &keys, &mut rng);
        #[cfg(feature = "elgamal3")]
        let decrypted = decrypt(&encrypted, &keys).unwrap();

        #[cfg(not(feature = "elgamal3"))]
        let decrypted = decrypt(&encrypted, &keys);

        let expected = json!({
            "id": "user@example.com"
        });

        assert_eq!(expected, decrypted.to_value().unwrap());
    }

    #[test]
    fn macro_standalone_pseudonym_with_string() {
        // Test standalone pseudonym with String variable
        let user_id = String::from("test@example.com");
        let pep_value = pep_json!(pseudonym(user_id));

        // Verify it creates the correct variant
        match pep_value {
            crate::data::json::data::PEPJSONValue::Pseudonym(_) => {
                // Expected for short pseudonyms
            }
            crate::data::json::data::PEPJSONValue::LongPseudonym(_) => {
                // Also acceptable if string is long
            }
            _ => panic!("Expected Pseudonym or LongPseudonym variant"),
        }
    }
}