Add Rust domain kernel and wire contracts
This commit is contained in:
parent
ff379fb002
commit
4da5a980ed
23 changed files with 3757 additions and 1 deletions
1128
Cargo.lock
generated
1128
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
17
Cargo.toml
17
Cargo.toml
|
|
@ -1,8 +1,23 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["crates/remora-api", "crates/remora-worker"]
|
members = [
|
||||||
|
"crates/remora-api",
|
||||||
|
"crates/remora-contracts",
|
||||||
|
"crates/remora-domain",
|
||||||
|
"crates/remora-worker",
|
||||||
|
]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.97"
|
rust-version = "1.97"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
remora-domain = { path = "crates/remora-domain" }
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
serde_json = "1.0.150"
|
||||||
|
serde_json_canonicalizer = "0.3.2"
|
||||||
|
sha2 = "0.11.0"
|
||||||
|
time = { version = "0.3.47", features = ["parsing"] }
|
||||||
|
unicode-general-category = "1.1.0"
|
||||||
|
unicode-normalization = "0.1.25"
|
||||||
|
|
|
||||||
244
contracts/golden/domain.json
Normal file
244
contracts/golden/domain.json
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
{
|
||||||
|
"job_phases": [
|
||||||
|
"Queued",
|
||||||
|
"Running",
|
||||||
|
"RetryWait",
|
||||||
|
"Cancelling",
|
||||||
|
"Succeeded",
|
||||||
|
"Failed",
|
||||||
|
"Cancelled"
|
||||||
|
],
|
||||||
|
"runtime_failures": [
|
||||||
|
{ "class": "authorization", "disposition": "permanent" },
|
||||||
|
{ "class": "source_not_found", "disposition": "permanent" },
|
||||||
|
{ "class": "source_changed", "disposition": "permanent" },
|
||||||
|
{ "class": "transient_source", "disposition": "retryable" },
|
||||||
|
{ "class": "transient_target", "disposition": "retryable" },
|
||||||
|
{ "class": "integrity", "disposition": "retryable" },
|
||||||
|
{ "class": "capacity", "disposition": "retryable" },
|
||||||
|
{ "class": "internal", "disposition": "retryable" }
|
||||||
|
],
|
||||||
|
"buckets": {
|
||||||
|
"valid": ["abc", "archive-01", "one.two-three"],
|
||||||
|
"invalid": [
|
||||||
|
"ab",
|
||||||
|
"Uppercase",
|
||||||
|
"-leading",
|
||||||
|
"trailing-",
|
||||||
|
"two..dots",
|
||||||
|
"under_score"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"target_prefixes": {
|
||||||
|
"valid": [
|
||||||
|
{ "input": "", "canonical": "" },
|
||||||
|
{ "input": "models/current/", "canonical": "models/current" },
|
||||||
|
{ "input": "cafe\u0301/data", "canonical": "caf\u00e9/data" }
|
||||||
|
],
|
||||||
|
"invalid": [
|
||||||
|
"/",
|
||||||
|
"/leading",
|
||||||
|
"two//segments",
|
||||||
|
"trailing//",
|
||||||
|
"dot/./segment",
|
||||||
|
"parent/../segment",
|
||||||
|
"back\\slash",
|
||||||
|
"private/\ue000"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reserved_target_prefixes": [
|
||||||
|
{ "input": "_remora/data", "reserved": ["_remora"] },
|
||||||
|
{
|
||||||
|
"input": "models/_internal/current",
|
||||||
|
"reserved": ["_internal", "_remora"]
|
||||||
|
},
|
||||||
|
{ "input": "caf\u00e9/data", "reserved": ["cafe\u0301"] }
|
||||||
|
],
|
||||||
|
"member_paths": {
|
||||||
|
"valid": [
|
||||||
|
{ "input": "result.bin", "canonical": "result.bin" },
|
||||||
|
{ "input": "cafe\u0301/model.bin", "canonical": "caf\u00e9/model.bin" }
|
||||||
|
],
|
||||||
|
"invalid": [
|
||||||
|
"",
|
||||||
|
"/leading",
|
||||||
|
"trailing/",
|
||||||
|
"two//segments",
|
||||||
|
".",
|
||||||
|
"../escape",
|
||||||
|
"back\\slash",
|
||||||
|
"private/\ue000"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scope_coverage": [
|
||||||
|
{
|
||||||
|
"scope": ["target-main", "target-bucket", "models"],
|
||||||
|
"target": ["target-main", "target-bucket", "models/current"],
|
||||||
|
"covers": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["target-main", "target-bucket", "model"],
|
||||||
|
"target": ["target-main", "target-bucket", "models/current"],
|
||||||
|
"covers": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["target-main", "target-bucket", ""],
|
||||||
|
"target": ["target-main", "target-bucket", "anything"],
|
||||||
|
"covers": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["target-main", "target-bucket", "models"],
|
||||||
|
"target": ["target-other", "target-bucket", "models"],
|
||||||
|
"covers": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fingerprints": [
|
||||||
|
{
|
||||||
|
"value": { "b": 2, "a": 1 },
|
||||||
|
"canonical": "{\"a\":1,\"b\":2}",
|
||||||
|
"sha256": "43258cff783fe7036d8a43033f830adfc60ec037382473548ac742b888292777"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": { "null": null, "array": [3, { "x": "\u00e9" }] },
|
||||||
|
"canonical": "{\"array\":[3,{\"x\":\"\u00e9\"}],\"null\":null}",
|
||||||
|
"sha256": "25c443412e0460747f70c56aa9aebf017d0f03e7571a0fa0016050e846127b14"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": [333333333.33333329, 1e30, 4.5, 2e-3, 1e-27],
|
||||||
|
"canonical": "[333333333.3333333,1e+30,4.5,0.002,1e-27]",
|
||||||
|
"sha256": "7c6bc86d861387d823ae596b79ca0b26567dddc22a77acb1f4e06d441f555adf"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"job_submissions": [
|
||||||
|
{
|
||||||
|
"source": {
|
||||||
|
"kind": "http",
|
||||||
|
"url": "https://example.invalid/file",
|
||||||
|
"result_path": "file.bin"
|
||||||
|
},
|
||||||
|
"label": "HTTP transfer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": {
|
||||||
|
"kind": "object_store",
|
||||||
|
"store": "source-main",
|
||||||
|
"bucket": "source-bucket",
|
||||||
|
"key": "models/model.bin",
|
||||||
|
"version_id": "version-1",
|
||||||
|
"result_path": "model.bin",
|
||||||
|
"credential_id": "credential_01"
|
||||||
|
},
|
||||||
|
"target": { "bucket": "target-bucket", "prefix": "models/current" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": {
|
||||||
|
"kind": "hugging_face",
|
||||||
|
"repository_type": "model",
|
||||||
|
"repository_id": "owner/model",
|
||||||
|
"revision": "main",
|
||||||
|
"selection": { "paths": ["config.json"], "prefixes": ["weights"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"event_frames": [
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"kind": "job_changed",
|
||||||
|
"event_id": "event_0001",
|
||||||
|
"sequence": 1,
|
||||||
|
"cursor": "cursor-value-0001",
|
||||||
|
"occurred_at": "2026-07-15T00:00:00Z",
|
||||||
|
"job_id": "job_000001",
|
||||||
|
"job_revision": 3,
|
||||||
|
"phase": "Running",
|
||||||
|
"lifecycle": "visible",
|
||||||
|
"authoritative_attempt": {
|
||||||
|
"attempt_id": "attempt_0001",
|
||||||
|
"fence": 2
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"attempt_id": "attempt_0001",
|
||||||
|
"fence": 2,
|
||||||
|
"completed_bytes": 1024,
|
||||||
|
"total_bytes": null,
|
||||||
|
"completed_members": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"kind": "resync_required",
|
||||||
|
"event_id": "event_0002",
|
||||||
|
"sequence": 2,
|
||||||
|
"cursor": "cursor-value-0002",
|
||||||
|
"occurred_at": "2026-07-15T00:00:01Z",
|
||||||
|
"reason": "authorization_changed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"kind": "deletion_changed",
|
||||||
|
"event_id": "event_0003",
|
||||||
|
"sequence": 3,
|
||||||
|
"cursor": "cursor-value-0003",
|
||||||
|
"occurred_at": "2026-07-15T00:00:02Z",
|
||||||
|
"job_id": "job_000001",
|
||||||
|
"operation_id": "operation_03",
|
||||||
|
"state": "running"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"kind": "heartbeat",
|
||||||
|
"event_id": "event_0004",
|
||||||
|
"sequence": 4,
|
||||||
|
"cursor": "cursor-value-0004",
|
||||||
|
"occurred_at": "2026-07-15T00:00:03Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"attempt_outcomes": [
|
||||||
|
{
|
||||||
|
"operation_id": "operation_01",
|
||||||
|
"fence": 2,
|
||||||
|
"worker_incarnation": "worker_0001",
|
||||||
|
"outcome": {
|
||||||
|
"kind": "success",
|
||||||
|
"result": {
|
||||||
|
"kind": "manifest",
|
||||||
|
"manifest": {
|
||||||
|
"store": "target-main",
|
||||||
|
"bucket": "target-bucket",
|
||||||
|
"key": "_remora/results/manifest.json",
|
||||||
|
"version_id": "version-1",
|
||||||
|
"checksum": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||||
|
},
|
||||||
|
"member_count": 1,
|
||||||
|
"total_bytes": 4096
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_id": "operation_02",
|
||||||
|
"fence": 3,
|
||||||
|
"worker_incarnation": "worker_0001",
|
||||||
|
"outcome": {
|
||||||
|
"kind": "failure",
|
||||||
|
"failure": {
|
||||||
|
"class": "transient_source",
|
||||||
|
"message": "source temporarily unavailable",
|
||||||
|
"safe_details": { "status": 503, "retry_hint_used": true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"job_failures": [
|
||||||
|
{
|
||||||
|
"class": "source_not_found",
|
||||||
|
"message": "source does not exist",
|
||||||
|
"retryable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": "transient_target",
|
||||||
|
"message": "target temporarily unavailable",
|
||||||
|
"retryable": true,
|
||||||
|
"safe_details": { "status": 503, "retry_hint_used": true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
16
crates/remora-contracts/Cargo.toml
Normal file
16
crates/remora-contracts/Cargo.toml
Normal 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"
|
||||||
149
crates/remora-contracts/src/credentials.rs
Normal file
149
crates/remora-contracts/src/credentials.rs
Normal 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>,
|
||||||
|
}
|
||||||
356
crates/remora-contracts/src/dto.rs
Normal file
356
crates/remora-contracts/src/dto.rs
Normal 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)
|
||||||
|
}
|
||||||
114
crates/remora-contracts/src/events.rs
Normal file
114
crates/remora-contracts/src/events.rs
Normal 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,
|
||||||
|
}
|
||||||
169
crates/remora-contracts/src/internal.rs
Normal file
169
crates/remora-contracts/src/internal.rs
Normal 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,
|
||||||
|
}
|
||||||
49
crates/remora-contracts/src/lib.rs
Normal file
49
crates/remora-contracts/src/lib.rs
Normal 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,
|
||||||
|
};
|
||||||
326
crates/remora-contracts/src/public.rs
Normal file
326
crates/remora-contracts/src/public.rs
Normal 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,
|
||||||
|
}
|
||||||
242
crates/remora-contracts/src/wire.rs
Normal file
242
crates/remora-contracts/src/wire.rs
Normal 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 {}
|
||||||
87
crates/remora-contracts/tests/golden.rs
Normal file
87
crates/remora-contracts/tests/golden.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
140
crates/remora-contracts/tests/schema_contract.rs
Normal file
140
crates/remora-contracts/tests/schema_contract.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
crates/remora-domain/Cargo.toml
Normal file
16
crates/remora-domain/Cargo.toml
Normal 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
|
||||||
68
crates/remora-domain/src/failure.rs
Normal file
68
crates/remora-domain/src/failure.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
crates/remora-domain/src/fingerprint.rs
Normal file
45
crates/remora-domain/src/fingerprint.rs
Normal 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 {}
|
||||||
73
crates/remora-domain/src/id.rs
Normal file
73
crates/remora-domain/src/id.rs
Normal 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 {}
|
||||||
16
crates/remora-domain/src/lib.rs
Normal file
16
crates/remora-domain/src/lib.rs
Normal 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,
|
||||||
|
};
|
||||||
90
crates/remora-domain/src/ports.rs
Normal file
90
crates/remora-domain/src/ports.rs
Normal 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,
|
||||||
|
}
|
||||||
55
crates/remora-domain/src/target.rs
Normal file
55
crates/remora-domain/src/target.rs
Normal 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 {}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue