use std::error::Error;
use std::fmt;
use std::marker::PhantomData;
use std::num::NonZeroU32;
use std::sync::Arc;
use serde_json::Value;
use sha2::{Digest, Sha256};
use crate::definition::ComponentStreamIdentity;
use crate::state::{json_depth, upgrade_schema_chain};
use crate::{
DurableStateKind, StateCodecError, StateError, StateLimits, StateSchemaId, StateSchemaVersion,
VersionedStateCodec,
};
const MAX_TOKEN_BYTES: usize = 128;
const MAX_UPGRADE_CHAIN: usize = 64;
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct CodecId(String);
impl CodecId {
pub fn new(value: impl Into<String>) -> Result<Self, ComponentStateError> {
let value = value.into();
if value.is_empty() {
return Err(ComponentStateError::EmptyCodecId);
}
if value.len() > MAX_TOKEN_BYTES {
return Err(ComponentStateError::CodecIdTooLong {
max_bytes: MAX_TOKEN_BYTES,
});
}
if value.trim() != value || value.chars().any(char::is_control) {
return Err(ComponentStateError::Malformed);
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Debug for CodecId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_tuple("CodecId").field(&self.0).finish()
}
}
impl fmt::Display for CodecId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct CodecVersion(NonZeroU32);
impl CodecVersion {
pub fn new(value: u32) -> Result<Self, ComponentStateError> {
NonZeroU32::new(value)
.map(Self)
.ok_or(ComponentStateError::ZeroCodecVersion)
}
#[must_use]
pub const fn get(self) -> u32 {
self.0.get()
}
}
#[derive(Clone, Copy)]
pub struct CodecVersionUpgrade {
from: CodecVersion,
to: CodecVersion,
apply: fn(&[u8]) -> Result<Vec<u8>, StateCodecError>,
}
impl CodecVersionUpgrade {
pub fn new(
from: CodecVersion,
to: CodecVersion,
apply: fn(&[u8]) -> Result<Vec<u8>, StateCodecError>,
) -> Result<Self, ComponentStateError> {
if to <= from {
return Err(ComponentStateError::NonIncreasingCodecUpgrade);
}
Ok(Self { from, to, apply })
}
#[must_use]
pub const fn from(&self) -> CodecVersion {
self.from
}
#[must_use]
pub const fn to(&self) -> CodecVersion {
self.to
}
}
impl fmt::Debug for CodecVersionUpgrade {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CodecVersionUpgrade")
.field("from", &self.from)
.field("to", &self.to)
.finish_non_exhaustive()
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum ChecksumAlgorithm {
Sha256,
}
impl ChecksumAlgorithm {
pub const CURRENT: Self = Self::Sha256;
const fn identifier(self) -> u16 {
match self {
Self::Sha256 => 1,
}
}
const fn algorithm_version(self) -> u16 {
match self {
Self::Sha256 => 1,
}
}
const fn resolve(identifier: u16, version: u16) -> Option<Self> {
match (identifier, version) {
(1, 1) => Some(Self::Sha256),
_ => None,
}
}
fn digest(self, input: &[u8]) -> [u8; 32] {
match self {
Self::Sha256 => Sha256::digest(input).into(),
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum StateSensitivity {
#[default]
Sensitive,
NonSensitive,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum RestartabilityDeclaration {
Restartable,
NotRestartable,
}
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct ContentIdentity([u8; 32]);
impl ContentIdentity {
#[must_use]
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
#[must_use]
pub fn of(bytes: &[u8]) -> Self {
Self(Sha256::digest(bytes).into())
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
impl fmt::Debug for ContentIdentity {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "ContentIdentity({})", hex_encode(&self.0))
}
}
fn hex_encode(bytes: &[u8]) -> String {
use fmt::Write as _;
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
let _ = write!(out, "{byte:02x}");
}
out
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ExternalStateReference {
content_id: ContentIdentity,
encoded_len: u64,
}
impl ExternalStateReference {
#[must_use]
pub const fn new(content_id: ContentIdentity, encoded_len: u64) -> Self {
Self {
content_id,
encoded_len,
}
}
#[must_use]
pub const fn content_id(&self) -> ContentIdentity {
self.content_id
}
#[must_use]
pub const fn encoded_len(&self) -> u64 {
self.encoded_len
}
pub fn verify(&self, bytes: &[u8]) -> Result<(), ComponentStateError> {
if ContentIdentity::of(bytes) == self.content_id {
Ok(())
} else {
Err(ComponentStateError::ExternalReferenceContentMismatch)
}
}
}
pub trait ExternalStateStore: Send + Sync {
fn resolve<'a>(
&'a self,
reference: &'a ExternalStateReference,
) -> impl Future<Output = Result<Vec<u8>, ExternalStateError>> + Send + 'a;
fn store<'a>(
&'a self,
bytes: &'a [u8],
) -> impl Future<Output = Result<ExternalStateReference, ExternalStateError>> + Send + 'a;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ExternalStateError {
ContentMissing,
ContentMismatch,
StoreFailed,
}
impl fmt::Display for ExternalStateError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::ContentMissing => "external component state content is missing",
Self::ContentMismatch => "external component state content identity mismatch",
Self::StoreFailed => "external component state store failed",
})
}
}
impl Error for ExternalStateError {}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ComponentStatePayload {
Inline(Vec<u8>),
External(ExternalStateReference),
}
#[derive(Clone, Eq, PartialEq)]
enum StoredPayload {
Inline(Vec<u8>),
External(ExternalStateReference),
}
#[derive(Clone, Eq, PartialEq)]
pub struct ComponentStateEnvelope {
namespace: ComponentStreamIdentity,
schema_id: StateSchemaId,
schema_version: StateSchemaVersion,
codec_id: CodecId,
codec_version: CodecVersion,
checksum_algorithm: ChecksumAlgorithm,
checksum: [u8; 32],
payload: StoredPayload,
encoded_len: usize,
}
fn canonical_checksum_input(
namespace: &ComponentStreamIdentity,
schema_id: &StateSchemaId,
schema_version: StateSchemaVersion,
codec_id: &CodecId,
codec_version: CodecVersion,
payload_marker: &[u8],
) -> Vec<u8> {
let mut input = Vec::with_capacity(64 + payload_marker.len());
input.extend_from_slice(b"oxide-batch.component-state\0");
input.extend_from_slice(namespace.as_str().as_bytes());
input.push(0);
input.extend_from_slice(schema_id.as_str().as_bytes());
input.push(0);
input.extend_from_slice(&schema_version.get().to_be_bytes());
input.extend_from_slice(codec_id.as_str().as_bytes());
input.push(0);
input.extend_from_slice(&codec_version.get().to_be_bytes());
input.extend_from_slice(payload_marker);
input
}
fn inline_payload_marker(bytes: &[u8]) -> Vec<u8> {
let mut marker = Vec::with_capacity(1 + bytes.len());
marker.push(0);
marker.extend_from_slice(bytes);
marker
}
fn external_payload_marker(reference: &ExternalStateReference) -> Vec<u8> {
let mut marker = Vec::with_capacity(1 + 32 + 8);
marker.push(1);
marker.extend_from_slice(reference.content_id().as_bytes());
marker.extend_from_slice(&reference.encoded_len().to_be_bytes());
marker
}
fn check_upgraded_component_state(payload: &[u8]) -> Result<(), ComponentStateError> {
const HARD_MAXIMUM_BYTES: usize = 1024 * 1024;
const HARD_MAXIMUM_DEPTH: usize = 64;
if payload.len() > HARD_MAXIMUM_BYTES {
return Err(ComponentStateError::TooLarge {
max_bytes: HARD_MAXIMUM_BYTES,
});
}
let value: Value =
serde_json::from_slice(payload).map_err(|_| ComponentStateError::PayloadNotObject)?;
if !value.is_object() {
return Err(ComponentStateError::PayloadNotObject);
}
if json_depth(&value) > HARD_MAXIMUM_DEPTH {
return Err(ComponentStateError::TooDeep {
max_depth: HARD_MAXIMUM_DEPTH,
});
}
Ok(())
}
fn upgrade_codec_chain<T>(
recorded: CodecVersion,
codec: &(impl ComponentStateCodec<T> + ?Sized),
mut payload: Vec<u8>,
) -> Result<Vec<u8>, ComponentStateError> {
let current = codec.codec_version();
let upgrades = codec.codec_upgrades();
let mut version = recorded;
let mut applied = 0_usize;
while version < current {
let mut edges = upgrades.iter().filter(|upgrade| upgrade.from == version);
let edge = edges
.next()
.ok_or(ComponentStateError::NoCodecUpgradePath {
found: version.get(),
current: current.get(),
})?;
if edges.next().is_some() {
return Err(ComponentStateError::AmbiguousCodecUpgrade {
from: version.get(),
});
}
if edge.to > current {
return Err(ComponentStateError::CodecUpgradeOvershootsCurrent {
to: edge.to.get(),
current: current.get(),
});
}
applied += 1;
if applied > MAX_UPGRADE_CHAIN {
return Err(ComponentStateError::CodecUpgradeChainTooLong {
max_upgrades: MAX_UPGRADE_CHAIN,
});
}
payload = (edge.apply)(&payload).map_err(ComponentStateError::Codec)?;
check_upgraded_component_state(&payload)?;
version = edge.to;
}
Ok(payload)
}
fn map_schema_chain_error(error: &StateError) -> ComponentStateError {
match error {
StateError::SchemaMismatch { .. } => ComponentStateError::SchemaMismatch,
StateError::UnsupportedSchemaVersion { found, current, .. } => {
ComponentStateError::UnsupportedSchemaVersion {
found: found.get(),
current: current.get(),
}
}
StateError::NoUpgradePath { found, current, .. } => {
ComponentStateError::NoSchemaUpgradePath {
found: found.get(),
current: current.get(),
}
}
StateError::AmbiguousUpgrade { from, .. } => {
ComponentStateError::AmbiguousSchemaUpgrade { from: from.get() }
}
StateError::UpgradeOvershootsCurrent { to, current, .. } => {
ComponentStateError::SchemaUpgradeOvershootsCurrent {
to: to.get(),
current: current.get(),
}
}
StateError::UpgradeChainTooLong { max_upgrades, .. } => {
ComponentStateError::SchemaUpgradeChainTooLong {
max_upgrades: *max_upgrades,
}
}
StateError::TooLarge { max_bytes, .. } => ComponentStateError::TooLarge {
max_bytes: *max_bytes,
},
StateError::TooDeep { max_depth, .. } => ComponentStateError::TooDeep {
max_depth: *max_depth,
},
StateError::Codec(inner) => ComponentStateError::Codec(*inner),
_ => ComponentStateError::Malformed,
}
}
impl ComponentStateEnvelope {
pub fn encode<T>(
namespace: ComponentStreamIdentity,
value: &T,
codec: &(impl ComponentStateCodec<T> + ?Sized),
limits: StateLimits,
) -> Result<Self, ComponentStateError> {
let payload_bytes = codec.encode(value).map_err(ComponentStateError::Codec)?;
if payload_bytes.len() > limits.maximum_bytes() {
return Err(ComponentStateError::TooLarge {
max_bytes: limits.maximum_bytes(),
});
}
let schema_id = codec.schema_id().clone();
let schema_version = codec.current_version();
let codec_id = codec.codec_id().clone();
let codec_version = codec.codec_version();
let checksum_algorithm = ChecksumAlgorithm::CURRENT;
let checksum = checksum_algorithm.digest(&canonical_checksum_input(
&namespace,
&schema_id,
schema_version,
&codec_id,
codec_version,
&inline_payload_marker(&payload_bytes),
));
let payload_value: Value = serde_json::from_slice(&payload_bytes)
.map_err(|_| ComponentStateError::InvalidPayload)?;
if !payload_value.is_object() {
return Err(ComponentStateError::PayloadNotObject);
}
if json_depth(&payload_value) > limits.maximum_depth() {
return Err(ComponentStateError::TooDeep {
max_depth: limits.maximum_depth(),
});
}
let encoded_len = payload_bytes.len();
Ok(Self {
namespace,
schema_id,
schema_version,
codec_id,
codec_version,
checksum_algorithm,
checksum,
payload: StoredPayload::Inline(payload_bytes),
encoded_len,
})
}
#[must_use]
pub fn external(
namespace: ComponentStreamIdentity,
schema_id: StateSchemaId,
schema_version: StateSchemaVersion,
codec_id: CodecId,
codec_version: CodecVersion,
reference: ExternalStateReference,
) -> Self {
let checksum_algorithm = ChecksumAlgorithm::CURRENT;
let checksum = checksum_algorithm.digest(&canonical_checksum_input(
&namespace,
&schema_id,
schema_version,
&codec_id,
codec_version,
&external_payload_marker(&reference),
));
Self {
namespace,
schema_id,
schema_version,
codec_id,
codec_version,
checksum_algorithm,
checksum,
payload: StoredPayload::External(reference),
encoded_len: 0,
}
}
#[allow(clippy::too_many_arguments)]
pub fn from_durable(
namespace: ComponentStreamIdentity,
schema_id: &str,
schema_version: u32,
codec_id: &str,
codec_version: u32,
checksum_algorithm: u16,
checksum_algorithm_version: u16,
checksum: [u8; 32],
payload: ComponentStatePayload,
limits: StateLimits,
) -> Result<Self, ComponentStateError> {
let schema_id =
StateSchemaId::new(schema_id).map_err(|_| ComponentStateError::Malformed)?;
let schema_version =
StateSchemaVersion::new(schema_version).map_err(|_| ComponentStateError::Malformed)?;
let codec_id = CodecId::new(codec_id)?;
let codec_version = CodecVersion::new(codec_version)?;
let algorithm = ChecksumAlgorithm::resolve(checksum_algorithm, checksum_algorithm_version)
.ok_or(ComponentStateError::ChecksumAlgorithmUnsupported {
algorithm: checksum_algorithm,
version: checksum_algorithm_version,
})?;
let (marker, size_check) = match &payload {
ComponentStatePayload::Inline(bytes) => {
(inline_payload_marker(bytes), Some(bytes.len()))
}
ComponentStatePayload::External(reference) => {
(external_payload_marker(reference), None)
}
};
if let Some(len) = size_check
&& len > limits.maximum_bytes()
{
return Err(ComponentStateError::TooLarge {
max_bytes: limits.maximum_bytes(),
});
}
let expected = algorithm.digest(&canonical_checksum_input(
&namespace,
&schema_id,
schema_version,
&codec_id,
codec_version,
&marker,
));
if expected != checksum {
return Err(ComponentStateError::ChecksumMismatch);
}
let (stored, encoded_len) = match payload {
ComponentStatePayload::Inline(bytes) => {
let value: Value =
serde_json::from_slice(&bytes).map_err(|_| ComponentStateError::Malformed)?;
if !value.is_object() {
return Err(ComponentStateError::PayloadNotObject);
}
if json_depth(&value) > limits.maximum_depth() {
return Err(ComponentStateError::TooDeep {
max_depth: limits.maximum_depth(),
});
}
let encoded_len = bytes.len();
(StoredPayload::Inline(bytes), encoded_len)
}
ComponentStatePayload::External(reference) => (StoredPayload::External(reference), 0),
};
Ok(Self {
namespace,
schema_id,
schema_version,
codec_id,
codec_version,
checksum_algorithm: algorithm,
checksum,
payload: stored,
encoded_len,
})
}
pub fn decode<T>(
&self,
codec: &(impl ComponentStateCodec<T> + ?Sized),
) -> Result<T, ComponentStateError> {
if self.codec_id != *codec.codec_id() {
return Err(ComponentStateError::UnknownCodec);
}
if self.schema_id != *codec.schema_id() {
return Err(ComponentStateError::SchemaMismatch);
}
let current_schema = codec.current_version();
if self.schema_version > current_schema {
return Err(ComponentStateError::UnsupportedSchemaVersion {
found: self.schema_version.get(),
current: current_schema.get(),
});
}
let current_codec = codec.codec_version();
if self.codec_version > current_codec {
return Err(ComponentStateError::UnsupportedCodecVersion {
found: self.codec_version.get(),
current: current_codec.get(),
});
}
let Self {
payload: StoredPayload::Inline(bytes),
..
} = self
else {
return Err(ComponentStateError::ExternalReferenceMissing);
};
let payload_bytes = bytes.clone();
let payload_bytes = upgrade_schema_chain(
DurableStateKind::ComponentState,
self.schema_version,
codec,
payload_bytes,
)
.map_err(|error| map_schema_chain_error(&error))?;
let payload_bytes = upgrade_codec_chain(self.codec_version, codec, payload_bytes)?;
codec
.decode(&payload_bytes)
.map_err(ComponentStateError::Codec)
}
pub fn validated_for_open<T>(
&self,
codec: &(impl ComponentStateCodec<T> + ?Sized),
) -> Result<Self, ComponentStateError> {
if self.codec_id != *codec.codec_id() {
return Err(ComponentStateError::UnknownCodec);
}
if self.schema_id != *codec.schema_id() {
return Err(ComponentStateError::SchemaMismatch);
}
let current_schema = codec.current_version();
if self.schema_version > current_schema {
return Err(ComponentStateError::UnsupportedSchemaVersion {
found: self.schema_version.get(),
current: current_schema.get(),
});
}
let current_codec = codec.codec_version();
if self.codec_version > current_codec {
return Err(ComponentStateError::UnsupportedCodecVersion {
found: self.codec_version.get(),
current: current_codec.get(),
});
}
if matches!(self.payload, StoredPayload::External(_)) {
if self.schema_version < current_schema {
return Err(ComponentStateError::ExternalSchemaMigrationUnsupported {
found: self.schema_version.get(),
current: current_schema.get(),
});
}
if self.codec_version < current_codec {
return Err(ComponentStateError::ExternalCodecMigrationUnsupported {
found: self.codec_version.get(),
current: current_codec.get(),
});
}
return Ok(self.clone());
}
let Self {
payload: StoredPayload::Inline(bytes),
..
} = self
else {
return Ok(self.clone());
};
let migrated = upgrade_schema_chain(
DurableStateKind::ComponentState,
self.schema_version,
codec,
bytes.clone(),
)
.map_err(|error| map_schema_chain_error(&error))?;
let migrated = upgrade_codec_chain(self.codec_version, codec, migrated)?;
if migrated == *bytes {
return Ok(self.clone());
}
let checksum_algorithm = ChecksumAlgorithm::CURRENT;
let checksum = checksum_algorithm.digest(&canonical_checksum_input(
&self.namespace,
&self.schema_id,
current_schema,
&self.codec_id,
current_codec,
&inline_payload_marker(&migrated),
));
let encoded_len = migrated.len();
Ok(Self {
namespace: self.namespace.clone(),
schema_id: self.schema_id.clone(),
schema_version: current_schema,
codec_id: self.codec_id.clone(),
codec_version: current_codec,
checksum_algorithm,
checksum,
payload: StoredPayload::Inline(migrated),
encoded_len,
})
}
#[must_use]
pub const fn namespace(&self) -> &ComponentStreamIdentity {
&self.namespace
}
#[must_use]
pub const fn schema_id(&self) -> &StateSchemaId {
&self.schema_id
}
#[must_use]
pub const fn schema_version(&self) -> StateSchemaVersion {
self.schema_version
}
#[must_use]
pub const fn codec_id(&self) -> &CodecId {
&self.codec_id
}
#[must_use]
pub const fn codec_version(&self) -> CodecVersion {
self.codec_version
}
#[must_use]
pub const fn is_external(&self) -> bool {
matches!(self.payload, StoredPayload::External(_))
}
#[must_use]
pub const fn checksum_algorithm(&self) -> u16 {
self.checksum_algorithm.identifier()
}
#[must_use]
pub const fn checksum_algorithm_version(&self) -> u16 {
self.checksum_algorithm.algorithm_version()
}
#[must_use]
pub const fn checksum(&self) -> [u8; 32] {
self.checksum
}
pub fn payload(&self) -> Result<ComponentStatePayload, ComponentStateError> {
match &self.payload {
StoredPayload::Inline(bytes) => Ok(ComponentStatePayload::Inline(bytes.clone())),
StoredPayload::External(reference) => Ok(ComponentStatePayload::External(*reference)),
}
}
#[must_use]
pub fn encoded_len(&self) -> usize {
match &self.payload {
StoredPayload::External(reference) => {
usize::try_from(reference.encoded_len()).unwrap_or(usize::MAX)
}
StoredPayload::Inline(_) => self.encoded_len,
}
}
}
impl fmt::Debug for ComponentStateEnvelope {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ComponentStateEnvelope")
.field("namespace", &self.namespace)
.field("schema_id", &self.schema_id)
.field("schema_version", &self.schema_version)
.field("codec_id", &self.codec_id)
.field("codec_version", &self.codec_version)
.field("checksum_algorithm", &self.checksum_algorithm)
.field("checksum", &"<redacted>")
.field("is_external", &self.is_external())
.field("encoded_len", &self.encoded_len())
.field("payload", &"<redacted>")
.finish()
}
}
pub trait ComponentStateCodec<T>: VersionedStateCodec<T> {
fn codec_id(&self) -> &CodecId;
fn codec_version(&self) -> CodecVersion;
fn codec_upgrades(&self) -> &[CodecVersionUpgrade] {
&[]
}
fn sensitivity(&self) -> StateSensitivity {
StateSensitivity::Sensitive
}
fn restartability(&self) -> RestartabilityDeclaration;
}
pub struct DefaultComponentCodec<C> {
schema: C,
codec_id: CodecId,
codec_version: CodecVersion,
codec_upgrades: Vec<CodecVersionUpgrade>,
sensitivity: StateSensitivity,
restartability: RestartabilityDeclaration,
}
impl<C> DefaultComponentCodec<C> {
#[must_use]
pub const fn new(
schema: C,
codec_id: CodecId,
codec_version: CodecVersion,
restartability: RestartabilityDeclaration,
) -> Self {
Self {
schema,
codec_id,
codec_version,
codec_upgrades: Vec::new(),
sensitivity: StateSensitivity::Sensitive,
restartability,
}
}
#[must_use]
pub fn with_codec_upgrades(mut self, upgrades: Vec<CodecVersionUpgrade>) -> Self {
self.codec_upgrades = upgrades;
self
}
#[must_use]
pub const fn with_sensitivity(mut self, sensitivity: StateSensitivity) -> Self {
self.sensitivity = sensitivity;
self
}
}
impl<T, C: VersionedStateCodec<T>> VersionedStateCodec<T> for DefaultComponentCodec<C> {
fn schema_id(&self) -> &StateSchemaId {
self.schema.schema_id()
}
fn current_version(&self) -> StateSchemaVersion {
self.schema.current_version()
}
fn upgrades(&self) -> &[crate::StateSchemaUpgrade] {
self.schema.upgrades()
}
fn encode(&self, value: &T) -> Result<Vec<u8>, StateCodecError> {
self.schema.encode(value)
}
fn decode(&self, payload: &[u8]) -> Result<T, StateCodecError> {
self.schema.decode(payload)
}
}
impl<T, C: VersionedStateCodec<T>> ComponentStateCodec<T> for DefaultComponentCodec<C> {
fn codec_id(&self) -> &CodecId {
&self.codec_id
}
fn codec_version(&self) -> CodecVersion {
self.codec_version
}
fn codec_upgrades(&self) -> &[CodecVersionUpgrade] {
&self.codec_upgrades
}
fn sensitivity(&self) -> StateSensitivity {
self.sensitivity
}
fn restartability(&self) -> RestartabilityDeclaration {
self.restartability
}
}
mod stream_contract_sealed {
use super::{
CodecId, ComponentStateEnvelope, ComponentStateError, RestartabilityDeclaration,
StateSchemaId,
};
pub trait StreamStateContractObject: Send + Sync {
fn schema_id(&self) -> &StateSchemaId;
fn codec_id(&self) -> &CodecId;
fn restartability(&self) -> RestartabilityDeclaration;
fn validate_for_open(
&self,
envelope: &ComponentStateEnvelope,
) -> Result<ComponentStateEnvelope, ComponentStateError>;
}
}
struct CodecContract<T, C> {
codec: C,
marker: PhantomData<fn() -> T>,
}
impl<T, C: ComponentStateCodec<T> + Send + Sync> stream_contract_sealed::StreamStateContractObject
for CodecContract<T, C>
{
fn schema_id(&self) -> &StateSchemaId {
self.codec.schema_id()
}
fn codec_id(&self) -> &CodecId {
self.codec.codec_id()
}
fn restartability(&self) -> RestartabilityDeclaration {
self.codec.restartability()
}
fn validate_for_open(
&self,
envelope: &ComponentStateEnvelope,
) -> Result<ComponentStateEnvelope, ComponentStateError> {
envelope.validated_for_open(&self.codec)
}
}
pub struct StreamStateContract(Arc<dyn stream_contract_sealed::StreamStateContractObject>);
impl StreamStateContract {
pub fn new<T, C>(codec: C) -> Self
where
C: ComponentStateCodec<T> + Send + Sync + 'static,
T: 'static,
{
Self(Arc::new(CodecContract {
codec,
marker: PhantomData,
}))
}
#[must_use]
pub fn schema_id(&self) -> &StateSchemaId {
self.0.schema_id()
}
#[must_use]
pub fn codec_id(&self) -> &CodecId {
self.0.codec_id()
}
#[must_use]
pub fn restartability(&self) -> RestartabilityDeclaration {
self.0.restartability()
}
pub fn validate_for_open(
&self,
envelope: &ComponentStateEnvelope,
) -> Result<ComponentStateEnvelope, ComponentStateError> {
self.0.validate_for_open(envelope)
}
}
impl fmt::Debug for StreamStateContract {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("StreamStateContract")
.field("schema_id", &self.schema_id())
.field("codec_id", &self.codec_id())
.field("restartability", &self.restartability())
.finish()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ComponentStateError {
EmptyCodecId,
CodecIdTooLong {
max_bytes: usize,
},
ZeroCodecVersion,
NonIncreasingCodecUpgrade,
TooLarge {
max_bytes: usize,
},
TooDeep {
max_depth: usize,
},
Malformed,
SchemaMismatch,
UnsupportedSchemaVersion {
found: u32,
current: u32,
},
NoSchemaUpgradePath {
found: u32,
current: u32,
},
AmbiguousSchemaUpgrade {
from: u32,
},
SchemaUpgradeOvershootsCurrent {
to: u32,
current: u32,
},
SchemaUpgradeChainTooLong {
max_upgrades: usize,
},
UnknownCodec,
UnsupportedCodecVersion {
found: u32,
current: u32,
},
ExternalSchemaMigrationUnsupported {
found: u32,
current: u32,
},
ExternalCodecMigrationUnsupported {
found: u32,
current: u32,
},
NoCodecUpgradePath {
found: u32,
current: u32,
},
AmbiguousCodecUpgrade {
from: u32,
},
CodecUpgradeOvershootsCurrent {
to: u32,
current: u32,
},
CodecUpgradeChainTooLong {
max_upgrades: usize,
},
ChecksumAlgorithmUnsupported {
algorithm: u16,
version: u16,
},
ChecksumMismatch,
ExternalReferenceMissing,
ExternalReferenceContentMismatch,
InvalidPayload,
PayloadNotObject,
Codec(StateCodecError),
}
impl fmt::Display for ComponentStateError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyCodecId => {
formatter.write_str("component state codec identifier must not be empty")
}
Self::CodecIdTooLong { max_bytes } => {
write!(
formatter,
"component state codec identifier exceeds {max_bytes} UTF-8 bytes"
)
}
Self::ZeroCodecVersion => {
formatter.write_str("component state codec version must be nonzero")
}
Self::NonIncreasingCodecUpgrade => {
formatter.write_str("component state codec upgrade must increase the version")
}
Self::TooLarge { max_bytes } => {
write!(formatter, "component state exceeds {max_bytes} bytes")
}
Self::TooDeep { max_depth } => {
write!(formatter, "component state exceeds JSON depth {max_depth}")
}
Self::Malformed => formatter.write_str("component state is malformed"),
Self::SchemaMismatch => {
formatter.write_str("component state schema does not match the component")
}
Self::UnsupportedSchemaVersion { .. } => {
formatter.write_str("component state schema version is unsupported")
}
Self::NoSchemaUpgradePath { .. } => {
formatter.write_str("component state schema version has no upgrade path")
}
Self::AmbiguousSchemaUpgrade { .. } => {
formatter.write_str("component state schema upgrade is ambiguous")
}
Self::SchemaUpgradeOvershootsCurrent { .. } => {
formatter.write_str("component state schema upgrade passes the current version")
}
Self::SchemaUpgradeChainTooLong { max_upgrades } => {
write!(
formatter,
"component state schema upgrade chain exceeds {max_upgrades} upgrades"
)
}
Self::UnknownCodec => formatter.write_str("component state codec is unknown"),
Self::UnsupportedCodecVersion { .. } => {
formatter.write_str("component state codec version is unsupported")
}
Self::ExternalSchemaMigrationUnsupported { .. } => {
formatter.write_str("external component state schema migration is unsupported")
}
Self::ExternalCodecMigrationUnsupported { .. } => {
formatter.write_str("external component state codec migration is unsupported")
}
Self::NoCodecUpgradePath { .. } => {
formatter.write_str("component state codec version has no upgrade path")
}
Self::AmbiguousCodecUpgrade { .. } => {
formatter.write_str("component state codec upgrade is ambiguous")
}
Self::CodecUpgradeOvershootsCurrent { .. } => {
formatter.write_str("component state codec upgrade passes the current version")
}
Self::CodecUpgradeChainTooLong { max_upgrades } => {
write!(
formatter,
"component state codec upgrade chain exceeds {max_upgrades} upgrades"
)
}
Self::ChecksumAlgorithmUnsupported { .. } => {
formatter.write_str("component state checksum algorithm is unsupported")
}
Self::ChecksumMismatch => {
formatter.write_str("component state checksum does not match")
}
Self::ExternalReferenceMissing => {
formatter.write_str("component state external reference is missing")
}
Self::ExternalReferenceContentMismatch => {
formatter.write_str("component state external content identity mismatch")
}
Self::InvalidPayload => {
formatter.write_str("component state payload is not valid JSON")
}
Self::PayloadNotObject => {
formatter.write_str("component state payload must be a JSON object")
}
Self::Codec(error) => error.fmt(formatter),
}
}
}
impl Error for ComponentStateError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Codec(error) => Some(error),
_ => None,
}
}
}