durable-actors 0.4.0

Standalone regional durable-actors control plane, host, and durability runtime
Documentation
use crate::{actor::ActorKey, actor_state::ActorStorageKey, host::HostId};
use anyhow::{Context, Result, ensure};
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};

pub const ROOT: &str = "durable-actors/v3/";

pub fn snapshots(actor: &ActorKey) -> Result<String> {
    actor.validate()?;
    Ok(format!("{ROOT}snapshots/{}/", actor_path(actor)))
}

pub fn owner(object: &ActorStorageKey) -> Result<String> {
    let actor = actor_from_key(object)?;
    Ok(format!("{ROOT}owners/{}.json", actor_path(&actor)))
}

pub fn host(host: &HostId) -> String {
    format!("{ROOT}hosts/{}/", component(host.as_str()))
}

pub fn session(host_id: &HostId, session: &str) -> String {
    format!("{}sessions/{}", host(host_id), component(session))
}

pub fn actor_from_snapshot(object: &str) -> Result<ActorKey> {
    let parts: Vec<_> = object
        .strip_prefix(ROOT)
        .context("invalid snapshot root")?
        .split('/')
        .collect();
    ensure!(
        parts.len() == 7 && parts[0] == "snapshots",
        "invalid snapshot path"
    );
    let actor = ActorKey {
        project_id: decode(parts[2])?,
        actor_name: decode(parts[3])?,
        actor_id: decode(parts[4])?,
    };
    actor.validate()?;
    ensure!(
        object.starts_with(&snapshots(&actor)?),
        "snapshot shard mismatch"
    );
    Ok(actor)
}

pub fn actor_from_key(key: &ActorStorageKey) -> Result<ActorKey> {
    let parts: Vec<_> = key
        .as_str()
        .strip_prefix("object.v4.")
        .context("invalid actor key")?
        .split(':')
        .collect();
    ensure!(parts.len() == 3, "invalid actor identity");
    let actor = ActorKey {
        project_id: parts[0].into(),
        actor_name: parts[1].into(),
        actor_id: parts[2].into(),
    };
    actor.validate()?;
    Ok(actor)
}

fn actor_path(actor: &ActorKey) -> String {
    let hash = aws_lc_rs::digest::digest(
        &aws_lc_rs::digest::SHA256,
        actor.storage_key().as_str().as_bytes(),
    );
    format!(
        "{:02x}/{}/{}/{}",
        hash.as_ref()[0],
        component(&actor.project_id),
        component(&actor.actor_name),
        component(&actor.actor_id)
    )
}

fn component(value: &str) -> String {
    URL_SAFE_NO_PAD.encode(value)
}
fn decode(value: &str) -> Result<String> {
    Ok(String::from_utf8(URL_SAFE_NO_PAD.decode(value)?)?)
}