Add Rust domain kernel and wire contracts

This commit is contained in:
ms2k 2026-07-15 21:00:33 +09:00
commit 4da5a980ed
20 changed files with 3757 additions and 1 deletions

View file

@ -0,0 +1,16 @@
[package]
name = "remora-contracts"
edition.workspace = true
publish = false
rust-version.workspace = true
version.workspace = true
[dependencies]
remora-domain.workspace = true
serde.workspace = true
time.workspace = true
[dev-dependencies]
jsonschema = { version = "0.47.0", default-features = false }
serde_json.workspace = true
serde_yaml_ng = "0.10.0"

View file

@ -0,0 +1,149 @@
use std::collections::BTreeMap;
use remora_domain::OpaqueId;
use serde::{Deserialize, Serialize};
use crate::{DateTimeDto, NullableDto};
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SourceCredentialKindDto {
BasicAuth,
BearerToken,
HttpHeaders,
ObjectStore,
HuggingFace,
}
#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct BasicAuthMaterialDto {
pub username: String,
pub password: String,
}
#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TokenMaterialDto {
pub token: String,
}
#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct HttpHeadersMaterialDto {
pub headers: BTreeMap<String, String>,
}
#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ObjectStoreCredentialMaterialDto {
pub access_key_id: String,
pub secret_access_key: String,
#[serde(
default,
deserialize_with = "crate::dto::optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub session_token: Option<String>,
}
#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
#[serde(untagged)]
pub enum CredentialMaterialDto {
BasicAuth(BasicAuthMaterialDto),
Token(TokenMaterialDto),
HttpHeaders(HttpHeadersMaterialDto),
ObjectStore(ObjectStoreCredentialMaterialDto),
}
#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum SourceCredentialCreateDto {
BasicAuth(BasicAuthCredentialCreateDto),
BearerToken(BearerTokenCredentialCreateDto),
HttpHeaders(HttpHeadersCredentialCreateDto),
ObjectStore(ObjectStoreCredentialCreateDto),
HuggingFace(HuggingFaceCredentialCreateDto),
}
#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct BasicAuthCredentialCreateDto {
pub label: String,
pub material: BasicAuthMaterialDto,
}
#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct BearerTokenCredentialCreateDto {
pub label: String,
pub material: TokenMaterialDto,
}
#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct HttpHeadersCredentialCreateDto {
pub label: String,
pub material: HttpHeadersMaterialDto,
}
#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ObjectStoreCredentialCreateDto {
pub label: String,
pub material: ObjectStoreCredentialMaterialDto,
}
#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct HuggingFaceCredentialCreateDto {
pub label: String,
pub material: TokenMaterialDto,
}
#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SourceCredentialRotateDto {
pub material: CredentialMaterialDto,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SourceCredentialVersionDto {
pub id: OpaqueId,
pub created_at: DateTimeDto,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CredentialGrantVersionDto {
pub credential_id: OpaqueId,
pub version_id: OpaqueId,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SourceCredentialStatusDto {
Active,
Revoked,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SourceCredentialDto {
pub id: OpaqueId,
pub owner: OpaqueId,
pub label: String,
pub kind: SourceCredentialKindDto,
pub status: SourceCredentialStatusDto,
pub active_version: NullableDto<SourceCredentialVersionDto>,
pub created_at: DateTimeDto,
pub updated_at: DateTimeDto,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SourceCredentialPageDto {
pub items: Vec<SourceCredentialDto>,
pub next_cursor: NullableDto<String>,
}

View file

@ -0,0 +1,356 @@
use remora_domain::{JobPhase, OpaqueId, RuntimeFailureClass};
use serde::{Deserialize, Serialize};
use crate::{DateTimeDto, PositiveIntegerDto, SafeIntegerDto, SignedSafeIntegerDto};
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TargetDto {
pub store: String,
pub bucket: String,
pub prefix: String,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TargetOverrideDto {
#[serde(
default,
deserialize_with = "optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub bucket: Option<String>,
#[serde(
default,
deserialize_with = "optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub prefix: Option<String>,
}
#[derive(Clone, Deserialize, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum SourceSubmissionDto {
Http(HttpSourceSubmissionDto),
ObjectStore(ObjectStoreSourceSubmissionDto),
HuggingFace(HuggingFaceSourceSubmissionDto),
}
#[derive(Clone, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct HttpSourceSubmissionDto {
pub url: String,
pub result_path: String,
#[serde(
default,
deserialize_with = "optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub credential_id: Option<OpaqueId>,
}
#[derive(Clone, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ObjectStoreSourceSubmissionDto {
pub store: String,
pub bucket: String,
pub key: String,
#[serde(
default,
deserialize_with = "optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub version_id: Option<String>,
pub result_path: String,
#[serde(
default,
deserialize_with = "optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub credential_id: Option<OpaqueId>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct HuggingFaceSelectionDto {
#[serde(
default,
deserialize_with = "optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub paths: Option<Vec<String>>,
#[serde(
default,
deserialize_with = "optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub prefixes: Option<Vec<String>>,
}
#[derive(Clone, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct HuggingFaceSourceSubmissionDto {
pub repository_type: HuggingFaceRepositoryTypeDto,
pub repository_id: String,
pub revision: String,
#[serde(
default,
deserialize_with = "optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub selection: Option<HuggingFaceSelectionDto>,
#[serde(
default,
deserialize_with = "optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub credential_id: Option<OpaqueId>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum HuggingFaceRepositoryTypeDto {
Model,
Dataset,
Space,
}
#[derive(Clone, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct JobSubmissionDto {
pub source: SourceSubmissionDto,
#[serde(
default,
deserialize_with = "optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub target: Option<TargetOverrideDto>,
#[serde(
default,
deserialize_with = "optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub label: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AttemptAuthorityDto {
pub attempt_id: OpaqueId,
pub fence: PositiveIntegerDto,
pub worker_incarnation: OpaqueId,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ProgressDto {
pub attempt_id: OpaqueId,
pub fence: PositiveIntegerDto,
pub completed_bytes: SafeIntegerDto,
pub total_bytes: Option<SafeIntegerDto>,
pub completed_members: SafeIntegerDto,
pub total_members: Option<SafeIntegerDto>,
pub observed_at: DateTimeDto,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(untagged)]
pub enum JobFailureDto {
Permanent(PermanentJobFailureDto),
Retryable(RetryableJobFailureDto),
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PermanentJobFailureDto {
pub class: PermanentFailureClassDto,
pub message: String,
pub retryable: PermanentDispositionDto,
#[serde(
default,
deserialize_with = "optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub safe_details: Option<std::collections::BTreeMap<String, SafeDetailValueDto>>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RetryableJobFailureDto {
pub class: RetryableFailureClassDto,
pub message: String,
pub retryable: RetryableDispositionDto,
#[serde(
default,
deserialize_with = "optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub safe_details: Option<std::collections::BTreeMap<String, SafeDetailValueDto>>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PermanentFailureClassDto {
Validation,
Authorization,
SourceNotFound,
SourceChanged,
Cancelled,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum RetryableFailureClassDto {
TransientSource,
TransientTarget,
Integrity,
Capacity,
Internal,
LostAttempt,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PermanentDispositionDto;
impl Serialize for PermanentDispositionDto {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_bool(false)
}
}
impl<'de> Deserialize<'de> for PermanentDispositionDto {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
if bool::deserialize(deserializer)? {
return Err(serde::de::Error::custom(
"permanent failure must not be retryable",
));
}
Ok(Self)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RetryableDispositionDto;
impl Serialize for RetryableDispositionDto {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_bool(true)
}
}
impl<'de> Deserialize<'de> for RetryableDispositionDto {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
if !bool::deserialize(deserializer)? {
return Err(serde::de::Error::custom(
"retryable failure must be retryable",
));
}
Ok(Self)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AttemptOutcomeRequestDto {
pub operation_id: OpaqueId,
pub fence: PositiveIntegerDto,
pub worker_incarnation: OpaqueId,
pub outcome: AttemptOutcomeDto,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum AttemptOutcomeDto {
Success(AttemptSuccessOutcomeDto),
Failure(AttemptFailureOutcomeDto),
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AttemptSuccessOutcomeDto {
pub result: ResultDescriptorDto,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AttemptFailureOutcomeDto {
pub failure: AttemptRuntimeFailureDto,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AttemptRuntimeFailureDto {
pub class: RuntimeFailureClass,
pub message: String,
#[serde(
default,
deserialize_with = "optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub safe_details: Option<std::collections::BTreeMap<String, SafeDetailValueDto>>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(untagged)]
pub enum SafeDetailValueDto {
String(String),
Integer(SignedSafeIntegerDto),
Boolean(bool),
Null,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ResultDescriptorDto {
pub kind: ResultKindDto,
pub manifest: ObjectIdentityDto,
pub member_count: PositiveIntegerDto,
pub total_bytes: SafeIntegerDto,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ResultKindDto {
Manifest,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ObjectIdentityDto {
pub store: String,
pub bucket: String,
pub key: String,
#[serde(
default,
deserialize_with = "optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub version_id: Option<String>,
pub checksum: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct JobPhaseProjectionDto {
pub phase: JobPhase,
}
pub(crate) fn optional_non_null<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: Deserialize<'de>,
{
T::deserialize(deserializer).map(Some)
}

View file

@ -0,0 +1,114 @@
use remora_domain::{JobLifecycle, JobPhase, OpaqueId};
use serde::{Deserialize, Serialize};
use crate::{
CursorDto, DateTimeDto, NullableDto, PositiveIntegerDto, SafeIntegerDto, SchemaVersionDto,
wire::optional_nullable,
};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum EventFrameDto {
JobChanged(JobChangedDto),
DeletionChanged(DeletionChangedDto),
ResyncRequired(ResyncRequiredDto),
Heartbeat(HeartbeatDto),
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct JobChangedDto {
pub schema_version: SchemaVersionDto,
pub event_id: OpaqueId,
pub sequence: PositiveIntegerDto,
pub cursor: CursorDto,
pub occurred_at: DateTimeDto,
pub job_id: OpaqueId,
pub job_revision: PositiveIntegerDto,
pub phase: JobPhase,
pub lifecycle: JobLifecycle,
pub authoritative_attempt: NullableDto<EventAttemptDto>,
pub progress: NullableDto<EventProgressDto>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct EventAttemptDto {
pub attempt_id: OpaqueId,
pub fence: PositiveIntegerDto,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct EventProgressDto {
pub attempt_id: OpaqueId,
pub fence: PositiveIntegerDto,
pub completed_bytes: SafeIntegerDto,
#[serde(
default,
deserialize_with = "optional_nullable",
skip_serializing_if = "Option::is_none"
)]
pub total_bytes: Option<NullableDto<SafeIntegerDto>>,
pub completed_members: SafeIntegerDto,
#[serde(
default,
deserialize_with = "optional_nullable",
skip_serializing_if = "Option::is_none"
)]
pub total_members: Option<NullableDto<SafeIntegerDto>>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct DeletionChangedDto {
pub schema_version: SchemaVersionDto,
pub event_id: OpaqueId,
pub sequence: PositiveIntegerDto,
pub cursor: CursorDto,
pub occurred_at: DateTimeDto,
pub job_id: OpaqueId,
pub operation_id: OpaqueId,
pub state: DeletionStateDto,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DeletionStateDto {
Queued,
Running,
RetryWait,
Blocked,
Completed,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ResyncRequiredDto {
pub schema_version: SchemaVersionDto,
pub event_id: OpaqueId,
pub sequence: PositiveIntegerDto,
pub cursor: CursorDto,
pub occurred_at: DateTimeDto,
pub reason: ResyncReasonDto,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ResyncReasonDto {
CursorAbsent,
CursorInvalid,
CursorExpired,
AuthorizationChanged,
HistoryUnavailable,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct HeartbeatDto {
pub schema_version: SchemaVersionDto,
pub event_id: OpaqueId,
pub sequence: PositiveIntegerDto,
pub cursor: CursorDto,
pub occurred_at: DateTimeDto,
}

View file

@ -0,0 +1,169 @@
use remora_domain::OpaqueId;
use serde::{Deserialize, Serialize};
use crate::{
AttemptAuthorityDto, CredentialGrantVersionDto, DurableDescriptorKindDto,
DurableDescriptorRefDto, LeaseProjectionDto, NullableDto, PositiveIntegerDto, ProgressValueDto,
SafeIntegerDto, SourceProjectionDto, StagingTargetDto, TargetDto,
};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ClaimRequestDto {
pub operation_id: OpaqueId,
pub worker_incarnation: OpaqueId,
}
#[derive(Clone, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ExecutionEnvelopeDto {
pub id: OpaqueId,
pub job_id: OpaqueId,
pub attempt_id: OpaqueId,
pub fence: PositiveIntegerDto,
pub worker_incarnation: OpaqueId,
pub source: SourceProjectionDto,
pub sensitive_source_ref: OpaqueId,
pub credential_version: NullableDto<CredentialGrantVersionDto>,
pub resolved_source_ref: NullableDto<DurableDescriptorRefDto>,
pub target: TargetDto,
pub staging_target: StagingTargetDto,
pub execution_grant_ref: OpaqueId,
pub checkpoint_ref: NullableDto<DurableDescriptorRefDto>,
}
#[derive(Clone, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ClaimResponseDto {
pub authority: AttemptAuthorityDto,
pub execution: ExecutionEnvelopeDto,
}
#[derive(Clone, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ActivationResponseDto {
pub authority: AttemptAuthorityDto,
pub execution: ExecutionEnvelopeDto,
pub lease: LeaseProjectionDto,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AttemptMutationDto {
pub operation_id: OpaqueId,
pub fence: PositiveIntegerDto,
pub worker_incarnation: OpaqueId,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AttemptRenewRequestDto {
pub operation_id: OpaqueId,
pub fence: PositiveIntegerDto,
pub worker_incarnation: OpaqueId,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AttemptActivateRequestDto {
pub operation_id: OpaqueId,
pub fence: PositiveIntegerDto,
pub worker_incarnation: OpaqueId,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ResolvedSourceDescriptorRefDto {
pub id: OpaqueId,
pub kind: ResolvedSourceDescriptorKindDto,
pub schema_version: PositiveIntegerDto,
pub byte_length: SafeIntegerDto,
#[serde(
default,
deserialize_with = "crate::dto::optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub item_count: Option<SafeIntegerDto>,
pub checksum: String,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ResolvedSourceDescriptorKindDto {
ResolvedSource,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CheckpointDescriptorRefDto {
pub id: OpaqueId,
pub kind: CheckpointDescriptorKindDto,
pub schema_version: PositiveIntegerDto,
pub byte_length: SafeIntegerDto,
#[serde(
default,
deserialize_with = "crate::dto::optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub item_count: Option<SafeIntegerDto>,
pub checksum: String,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CheckpointDescriptorKindDto {
Checkpoint,
}
impl From<ResolvedSourceDescriptorRefDto> for DurableDescriptorRefDto {
fn from(value: ResolvedSourceDescriptorRefDto) -> Self {
Self {
id: value.id,
kind: DurableDescriptorKindDto::ResolvedSource,
schema_version: value.schema_version,
byte_length: value.byte_length,
item_count: value.item_count,
checksum: value.checksum,
}
}
}
impl From<CheckpointDescriptorRefDto> for DurableDescriptorRefDto {
fn from(value: CheckpointDescriptorRefDto) -> Self {
Self {
id: value.id,
kind: DurableDescriptorKindDto::Checkpoint,
schema_version: value.schema_version,
byte_length: value.byte_length,
item_count: value.item_count,
checksum: value.checksum,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AttemptResolvedSourceRequestDto {
pub operation_id: OpaqueId,
pub fence: PositiveIntegerDto,
pub worker_incarnation: OpaqueId,
pub resolved_source_ref: ResolvedSourceDescriptorRefDto,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AttemptCheckpointRequestDto {
pub operation_id: OpaqueId,
pub fence: PositiveIntegerDto,
pub worker_incarnation: OpaqueId,
pub checkpoint_ref: CheckpointDescriptorRefDto,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AttemptProgressRequestDto {
pub operation_id: OpaqueId,
pub fence: PositiveIntegerDto,
pub worker_incarnation: OpaqueId,
pub progress: ProgressValueDto,
}

View file

@ -0,0 +1,49 @@
mod credentials;
mod dto;
mod events;
mod internal;
mod public;
mod wire;
pub use credentials::{
BasicAuthCredentialCreateDto, BasicAuthMaterialDto, BearerTokenCredentialCreateDto,
CredentialGrantVersionDto, CredentialMaterialDto, HttpHeadersCredentialCreateDto,
HttpHeadersMaterialDto, HuggingFaceCredentialCreateDto, ObjectStoreCredentialCreateDto,
ObjectStoreCredentialMaterialDto, SourceCredentialCreateDto, SourceCredentialDto,
SourceCredentialKindDto, SourceCredentialPageDto, SourceCredentialRotateDto,
SourceCredentialStatusDto, SourceCredentialVersionDto, TokenMaterialDto,
};
pub use dto::{
AttemptAuthorityDto, AttemptFailureOutcomeDto, AttemptOutcomeDto, AttemptOutcomeRequestDto,
AttemptRuntimeFailureDto, AttemptSuccessOutcomeDto, HttpSourceSubmissionDto,
HuggingFaceRepositoryTypeDto, HuggingFaceSelectionDto, HuggingFaceSourceSubmissionDto,
JobFailureDto, JobPhaseProjectionDto, JobSubmissionDto, ObjectIdentityDto,
ObjectStoreSourceSubmissionDto, PermanentDispositionDto, PermanentFailureClassDto,
PermanentJobFailureDto, ProgressDto, ResultDescriptorDto, ResultKindDto,
RetryableDispositionDto, RetryableFailureClassDto, RetryableJobFailureDto, SafeDetailValueDto,
SourceSubmissionDto, TargetDto, TargetOverrideDto,
};
pub use events::{
DeletionChangedDto, DeletionStateDto, EventAttemptDto, EventFrameDto, EventProgressDto,
HeartbeatDto, JobChangedDto, ResyncReasonDto, ResyncRequiredDto,
};
pub use internal::{
ActivationResponseDto, AttemptActivateRequestDto, AttemptCheckpointRequestDto,
AttemptMutationDto, AttemptProgressRequestDto, AttemptRenewRequestDto,
AttemptResolvedSourceRequestDto, CheckpointDescriptorKindDto, CheckpointDescriptorRefDto,
ClaimRequestDto, ClaimResponseDto, ExecutionEnvelopeDto, ResolvedSourceDescriptorKindDto,
ResolvedSourceDescriptorRefDto,
};
pub use public::{
ApiErrorCodeDto, ApiErrorDto, ApiErrorFieldDto, AttemptPageDto, AttemptRecordDto,
AttemptRecordStateDto, CheckpointProjectionDto, DeletionOperationDto, DeletionProblemClassDto,
DeletionProblemDto, DeletionStateDto as HttpDeletionStateDto, DurableDescriptorKindDto,
DurableDescriptorRefDto, HttpSourceProjectionDto, HuggingFaceSourceProjectionDto, JobDto,
JobKindDto, JobPageDto, JobResourceDto, JobTombstoneDto, LeaseProjectionDto,
ObjectStoreSourceProjectionDto, ProgressValueDto, ResultMemberDto, ResultMemberPageDto,
SourceProjectionDto, StagingTargetDto, TerminalJobPhaseDto, TombstoneKindDto,
};
pub use wire::{
CursorDto, DateTimeDto, NullableDto, PositiveIntegerDto, SafeIntegerDto, SchemaVersionDto,
SignedSafeIntegerDto,
};

View file

@ -0,0 +1,326 @@
use remora_domain::{AttemptState, JobLifecycle, JobPhase, OpaqueId};
use serde::{Deserialize, Serialize};
use crate::{
AttemptAuthorityDto, CursorDto, DateTimeDto, HuggingFaceRepositoryTypeDto,
HuggingFaceSelectionDto, JobFailureDto, NullableDto, ObjectIdentityDto, PositiveIntegerDto,
ProgressDto, ResultDescriptorDto, SafeIntegerDto, TargetDto,
};
#[derive(Clone, Deserialize, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum SourceProjectionDto {
Http(HttpSourceProjectionDto),
ObjectStore(ObjectStoreSourceProjectionDto),
HuggingFace(HuggingFaceSourceProjectionDto),
}
#[derive(Clone, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct HttpSourceProjectionDto {
pub display_url: String,
pub result_path: String,
#[serde(
default,
deserialize_with = "crate::dto::optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub credential_id: Option<OpaqueId>,
}
#[derive(Clone, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ObjectStoreSourceProjectionDto {
pub store: String,
pub bucket: String,
pub key: String,
#[serde(
default,
deserialize_with = "crate::dto::optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub version_id: Option<String>,
pub result_path: String,
#[serde(
default,
deserialize_with = "crate::dto::optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub credential_id: Option<OpaqueId>,
}
#[derive(Clone, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct HuggingFaceSourceProjectionDto {
pub repository_type: HuggingFaceRepositoryTypeDto,
pub repository_id: String,
pub revision: String,
#[serde(
default,
deserialize_with = "crate::dto::optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub selection: Option<HuggingFaceSelectionDto>,
#[serde(
default,
deserialize_with = "crate::dto::optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub credential_id: Option<OpaqueId>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct LeaseProjectionDto {
pub expires_at: DateTimeDto,
pub renew_after: DateTimeDto,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ProgressValueDto {
pub completed_bytes: SafeIntegerDto,
pub total_bytes: Option<SafeIntegerDto>,
pub completed_members: SafeIntegerDto,
pub total_members: Option<SafeIntegerDto>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CheckpointProjectionDto {
pub reusable: bool,
pub validated_bytes: SafeIntegerDto,
pub validated_members: SafeIntegerDto,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DurableDescriptorKindDto {
ResolvedSource,
Checkpoint,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct DurableDescriptorRefDto {
pub id: OpaqueId,
pub kind: DurableDescriptorKindDto,
pub schema_version: PositiveIntegerDto,
pub byte_length: SafeIntegerDto,
#[serde(
default,
deserialize_with = "crate::dto::optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub item_count: Option<SafeIntegerDto>,
pub checksum: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ResultMemberDto {
pub id: OpaqueId,
pub path: String,
pub size: SafeIntegerDto,
pub object: ObjectIdentityDto,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ResultMemberPageDto {
pub items: Vec<ResultMemberDto>,
pub next_cursor: NullableDto<String>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum JobKindDto {
Job,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TombstoneKindDto {
Tombstone,
}
#[derive(Clone, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct JobDto {
pub resource_kind: JobKindDto,
pub id: OpaqueId,
pub owner: OpaqueId,
pub label: NullableDto<String>,
pub revision: PositiveIntegerDto,
pub phase: JobPhase,
pub lifecycle: JobLifecycle,
pub source: SourceProjectionDto,
pub target: TargetDto,
pub authoritative_attempt: NullableDto<AttemptAuthorityDto>,
pub progress: NullableDto<ProgressDto>,
pub checkpoint: NullableDto<CheckpointProjectionDto>,
pub result: NullableDto<ResultDescriptorDto>,
pub failure: NullableDto<JobFailureDto>,
pub created_at: DateTimeDto,
pub updated_at: DateTimeDto,
pub event_cursor: CursorDto,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum TerminalJobPhaseDto {
Succeeded,
Failed,
Cancelled,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct JobTombstoneDto {
pub resource_kind: TombstoneKindDto,
pub id: OpaqueId,
pub owner: OpaqueId,
pub target: TargetDto,
pub terminal_phase: TerminalJobPhaseDto,
pub deletion_operation_id: OpaqueId,
pub created_at: DateTimeDto,
pub hard_delete_started_at: DateTimeDto,
pub updated_at: DateTimeDto,
pub event_cursor: CursorDto,
}
#[derive(Clone, Deserialize, PartialEq, Serialize)]
#[serde(untagged)]
pub enum JobResourceDto {
Job(Box<JobDto>),
Tombstone(Box<JobTombstoneDto>),
}
#[derive(Clone, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct JobPageDto {
pub items: Vec<JobResourceDto>,
pub next_cursor: NullableDto<String>,
pub event_cursor: CursorDto,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum AttemptRecordStateDto {
AttemptAuthorized,
AttemptExecuting,
AttemptStopped,
AttemptCommitted,
}
impl From<AttemptRecordStateDto> for AttemptState {
fn from(value: AttemptRecordStateDto) -> Self {
match value {
AttemptRecordStateDto::AttemptAuthorized => Self::AttemptAuthorized,
AttemptRecordStateDto::AttemptExecuting => Self::AttemptExecuting,
AttemptRecordStateDto::AttemptStopped => Self::AttemptStopped,
AttemptRecordStateDto::AttemptCommitted => Self::AttemptCommitted,
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AttemptRecordDto {
pub id: OpaqueId,
pub fence: PositiveIntegerDto,
pub worker_incarnation: OpaqueId,
pub state: AttemptRecordStateDto,
pub progress: NullableDto<ProgressDto>,
pub result: NullableDto<ResultDescriptorDto>,
pub failure: NullableDto<JobFailureDto>,
pub created_at: DateTimeDto,
pub updated_at: DateTimeDto,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AttemptPageDto {
pub items: Vec<AttemptRecordDto>,
pub next_cursor: NullableDto<String>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DeletionProblemClassDto {
Authorization,
TargetUnavailable,
Configuration,
Internal,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct DeletionProblemDto {
pub class: DeletionProblemClassDto,
pub message: String,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DeletionStateDto {
Queued,
Running,
RetryWait,
Blocked,
Completed,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct DeletionOperationDto {
pub id: OpaqueId,
pub job_id: OpaqueId,
pub state: DeletionStateDto,
pub problem: NullableDto<DeletionProblemDto>,
pub created_at: DateTimeDto,
pub updated_at: DateTimeDto,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ApiErrorCodeDto {
InvalidRequest,
Unauthenticated,
Forbidden,
NotFound,
IdempotencyConflict,
PreconditionFailed,
CursorInvalid,
CursorExpired,
RateLimited,
Unavailable,
Internal,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ApiErrorFieldDto {
pub pointer: String,
pub code: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ApiErrorDto {
pub code: ApiErrorCodeDto,
pub message: String,
pub request_id: OpaqueId,
#[serde(
default,
deserialize_with = "crate::dto::optional_non_null",
skip_serializing_if = "Option::is_none"
)]
pub fields: Option<Vec<ApiErrorFieldDto>>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct StagingTargetDto {
pub store: String,
pub bucket: String,
pub prefix: String,
}

View file

@ -0,0 +1,242 @@
use std::fmt;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
pub const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991;
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct NullableDto<T>(pub Option<T>);
pub(crate) fn optional_nullable<'de, D, T>(
deserializer: D,
) -> Result<Option<NullableDto<T>>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
{
NullableDto::deserialize(deserializer).map(Some)
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct SafeIntegerDto(u64);
impl SafeIntegerDto {
pub fn new(value: u64) -> Result<Self, WireValueError> {
if value > MAX_SAFE_INTEGER {
return Err(WireValueError::SafeInteger);
}
Ok(Self(value))
}
#[must_use]
pub const fn get(self) -> u64 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct SignedSafeIntegerDto(i64);
impl SignedSafeIntegerDto {
pub fn new(value: i64) -> Result<Self, WireValueError> {
if value.unsigned_abs() > MAX_SAFE_INTEGER {
return Err(WireValueError::SafeInteger);
}
Ok(Self(value))
}
#[must_use]
pub const fn get(self) -> i64 {
self.0
}
}
impl Serialize for SignedSafeIntegerDto {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_i64(self.0)
}
}
impl<'de> Deserialize<'de> for SignedSafeIntegerDto {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Self::new(i64::deserialize(deserializer)?).map_err(D::Error::custom)
}
}
impl Serialize for SafeIntegerDto {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u64(self.0)
}
}
impl<'de> Deserialize<'de> for SafeIntegerDto {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Self::new(u64::deserialize(deserializer)?).map_err(D::Error::custom)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PositiveIntegerDto(SafeIntegerDto);
impl PositiveIntegerDto {
pub fn new(value: u64) -> Result<Self, WireValueError> {
let value = SafeIntegerDto::new(value)?;
if value.get() == 0 {
return Err(WireValueError::PositiveInteger);
}
Ok(Self(value))
}
#[must_use]
pub const fn get(self) -> u64 {
self.0.get()
}
}
impl Serialize for PositiveIntegerDto {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u64(self.get())
}
}
impl<'de> Deserialize<'de> for PositiveIntegerDto {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Self::new(u64::deserialize(deserializer)?).map_err(D::Error::custom)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub struct SchemaVersionDto;
impl Serialize for SchemaVersionDto {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u8(1)
}
}
impl<'de> Deserialize<'de> for SchemaVersionDto {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
if u8::deserialize(deserializer)? != 1 {
return Err(D::Error::custom("unsupported schema version"));
}
Ok(Self)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct CursorDto(String);
impl CursorDto {
pub fn parse(value: impl Into<String>) -> Result<Self, WireValueError> {
let value = value.into();
if !(16..=2048).contains(&value.chars().count()) {
return Err(WireValueError::Cursor);
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Serialize for CursorDto {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.0)
}
}
impl<'de> Deserialize<'de> for CursorDto {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Self::parse(String::deserialize(deserializer)?).map_err(D::Error::custom)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct DateTimeDto(String);
impl DateTimeDto {
pub fn parse(value: impl Into<String>) -> Result<Self, WireValueError> {
let value = value.into();
OffsetDateTime::parse(&value, &Rfc3339).map_err(|_| WireValueError::DateTime)?;
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Serialize for DateTimeDto {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.0)
}
}
impl<'de> Deserialize<'de> for DateTimeDto {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Self::parse(String::deserialize(deserializer)?).map_err(D::Error::custom)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WireValueError {
SafeInteger,
PositiveInteger,
Cursor,
DateTime,
}
impl fmt::Display for WireValueError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::SafeInteger => "integer exceeds the interoperable JSON range",
Self::PositiveInteger => "expected a positive integer",
Self::Cursor => "invalid cursor length",
Self::DateTime => "invalid RFC 3339 date-time",
};
formatter.write_str(message)
}
}
impl std::error::Error for WireValueError {}

View file

@ -0,0 +1,87 @@
use remora_contracts::{AttemptOutcomeRequestDto, EventFrameDto, JobFailureDto, JobSubmissionDto};
use serde::Deserialize;
use serde_json::Value;
#[derive(Deserialize)]
struct Corpus {
job_submissions: Vec<Value>,
event_frames: Vec<Value>,
attempt_outcomes: Vec<Value>,
job_failures: Vec<Value>,
}
#[test]
fn mutable_dtos_round_trip_the_shared_corpus() {
let corpus: Corpus =
serde_json::from_str(include_str!("../../../contracts/golden/domain.json")).unwrap();
for value in corpus.job_submissions {
let dto: JobSubmissionDto = serde_json::from_value(value.clone()).unwrap();
assert_eq!(serde_json::to_value(dto).unwrap(), value);
}
for value in corpus.event_frames {
let dto: EventFrameDto = serde_json::from_value(value.clone()).unwrap();
assert_eq!(serde_json::to_value(dto).unwrap(), value);
}
for value in corpus.attempt_outcomes {
let dto: AttemptOutcomeRequestDto = serde_json::from_value(value.clone()).unwrap();
assert_eq!(serde_json::to_value(dto).unwrap(), value);
}
for value in corpus.job_failures {
let dto: JobFailureDto = serde_json::from_value(value.clone()).unwrap();
assert_eq!(serde_json::to_value(dto).unwrap(), value);
}
}
#[test]
fn dto_bindings_reject_cross_field_and_nullability_violations() {
let contradictory_failures = [
serde_json::json!({
"class": "source_not_found",
"message": "missing",
"retryable": true
}),
serde_json::json!({
"class": "transient_target",
"message": "unavailable",
"retryable": false
}),
];
for value in contradictory_failures {
assert!(serde_json::from_value::<JobFailureDto>(value).is_err());
}
let null_optional = serde_json::json!({
"source": {
"kind": "http",
"url": "https://example.invalid/file",
"result_path": "file.bin"
},
"label": null
});
assert!(serde_json::from_value::<JobSubmissionDto>(null_optional).is_err());
}
#[test]
fn websocket_bindings_reject_values_outside_the_schema() {
let invalid = [
serde_json::json!({
"schema_version": 2,
"kind": "heartbeat",
"event_id": "event_0001",
"sequence": 1,
"cursor": "cursor-value-0001",
"occurred_at": "2026-07-15T00:00:00Z"
}),
serde_json::json!({
"schema_version": 1,
"kind": "heartbeat",
"event_id": "event_0001",
"sequence": 0,
"cursor": "short",
"occurred_at": "not-a-date"
}),
];
for value in invalid {
assert!(serde_json::from_value::<EventFrameDto>(value).is_err());
}
}

View file

@ -0,0 +1,140 @@
use std::{collections::BTreeSet, fs};
use serde_json::Value;
#[test]
fn api_contract_documents_parse_and_local_references_resolve() {
let openapi_source = fs::read_to_string("../../docs/api/openapi.yaml").unwrap();
let openapi: Value = serde_yaml_ng::from_str(&openapi_source).unwrap();
assert_eq!(
openapi.pointer("/openapi").and_then(Value::as_str),
Some("3.2.0")
);
assert_local_references_resolve(&openapi);
let events_source = fs::read_to_string("../../docs/api/events.schema.json").unwrap();
let events: Value = serde_json::from_str(&events_source).unwrap();
assert_eq!(
events.pointer("/$schema").and_then(Value::as_str),
Some("https://json-schema.org/draft/2020-12/schema")
);
assert_local_references_resolve(&events);
jsonschema::meta::validate(&events).unwrap();
let event_validator = jsonschema::draft202012::options()
.should_validate_formats(true)
.build(&events)
.unwrap();
let corpus: Value =
serde_json::from_str(include_str!("../../../contracts/golden/domain.json")).unwrap();
for frame in corpus["event_frames"].as_array().unwrap() {
event_validator.validate(frame).unwrap();
}
let component_schema = openapi_component_schema(&openapi);
jsonschema::meta::validate(&component_schema).unwrap();
validate_openapi_corpus(
&component_schema,
&corpus,
"JobSubmission",
"job_submissions",
);
validate_openapi_corpus(
&component_schema,
&corpus,
"AttemptOutcomeRequest",
"attempt_outcomes",
);
validate_openapi_corpus(&component_schema, &corpus, "JobFailure", "job_failures");
}
fn openapi_component_schema(openapi: &Value) -> Value {
let mut definitions = openapi
.pointer("/components/schemas")
.and_then(Value::as_object)
.unwrap()
.clone();
for schema in definitions.values_mut() {
rewrite_component_references(schema);
}
serde_json::json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": definitions
})
}
fn rewrite_component_references(value: &mut Value) {
match value {
Value::Array(values) => {
for value in values {
rewrite_component_references(value);
}
}
Value::Object(object) => {
if let Some(Value::String(reference)) = object.get_mut("$ref")
&& let Some(name) = reference.strip_prefix("#/components/schemas/")
{
*reference = format!("#/$defs/{name}");
}
object.remove("discriminator");
for value in object.values_mut() {
rewrite_component_references(value);
}
}
_ => {}
}
}
fn validate_openapi_corpus(
component_schema: &Value,
corpus: &Value,
definition: &str,
corpus_key: &str,
) {
let mut schema = component_schema.clone();
schema.as_object_mut().unwrap().insert(
"$ref".to_owned(),
Value::String(format!("#/$defs/{definition}")),
);
let validator = jsonschema::draft202012::options()
.should_validate_formats(true)
.build(&schema)
.unwrap();
for value in corpus[corpus_key].as_array().unwrap() {
validator.validate(value).unwrap();
}
}
fn assert_local_references_resolve(document: &Value) {
let mut references = BTreeSet::new();
collect_local_references(document, &mut references);
assert!(!references.is_empty());
for reference in references {
let pointer = reference.strip_prefix('#').unwrap();
assert!(
document.pointer(pointer).is_some(),
"unresolved local reference {reference}"
);
}
}
fn collect_local_references(value: &Value, references: &mut BTreeSet<String>) {
match value {
Value::Array(values) => {
for value in values {
collect_local_references(value, references);
}
}
Value::Object(object) => {
if let Some(reference) = object.get("$ref").and_then(Value::as_str)
&& reference.starts_with("#/")
{
references.insert(reference.to_owned());
}
for value in object.values() {
collect_local_references(value, references);
}
}
_ => {}
}
}

View file

@ -0,0 +1,16 @@
[package]
name = "remora-domain"
edition.workspace = true
publish = false
rust-version.workspace = true
version.workspace = true
[dependencies]
serde.workspace = true
serde_json_canonicalizer.workspace = true
sha2.workspace = true
unicode-general-category.workspace = true
unicode-normalization.workspace = true
[dev-dependencies]
serde_json.workspace = true

View file

@ -0,0 +1,68 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum FailureDisposition {
Permanent,
Retryable,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeFailureClass {
Authorization,
SourceNotFound,
SourceChanged,
TransientSource,
TransientTarget,
Integrity,
Capacity,
Internal,
}
impl RuntimeFailureClass {
#[must_use]
pub const fn disposition(self) -> FailureDisposition {
match self {
Self::Authorization | Self::SourceNotFound | Self::SourceChanged => {
FailureDisposition::Permanent
}
Self::TransientSource
| Self::TransientTarget
| Self::Integrity
| Self::Capacity
| Self::Internal => FailureDisposition::Retryable,
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum FailureClass {
Validation,
Authorization,
SourceNotFound,
SourceChanged,
TransientSource,
TransientTarget,
Integrity,
Capacity,
Internal,
Cancelled,
LostAttempt,
}
impl From<RuntimeFailureClass> for FailureClass {
fn from(value: RuntimeFailureClass) -> Self {
match value {
RuntimeFailureClass::Authorization => Self::Authorization,
RuntimeFailureClass::SourceNotFound => Self::SourceNotFound,
RuntimeFailureClass::SourceChanged => Self::SourceChanged,
RuntimeFailureClass::TransientSource => Self::TransientSource,
RuntimeFailureClass::TransientTarget => Self::TransientTarget,
RuntimeFailureClass::Integrity => Self::Integrity,
RuntimeFailureClass::Capacity => Self::Capacity,
RuntimeFailureClass::Internal => Self::Internal,
}
}
}

View file

@ -0,0 +1,45 @@
use std::fmt;
use serde::Serialize;
use sha2::{Digest, Sha256};
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct RequestFingerprint([u8; 32]);
impl RequestFingerprint {
#[must_use]
pub fn to_hex(self) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut output = String::with_capacity(64);
for byte in self.0 {
output.push(char::from(HEX[usize::from(byte >> 4)]));
output.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
output
}
}
impl fmt::Debug for RequestFingerprint {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_tuple("RequestFingerprint")
.field(&self.to_hex())
.finish()
}
}
pub fn fingerprint(value: &impl Serialize) -> Result<RequestFingerprint, FingerprintError> {
let canonical = serde_json_canonicalizer::to_vec(value).map_err(|_| FingerprintError)?;
Ok(RequestFingerprint(Sha256::digest(canonical).into()))
}
#[derive(Debug)]
pub struct FingerprintError;
impl fmt::Display for FingerprintError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("request cannot be represented as canonical JSON")
}
}
impl std::error::Error for FingerprintError {}

View file

@ -0,0 +1,73 @@
use std::{fmt, str::FromStr};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct OpaqueId(String);
impl OpaqueId {
pub fn parse(value: impl Into<String>) -> Result<Self, IdError> {
let value = value.into();
let mut chars = value.chars();
let Some(first) = chars.next() else {
return Err(IdError);
};
if !(8..=128).contains(&value.len())
|| !first.is_ascii_alphanumeric()
|| !chars.all(|character| {
character.is_ascii_alphanumeric() || matches!(character, '_' | '-')
})
{
return Err(IdError);
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for OpaqueId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(formatter)
}
}
impl FromStr for OpaqueId {
type Err = IdError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::parse(value)
}
}
impl Serialize for OpaqueId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.0)
}
}
impl<'de> Deserialize<'de> for OpaqueId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Self::parse(String::deserialize(deserializer)?).map_err(D::Error::custom)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct IdError;
impl fmt::Display for IdError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("invalid opaque identifier")
}
}
impl std::error::Error for IdError {}

View file

@ -0,0 +1,16 @@
mod failure;
mod fingerprint;
mod id;
pub mod ports;
mod target;
mod validation;
mod vocabulary;
pub use failure::{FailureClass, FailureDisposition, RuntimeFailureClass};
pub use fingerprint::{FingerprintError, RequestFingerprint, fingerprint};
pub use id::{IdError, OpaqueId};
pub use target::{StorageScope, Target, TargetStoreId, TargetStoreIdError};
pub use validation::{BucketName, MemberPath, TargetPrefix, ValidationError};
pub use vocabulary::{
AttemptState, JobLifecycle, JobPhase, LeaseState, OutcomeRequest, StageState,
};

View file

@ -0,0 +1,90 @@
use std::future::Future;
use crate::{OpaqueId, RequestFingerprint};
pub trait Clock: Send + Sync {
type Instant: Copy + Ord + Send;
fn now(&self) -> Self::Instant;
}
pub trait DurableMutationStore: Send + Sync {
type Error;
type Request;
type Response;
fn execute(
&self,
scope: MutationScope<'_>,
request: &Self::Request,
request_fingerprint: RequestFingerprint,
) -> impl Future<Output = Result<Self::Response, Self::Error>> + Send;
}
pub trait CoordinationStore: Send + Sync {
type Error;
type Claim;
fn claim(
&self,
worker_incarnation: &OpaqueId,
operation_id: &OpaqueId,
) -> impl Future<Output = Result<Option<Self::Claim>, Self::Error>> + Send;
}
pub trait IdentityProvider: Send + Sync {
type Error;
type Principal;
fn authenticate(
&self,
bearer_token: &str,
) -> impl Future<Output = Result<Self::Principal, Self::Error>> + Send;
}
pub trait CredentialResolver: Send + Sync {
type Error;
type Secret;
fn resolve(
&self,
credential_id: &OpaqueId,
version_id: &OpaqueId,
) -> impl Future<Output = Result<Self::Secret, Self::Error>> + Send;
}
pub trait SourceAdapter: Send + Sync {
type Error;
type Request;
type ResolvedSource;
fn resolve(
&self,
request: &Self::Request,
) -> impl Future<Output = Result<Self::ResolvedSource, Self::Error>> + Send;
}
pub trait StorageAdapter: Send + Sync {
type Error;
type Request;
type StoredObject;
fn create(
&self,
request: &Self::Request,
) -> impl Future<Output = Result<Self::StoredObject, Self::Error>> + Send;
}
pub trait EventSink: Send + Sync {
type Error;
type Event;
fn publish(&self, event: Self::Event) -> impl Future<Output = Result<(), Self::Error>> + Send;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MutationScope<'a> {
pub subject: &'a OpaqueId,
pub operation: &'a str,
pub key: &'a str,
}

View file

@ -0,0 +1,55 @@
use std::fmt;
use crate::{BucketName, TargetPrefix};
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct TargetStoreId(String);
impl TargetStoreId {
pub fn parse(value: impl Into<String>) -> Result<Self, TargetStoreIdError> {
let value = value.into();
if value.is_empty() || value.chars().count() > 128 {
return Err(TargetStoreIdError);
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Target {
pub store: TargetStoreId,
pub bucket: BucketName,
pub prefix: TargetPrefix,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StorageScope {
pub store: TargetStoreId,
pub bucket: BucketName,
pub prefix: TargetPrefix,
}
impl StorageScope {
#[must_use]
pub fn covers(&self, target: &Target) -> bool {
self.store == target.store
&& self.bucket == target.bucket
&& target.prefix.segments().starts_with(self.prefix.segments())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TargetStoreIdError;
impl fmt::Display for TargetStoreIdError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("invalid target store identifier")
}
}
impl std::error::Error for TargetStoreIdError {}

View file

@ -0,0 +1,161 @@
use std::{collections::BTreeSet, fmt, str::FromStr};
use unicode_general_category::{GeneralCategory, get_general_category};
use unicode_normalization::UnicodeNormalization;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct BucketName(String);
impl BucketName {
pub fn parse(value: impl Into<String>) -> Result<Self, ValidationError> {
let value = value.into();
let bytes = value.as_bytes();
let valid_character = |byte: &u8| {
byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'.' | b'-')
};
if !(3..=63).contains(&bytes.len())
|| !bytes.first().is_some_and(u8::is_ascii_alphanumeric)
|| !bytes.last().is_some_and(u8::is_ascii_alphanumeric)
|| !bytes.iter().all(valid_character)
|| value.contains("..")
{
return Err(ValidationError::Bucket);
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl FromStr for BucketName {
type Err = ValidationError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::parse(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct TargetPrefix(Vec<String>);
impl TargetPrefix {
pub fn parse<'a>(
value: &str,
reserved_segments: impl IntoIterator<Item = &'a str>,
) -> Result<Self, ValidationError> {
if value == "/" || value.starts_with('/') || value.contains("//") || value.contains('\\') {
return Err(ValidationError::TargetPrefix);
}
let value = value.strip_suffix('/').unwrap_or(value);
if value.is_empty() {
return Ok(Self(Vec::new()));
}
let segments = value
.split('/')
.map(|segment| normalize_segment(segment, ValidationError::TargetPrefix))
.collect::<Result<Vec<_>, _>>()?;
let reserved_segments = reserved_segments
.into_iter()
.map(|segment| normalize_segment(segment, ValidationError::ReservedTargetPrefix))
.collect::<Result<BTreeSet<_>, _>>()?;
if segments
.iter()
.any(|segment| reserved_segments.contains(segment))
{
return Err(ValidationError::ReservedTargetPrefix);
}
Ok(Self(segments))
}
#[must_use]
pub fn segments(&self) -> &[String] {
&self.0
}
#[must_use]
pub fn canonical(&self) -> String {
self.0.join("/")
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct MemberPath(Vec<String>);
impl MemberPath {
pub fn parse(value: &str) -> Result<Self, ValidationError> {
if value.is_empty()
|| value.starts_with('/')
|| value.ends_with('/')
|| value.contains("//")
|| value.contains('\\')
{
return Err(ValidationError::MemberPath);
}
let segments = value
.split('/')
.map(|segment| normalize_segment(segment, ValidationError::MemberPath))
.collect::<Result<Vec<_>, _>>()?;
Ok(Self(segments))
}
#[must_use]
pub fn segments(&self) -> &[String] {
&self.0
}
#[must_use]
pub fn canonical(&self) -> String {
self.0.join("/")
}
}
impl FromStr for MemberPath {
type Err = ValidationError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::parse(value)
}
}
fn normalize_segment(segment: &str, error: ValidationError) -> Result<String, ValidationError> {
if segment.is_empty() || matches!(segment, "." | "..") || segment.chars().any(is_category_c) {
return Err(error);
}
Ok(segment.nfc().collect())
}
fn is_category_c(character: char) -> bool {
matches!(
get_general_category(character),
GeneralCategory::Control
| GeneralCategory::Format
| GeneralCategory::PrivateUse
| GeneralCategory::Surrogate
| GeneralCategory::Unassigned
)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ValidationError {
Bucket,
TargetPrefix,
ReservedTargetPrefix,
MemberPath,
}
impl fmt::Display for ValidationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::Bucket => "invalid bucket name",
Self::TargetPrefix => "invalid target prefix",
Self::ReservedTargetPrefix => "target prefix uses a reserved segment",
Self::MemberPath => "invalid result member path",
};
formatter.write_str(message)
}
}
impl std::error::Error for ValidationError {}

View file

@ -0,0 +1,60 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub enum JobPhase {
Queued,
Running,
RetryWait,
Cancelling,
Succeeded,
Failed,
Cancelled,
}
impl JobPhase {
#[must_use]
pub const fn is_terminal(self) -> bool {
matches!(self, Self::Succeeded | Self::Failed | Self::Cancelled)
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub enum AttemptState {
AttemptUnused,
AttemptAuthorized,
AttemptExecuting,
AttemptStopped,
AttemptCommitted,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub enum LeaseState {
NoLease,
LeaseLive,
LeaseExpired,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub enum StageState {
StageNone,
StagePartial,
StageComplete,
StageCleaned,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub enum OutcomeRequest {
NoOutcome,
WantsSuccess,
WantsRetryableFailure,
WantsPermanentFailure,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum JobLifecycle {
Visible,
SoftDeleted,
HardDeleting,
HardDeleted,
}

View file

@ -0,0 +1,137 @@
use remora_domain::{
BucketName, FailureDisposition, JobPhase, MemberPath, RuntimeFailureClass, StorageScope,
Target, TargetPrefix, TargetStoreId, fingerprint,
};
use serde::Deserialize;
use serde_json::Value;
#[derive(Deserialize)]
struct Corpus {
job_phases: Vec<JobPhase>,
runtime_failures: Vec<FailureCase>,
buckets: ValidationCases<String>,
target_prefixes: ValidationCases<CanonicalCase>,
reserved_target_prefixes: Vec<ReservedPrefixCase>,
member_paths: ValidationCases<CanonicalCase>,
scope_coverage: Vec<ScopeCase>,
fingerprints: Vec<FingerprintCase>,
}
#[derive(Deserialize)]
struct FailureCase {
class: RuntimeFailureClass,
disposition: FailureDisposition,
}
#[derive(Deserialize)]
struct ValidationCases<T> {
valid: Vec<T>,
invalid: Vec<String>,
}
#[derive(Deserialize)]
struct CanonicalCase {
input: String,
canonical: String,
}
#[derive(Deserialize)]
struct FingerprintCase {
value: Value,
canonical: String,
sha256: String,
}
#[derive(Deserialize)]
struct ScopeCase {
scope: [String; 3],
target: [String; 3],
covers: bool,
}
#[derive(Deserialize)]
struct ReservedPrefixCase {
input: String,
reserved: Vec<String>,
}
fn corpus() -> Corpus {
serde_json::from_str(include_str!("../../../contracts/golden/domain.json")).unwrap()
}
#[test]
fn protocol_phase_vocabulary_is_exact() {
let phases = corpus().job_phases;
assert_eq!(phases.len(), 7);
assert!(phases[..4].iter().all(|phase| !phase.is_terminal()));
assert!(phases[4..].iter().all(|phase| phase.is_terminal()));
}
#[test]
fn runtime_failure_dispositions_match_the_architecture() {
for case in corpus().runtime_failures {
assert_eq!(case.class.disposition(), case.disposition);
}
}
#[test]
fn domain_values_validate_and_normalize() {
let corpus = corpus();
for value in corpus.buckets.valid {
assert_eq!(BucketName::parse(&value).unwrap().as_str(), value);
}
for value in corpus.buckets.invalid {
assert!(BucketName::parse(value).is_err());
}
for case in corpus.target_prefixes.valid {
assert_eq!(
TargetPrefix::parse(&case.input, []).unwrap().canonical(),
case.canonical
);
}
for value in corpus.target_prefixes.invalid {
assert!(TargetPrefix::parse(&value, []).is_err());
}
for case in corpus.reserved_target_prefixes {
assert!(
TargetPrefix::parse(&case.input, case.reserved.iter().map(String::as_str)).is_err()
);
}
for case in corpus.member_paths.valid {
assert_eq!(
MemberPath::parse(&case.input).unwrap().canonical(),
case.canonical
);
}
for value in corpus.member_paths.invalid {
assert!(MemberPath::parse(&value).is_err());
}
}
#[test]
fn request_fingerprints_match_the_shared_corpus() {
for case in corpus().fingerprints {
assert_eq!(
serde_json_canonicalizer::to_string(&case.value).unwrap(),
case.canonical
);
assert_eq!(fingerprint(&case.value).unwrap().to_hex(), case.sha256);
}
}
#[test]
fn storage_scope_coverage_uses_exact_segment_tuples() {
for case in corpus().scope_coverage {
let scope = StorageScope {
store: TargetStoreId::parse(&case.scope[0]).unwrap(),
bucket: BucketName::parse(&case.scope[1]).unwrap(),
prefix: TargetPrefix::parse(&case.scope[2], []).unwrap(),
};
let target = Target {
store: TargetStoreId::parse(&case.target[0]).unwrap(),
bucket: BucketName::parse(&case.target[1]).unwrap(),
prefix: TargetPrefix::parse(&case.target[2], []).unwrap(),
};
assert_eq!(scope.covers(&target), case.covers);
}
}