rivet/config/schema.rs
1//! **Layer: Config support** (JSON Schema generation, v0.7.3 P0.1/P0.2)
2//!
3//! Single source of truth for the JSON Schema that describes
4//! `rivet.yaml`. Used by:
5//!
6//! - `rivet schema config` — prints the schema to stdout so operators
7//! can drop it into their repo or pipe it to a file.
8//! - `tests/schema_drift.rs` — pins the in-tree
9//! `schemas/rivet.schema.json` artifact to the value derived from the
10//! current binary's types, so a config field added without a schema
11//! refresh fails CI.
12//!
13//! The schema title carries the running binary's version so editor
14//! tooling can show "Rivet 0.7.3 config" rather than a versionless
15//! generic name.
16
17use schemars::Schema;
18use schemars::generate::SchemaSettings;
19
20use crate::config::Config;
21
22/// Generate the JSON Schema for the top-level [`Config`] struct.
23///
24/// Returns a [`Schema`] suitable for `serde_json::to_string_pretty`.
25/// The schema embeds the binary's `CARGO_PKG_VERSION` in its `title`
26/// so a single shipped schema artifact carries its provenance.
27pub fn generate_config_schema() -> Schema {
28 // Draft 2020-12 is what the YAML Language Server and VS Code's
29 // RedHat YAML extension accept natively; `Draft07` works too but
30 // loses oneOf/anyOf composition niceties that the schemars derive
31 // emits for some of our enums.
32 let settings = SchemaSettings::draft2020_12();
33 let generator = settings.into_generator();
34 let mut schema = generator.into_root_schema_for::<Config>();
35 // Tag the schema with the binary version so editors / CI can
36 // detect drift between a checked-in artifact and the running
37 // toolchain.
38 schema.insert(
39 "title".into(),
40 serde_json::Value::String(format!(
41 "Rivet config (rivet-cli {})",
42 env!("CARGO_PKG_VERSION")
43 )),
44 );
45 schema
46}
47
48/// Render the schema as a pretty-printed JSON string, terminated by a
49/// trailing newline (POSIX text-file convention; matches what
50/// `serde_json::to_writer_pretty` + `writeln!` produces and what the
51/// schema-drift test expects in the checked-in artifact).
52pub fn generate_config_schema_pretty() -> crate::error::Result<String> {
53 let schema = generate_config_schema();
54 let mut s = serde_json::to_string_pretty(&schema)?;
55 if !s.ends_with('\n') {
56 s.push('\n');
57 }
58 Ok(s)
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 #[test]
66 fn schema_carries_binary_version_in_title() {
67 let schema = generate_config_schema();
68 let title = schema
69 .as_object()
70 .and_then(|m| m.get("title"))
71 .and_then(|v| v.as_str())
72 .expect("schema must have a title");
73 assert!(
74 title.contains(env!("CARGO_PKG_VERSION")),
75 "title must embed CARGO_PKG_VERSION ({}): {title}",
76 env!("CARGO_PKG_VERSION"),
77 );
78 }
79
80 #[test]
81 fn schema_is_valid_json_object() {
82 let pretty = generate_config_schema_pretty().expect("schema must serialize");
83 // Round-trip through serde_json::Value to prove the rendered
84 // bytes parse back as a JSON object (sanity check for any
85 // future custom Display/Serialize change on Schema).
86 let v: serde_json::Value =
87 serde_json::from_str(&pretty).expect("rendered schema must parse as JSON");
88 assert!(v.is_object(), "schema root must be a JSON object");
89 }
90
91 #[test]
92 fn schema_ends_with_newline() {
93 let pretty = generate_config_schema_pretty().unwrap();
94 assert!(
95 pretty.ends_with('\n'),
96 "schema artifact must end with a trailing newline so POSIX tools / diffs behave",
97 );
98 }
99
100 #[test]
101 fn schema_mentions_top_level_required_fields() {
102 // Smoke check that the derive surfaces the Config root's
103 // required keys. If the Config struct gains/loses a required
104 // field, this test fails before the in-tree schema file goes
105 // stale.
106 let pretty = generate_config_schema_pretty().unwrap();
107 assert!(pretty.contains("\"source\""), "must include 'source' field");
108 assert!(
109 pretty.contains("\"exports\""),
110 "must include 'exports' field"
111 );
112 }
113}