Add Rust domain kernel and wire contracts
This commit is contained in:
parent
ff379fb002
commit
4da5a980ed
2 changed files with 3757 additions and 1 deletions
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);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue