Skip to main content

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}