Govern wire schemas from TypeSpec
This commit is contained in:
parent
fa81cb7f21
commit
2020dd4ed3
6 changed files with 10189 additions and 1571 deletions
|
|
@ -6,11 +6,13 @@ rust-version.workspace = true
|
|||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
fancy-regex = "0.16.2"
|
||||
jsonschema = { version = "0.47.0", default-features = false }
|
||||
remora-domain.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
time.workspace = true
|
||||
url = { version = "2.5.8", features = ["serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
jsonschema = { version = "0.47.0", default-features = false }
|
||||
serde_json.workspace = true
|
||||
serde_yaml_ng = "0.10.0"
|
||||
|
|
|
|||
448
crates/remora-contracts/src/generated_wire.rs
Normal file
448
crates/remora-contracts/src/generated_wire.rs
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Rfc3339DateTime(time::OffsetDateTime);
|
||||
|
||||
impl serde::Serialize for Rfc3339DateTime {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let value = self
|
||||
.0
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
serializer.serialize_str(&value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for Rfc3339DateTime {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = <String as serde::Deserialize>::deserialize(deserializer)?;
|
||||
time::OffsetDateTime::parse(&value, &time::format_description::well_known::Rfc3339)
|
||||
.map(Self)
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct ConstU64<const VALUE: u64>(u64);
|
||||
|
||||
impl<'de, const VALUE: u64> serde::Deserialize<'de> for ConstU64<VALUE> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = <u64 as serde::Deserialize>::deserialize(deserializer)?;
|
||||
if value == VALUE {
|
||||
Ok(Self(value))
|
||||
} else {
|
||||
Err(serde::de::Error::custom("incorrect integer literal"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct ConstBool<const VALUE: bool>(bool);
|
||||
|
||||
impl<'de, const VALUE: bool> serde::Deserialize<'de> for ConstBool<VALUE> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = <bool as serde::Deserialize>::deserialize(deserializer)?;
|
||||
if value == VALUE {
|
||||
Ok(Self(value))
|
||||
} else {
|
||||
Err(serde::de::Error::custom("incorrect boolean literal"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct BoundedU64<const MIN: u64, const MAX: u64>(u64);
|
||||
|
||||
impl<'de, const MIN: u64, const MAX: u64> serde::Deserialize<'de> for BoundedU64<MIN, MAX> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = <u64 as serde::Deserialize>::deserialize(deserializer)?;
|
||||
if (MIN..=MAX).contains(&value) {
|
||||
Ok(Self(value))
|
||||
} else {
|
||||
Err(serde::de::Error::custom("integer outside bounds"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct BoundedString<const MIN: usize, const MAX: usize>(String);
|
||||
|
||||
impl<'de, const MIN: usize, const MAX: usize> serde::Deserialize<'de> for BoundedString<MIN, MAX> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = <String as serde::Deserialize>::deserialize(deserializer)?;
|
||||
if (MIN..=MAX).contains(&value.chars().count()) {
|
||||
Ok(Self(value))
|
||||
} else {
|
||||
Err(serde::de::Error::custom("string length outside bounds"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct OpaqueId(String);
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for OpaqueId {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = <String as serde::Deserialize>::deserialize(deserializer)?;
|
||||
let valid = (8..=128).contains(&value.len())
|
||||
&& value
|
||||
.as_bytes()
|
||||
.first()
|
||||
.is_some_and(u8::is_ascii_alphanumeric)
|
||||
&& value
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-');
|
||||
if valid {
|
||||
Ok(Self(value))
|
||||
} else {
|
||||
Err(serde::de::Error::custom("invalid opaque ID"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct BucketName(String);
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for BucketName {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = <String as serde::Deserialize>::deserialize(deserializer)?;
|
||||
let pattern = fancy_regex::Regex::new("^(?!.*\\.\\.)[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$")
|
||||
.map_err(serde::de::Error::custom)?;
|
||||
match pattern.is_match(&value).map_err(serde::de::Error::custom)? {
|
||||
true => Ok(Self(value)),
|
||||
false => Err(serde::de::Error::custom(
|
||||
"string does not match required pattern",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct HuggingFaceRepositoryId(String);
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for HuggingFaceRepositoryId {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = <String as serde::Deserialize>::deserialize(deserializer)?;
|
||||
let pattern = fancy_regex::Regex::new("^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$")
|
||||
.map_err(serde::de::Error::custom)?;
|
||||
match pattern.is_match(&value).map_err(serde::de::Error::custom)? {
|
||||
true => Ok(Self(value)),
|
||||
false => Err(serde::de::Error::custom(
|
||||
"string does not match required pattern",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type CredentialId = OpaqueId;
|
||||
|
||||
pub type ResultPath = BoundedString<1, 4096>;
|
||||
|
||||
pub type EventCursor = BoundedString<16, 2048>;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub enum JobPhase {
|
||||
#[serde(rename = "Queued")]
|
||||
Queued,
|
||||
#[serde(rename = "Running")]
|
||||
Running,
|
||||
#[serde(rename = "RetryWait")]
|
||||
RetryWait,
|
||||
#[serde(rename = "Cancelling")]
|
||||
Cancelling,
|
||||
#[serde(rename = "Succeeded")]
|
||||
Succeeded,
|
||||
#[serde(rename = "Failed")]
|
||||
Failed,
|
||||
#[serde(rename = "Cancelled")]
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub enum JobLifecycle {
|
||||
#[serde(rename = "visible")]
|
||||
Visible,
|
||||
#[serde(rename = "soft_deleted")]
|
||||
SoftDeleted,
|
||||
#[serde(rename = "hard_deleting")]
|
||||
HardDeleting,
|
||||
#[serde(rename = "hard_deleted")]
|
||||
HardDeleted,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct TargetOverride {
|
||||
pub bucket: Option<BucketName>,
|
||||
pub prefix: Option<BoundedString<0, 1024>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct HttpSourceSubmission {
|
||||
pub url: url::Url,
|
||||
pub result_path: ResultPath,
|
||||
pub credential_id: Option<CredentialId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ObjectStoreSourceSubmission {
|
||||
pub store: BoundedString<1, 128>,
|
||||
pub bucket: BoundedString<3, 63>,
|
||||
pub key: BoundedString<1, 4096>,
|
||||
pub version_id: Option<BoundedString<0, 1024>>,
|
||||
pub result_path: ResultPath,
|
||||
pub credential_id: Option<CredentialId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct HuggingFaceSelection {
|
||||
pub paths: Option<Vec<ResultPath>>,
|
||||
pub prefixes: Option<Vec<ResultPath>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub enum HuggingFaceSourceSubmissionRepositoryType {
|
||||
#[serde(rename = "model")]
|
||||
Model,
|
||||
#[serde(rename = "dataset")]
|
||||
Dataset,
|
||||
#[serde(rename = "space")]
|
||||
Space,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct HuggingFaceSourceSubmission {
|
||||
pub repository_type: HuggingFaceSourceSubmissionRepositoryType,
|
||||
pub repository_id: HuggingFaceRepositoryId,
|
||||
pub revision: BoundedString<1, 256>,
|
||||
pub selection: Option<HuggingFaceSelection>,
|
||||
pub credential_id: Option<CredentialId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct JobSubmission {
|
||||
pub source: SourceSubmission,
|
||||
pub target: Option<TargetOverride>,
|
||||
pub label: Option<BoundedString<1, 200>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub enum PermanentJobFailureClass {
|
||||
#[serde(rename = "validation")]
|
||||
Validation,
|
||||
#[serde(rename = "authorization")]
|
||||
Authorization,
|
||||
#[serde(rename = "source_not_found")]
|
||||
SourceNotFound,
|
||||
#[serde(rename = "source_changed")]
|
||||
SourceChanged,
|
||||
#[serde(rename = "cancelled")]
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PermanentJobFailure {
|
||||
pub class: PermanentJobFailureClass,
|
||||
pub retryable: ConstBool<false>,
|
||||
pub message: BoundedString<0, 500>,
|
||||
pub safe_details: Option<std::collections::BTreeMap<String, SafeDetailValue>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub enum RetryableJobFailureClass {
|
||||
#[serde(rename = "transient_source")]
|
||||
TransientSource,
|
||||
#[serde(rename = "transient_target")]
|
||||
TransientTarget,
|
||||
#[serde(rename = "integrity")]
|
||||
Integrity,
|
||||
#[serde(rename = "capacity")]
|
||||
Capacity,
|
||||
#[serde(rename = "internal")]
|
||||
Internal,
|
||||
#[serde(rename = "lost_attempt")]
|
||||
LostAttempt,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct RetryableJobFailure {
|
||||
pub class: RetryableJobFailureClass,
|
||||
pub retryable: ConstBool<true>,
|
||||
pub message: BoundedString<0, 500>,
|
||||
pub safe_details: Option<std::collections::BTreeMap<String, SafeDetailValue>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct EventAttempt {
|
||||
pub attempt_id: OpaqueId,
|
||||
pub fence: BoundedU64<1, 9007199254740991>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct EventProgress {
|
||||
pub attempt_id: OpaqueId,
|
||||
pub fence: BoundedU64<1, 9007199254740991>,
|
||||
pub completed_bytes: BoundedU64<0, 9007199254740991>,
|
||||
pub total_bytes: Option<Option<i64>>,
|
||||
pub completed_members: BoundedU64<0, 9007199254740991>,
|
||||
pub total_members: Option<Option<i64>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct JobChanged {
|
||||
pub job_id: OpaqueId,
|
||||
pub job_revision: BoundedU64<1, 9007199254740991>,
|
||||
pub phase: JobPhase,
|
||||
pub lifecycle: JobLifecycle,
|
||||
pub authoritative_attempt: Option<EventAttempt>,
|
||||
pub progress: Option<EventProgress>,
|
||||
pub schema_version: ConstU64<1>,
|
||||
pub event_id: OpaqueId,
|
||||
pub sequence: BoundedU64<1, 9007199254740991>,
|
||||
pub cursor: EventCursor,
|
||||
pub occurred_at: Rfc3339DateTime,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub enum DeletionChangedState {
|
||||
#[serde(rename = "queued")]
|
||||
Queued,
|
||||
#[serde(rename = "running")]
|
||||
Running,
|
||||
#[serde(rename = "retry_wait")]
|
||||
RetryWait,
|
||||
#[serde(rename = "blocked")]
|
||||
Blocked,
|
||||
#[serde(rename = "completed")]
|
||||
Completed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DeletionChanged {
|
||||
pub job_id: OpaqueId,
|
||||
pub operation_id: OpaqueId,
|
||||
pub state: DeletionChangedState,
|
||||
pub schema_version: ConstU64<1>,
|
||||
pub event_id: OpaqueId,
|
||||
pub sequence: BoundedU64<1, 9007199254740991>,
|
||||
pub cursor: EventCursor,
|
||||
pub occurred_at: Rfc3339DateTime,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub enum ResyncRequiredReason {
|
||||
#[serde(rename = "cursor_absent")]
|
||||
CursorAbsent,
|
||||
#[serde(rename = "cursor_invalid")]
|
||||
CursorInvalid,
|
||||
#[serde(rename = "cursor_expired")]
|
||||
CursorExpired,
|
||||
#[serde(rename = "authorization_changed")]
|
||||
AuthorizationChanged,
|
||||
#[serde(rename = "history_unavailable")]
|
||||
HistoryUnavailable,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ResyncRequired {
|
||||
pub reason: ResyncRequiredReason,
|
||||
pub schema_version: ConstU64<1>,
|
||||
pub event_id: OpaqueId,
|
||||
pub sequence: BoundedU64<1, 9007199254740991>,
|
||||
pub cursor: EventCursor,
|
||||
pub occurred_at: Rfc3339DateTime,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Heartbeat {
|
||||
pub schema_version: ConstU64<1>,
|
||||
pub event_id: OpaqueId,
|
||||
pub sequence: BoundedU64<1, 9007199254740991>,
|
||||
pub cursor: EventCursor,
|
||||
pub occurred_at: Rfc3339DateTime,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum SourceSubmission {
|
||||
#[serde(rename = "http")]
|
||||
Http(HttpSourceSubmission),
|
||||
#[serde(rename = "object_store")]
|
||||
ObjectStore(ObjectStoreSourceSubmission),
|
||||
#[serde(rename = "hugging_face")]
|
||||
HuggingFace(HuggingFaceSourceSubmission),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum SafeDetailValue {
|
||||
Text(String),
|
||||
Integer(i64),
|
||||
Boolean(bool),
|
||||
SymbolName(()),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum JobFailure {
|
||||
Permanent(PermanentJobFailure),
|
||||
Retryable(RetryableJobFailure),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum EventFrame {
|
||||
#[serde(rename = "job_changed")]
|
||||
JobChanged(JobChanged),
|
||||
#[serde(rename = "deletion_changed")]
|
||||
DeletionChanged(DeletionChanged),
|
||||
#[serde(rename = "resync_required")]
|
||||
ResyncRequired(ResyncRequired),
|
||||
#[serde(rename = "heartbeat")]
|
||||
Heartbeat(Heartbeat),
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
mod credentials;
|
||||
mod dto;
|
||||
mod events;
|
||||
pub mod generated_wire;
|
||||
mod internal;
|
||||
mod public;
|
||||
mod validation;
|
||||
mod wire;
|
||||
|
||||
pub use credentials::{
|
||||
|
|
@ -46,6 +48,7 @@ pub use public::{
|
|||
ResultMemberAccessDto, ResultMemberDto, ResultMemberPageDto, SourceProjectionDto,
|
||||
StagingTargetDto, TerminalJobPhaseDto, TombstoneKindDto,
|
||||
};
|
||||
pub use validation::request_is_valid;
|
||||
pub use wire::{
|
||||
CursorDto, DateTimeDto, NullableDto, PositiveIntegerDto, SafeIntegerDto, SchemaVersionDto,
|
||||
SignedSafeIntegerDto,
|
||||
|
|
|
|||
61
crates/remora-contracts/src/validation.rs
Normal file
61
crates/remora-contracts/src/validation.rs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
use std::{collections::HashMap, sync::LazyLock};
|
||||
|
||||
use jsonschema::Validator;
|
||||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
const REQUEST_SCHEMAS: &[&str] = &[
|
||||
"JobSubmission",
|
||||
"SourceCredentialCreate",
|
||||
"SourceCredentialRotate",
|
||||
"ClaimRequest",
|
||||
"AttemptRenewRequest",
|
||||
"AttemptActivateRequest",
|
||||
"AttemptStopRequest",
|
||||
"AttemptStagingObjectRequest",
|
||||
"AttemptStagingWriteIntentRequest",
|
||||
"AttemptStagingMultipartRequest",
|
||||
"AttemptResolvedSourceRequest",
|
||||
"AttemptCheckpointRequest",
|
||||
"AttemptProgressRequest",
|
||||
"AttemptOutcomeRequest",
|
||||
];
|
||||
|
||||
static REQUEST_VALIDATORS: LazyLock<HashMap<&'static str, Validator>> = LazyLock::new(|| {
|
||||
let bundle: Value =
|
||||
serde_json::from_str(include_str!("../../../docs/api/events.schema.json")).unwrap();
|
||||
REQUEST_SCHEMAS
|
||||
.iter()
|
||||
.map(|name| {
|
||||
let mut schema = bundle.clone();
|
||||
schema
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert("$ref".into(), json!(format!("#/$defs/{name}")));
|
||||
let validator = jsonschema::draft202012::options()
|
||||
.should_validate_formats(true)
|
||||
.build(&schema)
|
||||
.unwrap();
|
||||
(*name, validator)
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
pub fn request_is_valid(name: &str, value: &impl Serialize) -> bool {
|
||||
let Some(validator) = REQUEST_VALIDATORS.get(name) else {
|
||||
return false;
|
||||
};
|
||||
serde_json::to_value(value)
|
||||
.ok()
|
||||
.is_some_and(|value| validator.is_valid(&value))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::request_is_valid;
|
||||
|
||||
#[test]
|
||||
fn rejects_unknown_schema_names() {
|
||||
assert!(!request_is_valid("Unknown", &serde_json::json!({})));
|
||||
}
|
||||
}
|
||||
63
crates/remora-contracts/tests/generated_wire.rs
Normal file
63
crates/remora-contracts/tests/generated_wire.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
use remora_contracts::generated_wire::{EventFrame, JobFailure, JobSubmission};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
#[test]
|
||||
fn generated_roots_accept_the_shared_valid_corpus() {
|
||||
let corpus: Value =
|
||||
serde_json::from_str(include_str!("../../../contracts/golden/domain.json")).unwrap();
|
||||
assert_all::<JobSubmission>(&corpus["job_submissions"]);
|
||||
assert_all::<JobFailure>(&corpus["job_failures"]);
|
||||
assert_all::<EventFrame>(&corpus["event_frames"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_roots_reject_governance_regressions() {
|
||||
assert_invalid::<JobSubmission>(json!({
|
||||
"source": {
|
||||
"kind": "http",
|
||||
"url": "not a URL",
|
||||
"result_path": "file.bin"
|
||||
}
|
||||
}));
|
||||
assert_invalid::<JobSubmission>(json!({
|
||||
"source": {
|
||||
"kind": "hugging_face",
|
||||
"repository_type": "model",
|
||||
"repository_id": "missing-owner",
|
||||
"revision": "main"
|
||||
}
|
||||
}));
|
||||
assert_invalid::<JobFailure>(json!({
|
||||
"class": "source_not_found",
|
||||
"message": "missing",
|
||||
"retryable": true
|
||||
}));
|
||||
assert_invalid::<EventFrame>(json!({
|
||||
"schema_version": 2,
|
||||
"kind": "heartbeat",
|
||||
"event_id": "event_0001",
|
||||
"sequence": 1,
|
||||
"cursor": "cursor-value-0001",
|
||||
"occurred_at": "2026-07-15T00:00:00Z"
|
||||
}));
|
||||
assert_invalid::<EventFrame>(json!({
|
||||
"schema_version": 1,
|
||||
"kind": "heartbeat",
|
||||
"event_id": "event_0001",
|
||||
"sequence": 1,
|
||||
"cursor": "cursor-value-0001",
|
||||
"occurred_at": "2026-07-15T00:00:00Z",
|
||||
"unexpected": true
|
||||
}));
|
||||
}
|
||||
|
||||
fn assert_all<T: DeserializeOwned>(values: &Value) {
|
||||
for value in values.as_array().unwrap() {
|
||||
serde_json::from_value::<T>(value.clone()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_invalid<T: DeserializeOwned>(value: Value) {
|
||||
assert!(serde_json::from_value::<T>(value).is_err());
|
||||
}
|
||||
|
|
@ -340,7 +340,24 @@ where
|
|||
T: DeserializeOwned + Serialize,
|
||||
{
|
||||
let dto: T = serde_json::from_value(value.clone()).unwrap();
|
||||
assert_eq!(serde_json::to_value(dto).unwrap(), value);
|
||||
let serialized = serde_json::to_value(&dto).unwrap();
|
||||
assert_eq!(serialized, value);
|
||||
if matches!(
|
||||
definition,
|
||||
"ClaimRequest"
|
||||
| "AttemptRenewRequest"
|
||||
| "AttemptActivateRequest"
|
||||
| "AttemptStopRequest"
|
||||
| "AttemptStagingObjectRequest"
|
||||
| "AttemptStagingWriteIntentRequest"
|
||||
| "AttemptStagingMultipartRequest"
|
||||
| "AttemptResolvedSourceRequest"
|
||||
| "AttemptCheckpointRequest"
|
||||
| "AttemptProgressRequest"
|
||||
| "AttemptOutcomeRequest"
|
||||
) {
|
||||
assert!(remora_contracts::request_is_valid(definition, &dto));
|
||||
}
|
||||
validate_openapi_value(component_schema, definition, &value);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue