Skip to content

Commit 039191f

Browse files
committed
feat: JSON Schema export for all plugin-kind crates + spec doc fixes
Make the Rust protocol the single source of truth for downstream language SDKs by exporting JSON Schema from every plugin-kind crate. - Add #[derive(JsonSchema)] to the public wire types in provider, trigger, log-storage, transport, and notifier protocol crates. - Add an export_schema bin to all 9 remaining crates (provider, trigger, log-storage, transport, queue, workflow-runner, durable-store, memory-store, notifier), mirroring the existing plugin/subject bins; emit per-type files plus an _all.json bundle into schemas/<crate>/. - Fix default_out_dir() in all export bins to resolve the workspace root one level up (was writing schemas outside the repo in this single-level layout). - Add scripts/export-all-schemas.sh to regenerate every bundle in one command. - spec.md: document the notifier role (§5 table, §7.9 methods, §12.5 types) and add queue/release_pending to the canonical §5 queue method list. cargo build/test/clippy/fmt green.
1 parent 6b98095 commit 039191f

152 files changed

Lines changed: 12876 additions & 28 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

animus-durable-store-protocol/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,10 @@ animus-plugin-protocol = { path = "../animus-plugin-protocol" }
1313
serde = { version = "1", features = ["derive"] }
1414
serde_json = "1"
1515
schemars = { version = "1.0" }
16+
17+
[dev-dependencies]
18+
tempfile = "3"
19+
20+
[[bin]]
21+
name = "animus-durable-store-protocol-export-schema"
22+
path = "src/bin/export_schema.rs"
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
//! Export JSON Schema artifacts for every public wire type in
2+
//! `animus-durable-store-protocol`.
3+
//!
4+
//! Usage:
5+
//!
6+
//! ```text
7+
//! cargo run -p animus-durable-store-protocol --bin animus-durable-store-protocol-export-schema -- [--out <dir>]
8+
//! ```
9+
//!
10+
//! Mirrors the binary in `animus-plugin-protocol`. The default output
11+
//! directory is `schemas/animus-durable-store-protocol/` resolved relative
12+
//! to the workspace root. One file is written per type plus an `_all.json`
13+
//! bundle for tooling that wants a single artifact.
14+
//!
15+
//! The `step_status` / `commit_outcome` / `run_status` / `error_codes`
16+
//! const modules are intentionally omitted: they are not wire message
17+
//! types. The wire-level error shape is `animus_plugin_protocol::RpcError`,
18+
//! exported by the sibling `animus-plugin-protocol` schema binary.
19+
20+
use std::collections::BTreeMap;
21+
use std::env;
22+
use std::fs;
23+
use std::path::{Path, PathBuf};
24+
use std::process::ExitCode;
25+
26+
use animus_durable_store_protocol::{
27+
AbandonStepRequest, AbandonStepResponse, BeginStepRequest, BeginStepResponse,
28+
BeginWorkflowRunRequest, BeginWorkflowRunResponse, CommitStepRequest, CommitStepResponse,
29+
DurableStoreCapabilities, InFlightRun, QueryRunRequest, QueryRunResponse,
30+
RecoverInFlightRequest, RecoverInFlightResponse, StepError, StepRecord,
31+
};
32+
use schemars::{schema_for, Schema};
33+
34+
fn default_out_dir() -> PathBuf {
35+
let base = env::var_os("CARGO_MANIFEST_DIR")
36+
.map(PathBuf::from)
37+
.and_then(|dir| dir.parent().map(Path::to_path_buf))
38+
.unwrap_or_else(|| env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
39+
base.join("schemas").join("animus-durable-store-protocol")
40+
}
41+
42+
fn parse_out_dir(args: &[String]) -> Option<PathBuf> {
43+
let mut iter = args.iter().skip(1);
44+
while let Some(arg) = iter.next() {
45+
match arg.as_str() {
46+
"--out" | "-o" => {
47+
if let Some(value) = iter.next() {
48+
return Some(PathBuf::from(value));
49+
}
50+
}
51+
other if other.starts_with("--out=") => {
52+
return Some(PathBuf::from(&other["--out=".len()..]));
53+
}
54+
_ => {}
55+
}
56+
}
57+
None
58+
}
59+
60+
/// Build the list of `(TypeName, Schema)` pairs. Centralized so the
61+
/// smoke test and the binary stay in sync.
62+
pub fn all_schemas() -> Vec<(&'static str, Schema)> {
63+
vec![
64+
(
65+
"BeginWorkflowRunRequest",
66+
schema_for!(BeginWorkflowRunRequest),
67+
),
68+
(
69+
"BeginWorkflowRunResponse",
70+
schema_for!(BeginWorkflowRunResponse),
71+
),
72+
("BeginStepRequest", schema_for!(BeginStepRequest)),
73+
("BeginStepResponse", schema_for!(BeginStepResponse)),
74+
("CommitStepRequest", schema_for!(CommitStepRequest)),
75+
("CommitStepResponse", schema_for!(CommitStepResponse)),
76+
("StepError", schema_for!(StepError)),
77+
("AbandonStepRequest", schema_for!(AbandonStepRequest)),
78+
("AbandonStepResponse", schema_for!(AbandonStepResponse)),
79+
(
80+
"RecoverInFlightRequest",
81+
schema_for!(RecoverInFlightRequest),
82+
),
83+
(
84+
"RecoverInFlightResponse",
85+
schema_for!(RecoverInFlightResponse),
86+
),
87+
("InFlightRun", schema_for!(InFlightRun)),
88+
("QueryRunRequest", schema_for!(QueryRunRequest)),
89+
("QueryRunResponse", schema_for!(QueryRunResponse)),
90+
("StepRecord", schema_for!(StepRecord)),
91+
(
92+
"DurableStoreCapabilities",
93+
schema_for!(DurableStoreCapabilities),
94+
),
95+
]
96+
}
97+
98+
/// Write every type's schema to `out_dir` and emit a combined
99+
/// `_all.json` bundle. Used by the binary and the smoke test.
100+
///
101+
/// The bundle places every type — and every nested `$defs` entry —
102+
/// under a single top-level `$defs`, so `#/$defs/<Name>` references
103+
/// inside the bundled schemas resolve against the bundle root.
104+
pub fn export_to(out_dir: &Path) -> std::io::Result<usize> {
105+
fs::create_dir_all(out_dir)?;
106+
let schemas = all_schemas();
107+
let mut defs: BTreeMap<String, serde_json::Value> = BTreeMap::new();
108+
for (name, schema) in &schemas {
109+
let path = out_dir.join(format!("{name}.json"));
110+
let pretty = serde_json::to_string_pretty(schema).expect("schema serializes to JSON");
111+
fs::write(&path, format!("{pretty}\n"))?;
112+
113+
let mut as_value = serde_json::to_value(schema).expect("schema serializes to JSON value");
114+
if let Some(obj) = as_value.as_object_mut() {
115+
if let Some(serde_json::Value::Object(inner)) = obj.remove("$defs") {
116+
for (k, v) in inner {
117+
defs.entry(k).or_insert(v);
118+
}
119+
}
120+
obj.remove("$schema");
121+
}
122+
defs.insert((*name).to_string(), as_value);
123+
}
124+
125+
let bundle = serde_json::json!({
126+
"$schema": "https://json-schema.org/draft/2020-12/schema",
127+
"title": "animus-durable-store-protocol",
128+
"$defs": defs,
129+
});
130+
let bundle_path = out_dir.join("_all.json");
131+
let bundle_pretty = serde_json::to_string_pretty(&bundle).expect("bundle serializes to JSON");
132+
fs::write(&bundle_path, format!("{bundle_pretty}\n"))?;
133+
Ok(schemas.len())
134+
}
135+
136+
fn main() -> ExitCode {
137+
let args: Vec<String> = env::args().collect();
138+
let out_dir = parse_out_dir(&args).unwrap_or_else(default_out_dir);
139+
match export_to(&out_dir) {
140+
Ok(count) => {
141+
println!("wrote {count} schemas + _all.json to {}", out_dir.display());
142+
ExitCode::SUCCESS
143+
}
144+
Err(err) => {
145+
eprintln!("export-schema: {err}");
146+
ExitCode::FAILURE
147+
}
148+
}
149+
}
150+
151+
#[cfg(test)]
152+
mod tests {
153+
use super::*;
154+
use serde_json::Value;
155+
156+
#[test]
157+
fn export_writes_one_file_per_type_and_bundle() {
158+
let tmp = tempfile::tempdir().expect("tempdir");
159+
let count = export_to(tmp.path()).expect("export ok");
160+
assert!(count > 0);
161+
162+
for (name, _) in all_schemas() {
163+
let path = tmp.path().join(format!("{name}.json"));
164+
let raw = std::fs::read_to_string(&path).expect("schema file readable");
165+
let value: Value = serde_json::from_str(&raw).expect("schema file parses");
166+
assert!(value.is_object(), "{name} schema should be a JSON object");
167+
assert!(
168+
value.get("$schema").is_some(),
169+
"{name} schema should include $schema"
170+
);
171+
assert!(
172+
value.get("title").is_some(),
173+
"{name} schema should include title"
174+
);
175+
}
176+
177+
let bundle_raw =
178+
std::fs::read_to_string(tmp.path().join("_all.json")).expect("bundle readable");
179+
let bundle: Value = serde_json::from_str(&bundle_raw).expect("bundle parses");
180+
let defs = bundle
181+
.get("$defs")
182+
.and_then(|d| d.as_object())
183+
.expect("bundle has $defs");
184+
for (name, _) in all_schemas() {
185+
assert!(
186+
defs.contains_key(name),
187+
"bundle $defs should contain {name}"
188+
);
189+
}
190+
assert!(
191+
bundle.get("$schema").is_some(),
192+
"bundle should advertise $schema"
193+
);
194+
}
195+
196+
#[test]
197+
fn begin_step_request_emits_object_type() {
198+
let schema = schema_for!(BeginStepRequest);
199+
let value = serde_json::to_value(&schema).expect("serializes");
200+
let type_field = value.get("type").expect("schema has a type field").clone();
201+
assert!(
202+
type_field == Value::String("object".to_string())
203+
|| type_field
204+
.as_array()
205+
.map(|arr| arr.iter().any(|v| v == "object"))
206+
.unwrap_or(false),
207+
"BeginStepRequest schema should report object type, got {type_field}"
208+
);
209+
}
210+
}

animus-log-storage-protocol/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,11 @@ anyhow.workspace = true
1818
chrono.workspace = true
1919
thiserror.workspace = true
2020
futures-core.workspace = true
21+
schemars = { version = "1.0", features = ["chrono04"] }
22+
23+
[dev-dependencies]
24+
tempfile = "3"
25+
26+
[[bin]]
27+
name = "animus-log-storage-protocol-export-schema"
28+
path = "src/bin/export_schema.rs"

0 commit comments

Comments
 (0)