pdk-data-storage-lib 1.6.0

PDK Data Storage Library
Documentation
// Copyright (c) 2025, Salesforce, Inc.,
// All rights reserved.
// For full license text, see the LICENSE.txt file

//! Data models used by the distributed storage client
//!
//! Public types like [`StoreMode`] and [`Store`] are part of the module API.
//! Internal transport/data-transfer structs are hidden from docs.

use pdk_core::logger;

use base64::{DecodeError, Engine};
use std::collections::HashMap;
use std::convert::TryFrom;

use serde::{Deserialize, Serialize};

/// Maximum TTL in seconds (30 days).
const MAX_TTL_SECONDS: u32 = 2592000;

#[derive(PartialEq, Eq, Debug)]
pub enum StoreMode {
    /// Indicates that the store operation does not depend on any condition.
    Always,
    /// Indicates that the store operation should succeed only if no other value was present in the store.
    Absent,
    /// Indicates that the store operation should succeed only if the stored value matches the provided cas.
    Cas(String),
}

#[allow(dead_code)]
#[doc(hidden)]
// Code is actually read in runtime for serialization/deserialization, depending on the response from the OS
#[derive(Deserialize, Debug)]
pub struct Keys {
    pub values: Vec<Key>,
    next_page_token: Option<String>,
}

#[allow(dead_code)]
#[doc(hidden)]
// Code is actually read in runtime for serialization/deserialization, depending on the response from the OS
#[derive(Deserialize, Debug)]
pub struct Key {
    #[serde(rename = "keyId")]
    pub key_id: String,
}

#[allow(dead_code)]
#[doc(hidden)]
// Code is actually read in runtime for serialization/deserialization, depending on the response from the OS
#[derive(Deserialize, Debug)]
pub struct Partitions {
    pub values: Vec<String>,
    next_page_token: Option<String>,
}

#[allow(dead_code)]
#[doc(hidden)]
// Code is actually read in runtime for serialization/deserialization, depending on the response from the OS
#[derive(Deserialize, Debug)]
pub struct Stores {
    pub values: Vec<Store>,
    next_page_token: Option<String>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct Store {
    #[serde(rename = "storeId")]
    store_id: String,

    #[serde(rename = "defaultTtlSeconds")]
    default_ttl_seconds: u32,

    #[serde(rename = "maximumEntries")]
    #[serde(skip_serializing_if = "Option::is_none")]
    max_entries: Option<u32>,
}

#[allow(dead_code)]
impl Store {
    pub fn new(store_id: String, ttl_millis: Option<u32>, max_entries: Option<u32>) -> Self {
        Self {
            store_id,
            default_ttl_seconds: convert_to_seconds(ttl_millis),
            max_entries,
        }
    }

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

    pub fn default_ttl_seconds(&self) -> u32 {
        self.default_ttl_seconds
    }

    pub fn max_entries(&self) -> Option<u32> {
        self.max_entries
    }
}

#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(rename_all = "UPPERCASE")]
enum Types {
    Number,
    Binary,
    String,
}

#[derive(Serialize, Deserialize, Debug)]
#[doc(hidden)]
pub struct Object {
    #[serde(rename = "keyId", skip_serializing_if = "Option::is_none")]
    key_id: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    index: Option<u32>,

    #[serde(skip_serializing_if = "Option::is_none")]
    metadata: Option<HashMap<String, String>>,

    #[serde(rename = "valueType")]
    value_type: Types,

    #[serde(rename = "stringValue", skip_serializing_if = "Option::is_none")]
    string_value: Option<String>,

    #[serde(rename = "numberValue", skip_serializing_if = "Option::is_none")]
    number_value: Option<u64>,

    #[serde(rename = "binaryValue", skip_serializing_if = "Option::is_none")]
    binary_value: Option<String>,
}

impl TryFrom<&[u8]> for Object {
    type Error = serde_json::Error;

    /// Parses error from JSON.
    fn try_from(value: &[u8]) -> serde_json::Result<Self> {
        serde_json::from_slice(value)
    }
}

impl Object {
    pub fn new_binary(value: &[u8]) -> Self {
        let result = base64::engine::general_purpose::STANDARD.encode(value);

        Self {
            key_id: None,
            index: None,
            metadata: None,
            value_type: Types::Binary,
            string_value: None,
            number_value: None,
            binary_value: Some(result),
        }
    }

    pub fn get_binary(&self) -> Result<Vec<u8>, DecodeError> {
        self.binary_value
            .as_ref()
            .map(|binary| base64::engine::general_purpose::STANDARD.decode(binary))
            .unwrap_or(Ok(vec![]))
    }
}

/// Converts TTL from milliseconds to seconds and validates against maximum allowed value.
/// If the value exceeds MAX_TTL_SECONDS, the TTL will be set to the maximum allowed value.
fn convert_to_seconds(ttl_millis: Option<u32>) -> u32 {
    // Convert milliseconds to seconds or set to default max value if not provided.
    let mut ttl_seconds = ttl_millis.map_or(MAX_TTL_SECONDS, |m| m / 1000);

    // If the TTL is greater than the maximum allowed value, set it to the maximum allowed value.
    if ttl_seconds > MAX_TTL_SECONDS {
        logger::warn!("TTL is greater than the maximum allowed value of {MAX_TTL_SECONDS} seconds. Setting TTL to default max value: {MAX_TTL_SECONDS} seconds.");
        ttl_seconds = MAX_TTL_SECONDS;
    }

    ttl_seconds
}

#[cfg(test)]
mod tests {
    use super::{convert_to_seconds, MAX_TTL_SECONDS};

    #[test]
    fn test_convert_to_seconds_normal_ttl_millis() {
        let result = convert_to_seconds(Some(60000));
        assert_eq!(result, 60);
    }

    #[test]
    fn test_convert_to_seconds_large_ttl_sets_max_ttl_value() {
        let large_ttl_millis = (MAX_TTL_SECONDS + 1) * 1000;
        let result = convert_to_seconds(Some(large_ttl_millis));
        assert_eq!(result, MAX_TTL_SECONDS);
    }

    #[test]
    fn test_convert_to_seconds_none_sets_max_ttl_value() {
        let result = convert_to_seconds(None);
        assert_eq!(result, MAX_TTL_SECONDS);
    }
}