Skip to main content

oxide_update_engine_types/
schema.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Helpers for building `x-rust-type` JSON Schema extensions.
6//!
7//! The `x-rust-type` extension tells code generators like typify and
8//! progenitor to use an existing Rust type rather than generating a
9//! new one. This module provides the shared constants and builder
10//! used by the manual `JsonSchema` implementations throughout this
11//! crate.
12
13/// Information for the `x-rust-type` JSON Schema extension.
14///
15/// When an [`EngineSpec`](crate::spec::EngineSpec) provides this,
16/// generic types parameterized by that spec will include the
17/// `x-rust-type` extension in their JSON Schema.
18///
19/// This describes only the **spec type** (the generic parameter).
20/// The outer types (`StepEvent`, `ProgressEvent`, `EventReport`)
21/// always live in `oxide-update-engine-types` and use the module
22/// constants directly.
23#[derive(Clone, Debug)]
24pub struct RustTypeInfo {
25    /// The crate that defines the spec type (e.g.
26    /// `"oxide-update-engine-types"` for `GenericSpec`, or
27    /// `"my-crate"` for a user-defined spec).
28    pub crate_name: &'static str,
29    /// The version requirement for the spec's crate (e.g. `"*"`).
30    pub version: &'static str,
31    /// The full path to the spec type (e.g.
32    /// `"oxide_update_engine_types::spec::GenericSpec"` or
33    /// `"my_crate::MySpec"`).
34    pub path: &'static str,
35}
36
37/// Crate name used in `x-rust-type` for types in this crate.
38pub(crate) const CRATE_NAME: &str = "oxide-update-engine-types";
39
40/// Version requirement used in `x-rust-type` for types in this
41/// crate.
42pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION");
43
44/// Module path for `events` types in this crate.
45#[cfg(feature = "schemars08")]
46pub(crate) const EVENTS_MODULE: &str = "oxide_update_engine_types::events";
47
48/// Full path to `GenericSpec` in this crate.
49pub(crate) const GENERIC_SPEC_PATH: &str =
50    "oxide_update_engine_types::spec::GenericSpec";
51
52/// Attaches a description to a schema, mirroring what the schemars
53/// derive does for doc comments on struct fields.
54///
55/// If the schema is a `$ref`, it is wrapped in `allOf` so that the
56/// description can sit alongside the reference (JSON Schema does not
57/// allow sibling keywords next to `$ref` in draft-07).
58#[cfg(feature = "schemars08")]
59pub(crate) fn with_description(
60    schema: schemars::schema::Schema,
61    description: &str,
62) -> schemars::schema::Schema {
63    use schemars::schema::{Metadata, Schema, SchemaObject};
64
65    match schema {
66        Schema::Object(obj) if obj.reference.is_some() => {
67            // Wrap `$ref` in allOf so the description is
68            // preserved.
69            SchemaObject {
70                metadata: Some(Box::new(Metadata {
71                    description: Some(description.to_owned()),
72                    ..Default::default()
73                })),
74                subschemas: Some(Box::new(
75                    schemars::schema::SubschemaValidation {
76                        all_of: Some(vec![Schema::Object(obj)]),
77                        ..Default::default()
78                    },
79                )),
80                ..Default::default()
81            }
82            .into()
83        }
84        Schema::Object(mut obj) => {
85            let metadata = obj.metadata.get_or_insert_with(Default::default);
86            metadata.description = Some(description.to_owned());
87            obj.into()
88        }
89        // Schema::Bool (e.g. `true` for unconstrained types like
90        // serde_json::Value) cannot carry metadata directly.
91        // Wrap it in allOf so we can attach the description,
92        // mirroring the $ref case above.
93        other => SchemaObject {
94            metadata: Some(Box::new(Metadata {
95                description: Some(description.to_owned()),
96                ..Default::default()
97            })),
98            subschemas: Some(Box::new(schemars::schema::SubschemaValidation {
99                all_of: Some(vec![other]),
100                ..Default::default()
101            })),
102            ..Default::default()
103        }
104        .into(),
105    }
106}
107
108/// Builds the `x-rust-type` extension value for a type in the
109/// `events` module of this crate.
110#[cfg(feature = "schemars08")]
111pub(crate) fn rust_type_for_events(type_path: &str) -> serde_json::Value {
112    serde_json::json!({
113        "crate": CRATE_NAME,
114        "version": VERSION,
115        "path": type_path,
116    })
117}
118
119/// Builds the `x-rust-type` extension value for a generic type
120/// parameterized by a spec (e.g. `StepEvent<GenericSpec>`).
121///
122/// The outer type (e.g. `StepEvent`) always lives in
123/// `oxide-update-engine-types`, so the top-level crate/version/path
124/// come from the module constants. The `parameters` array contains
125/// an inline schema with its own `x-rust-type` pointing to the
126/// spec type, which may live in a different crate.
127#[cfg(feature = "schemars08")]
128pub(crate) fn rust_type_for_generic(
129    info: &RustTypeInfo,
130    type_name: &str,
131) -> serde_json::Value {
132    serde_json::json!({
133        "crate": CRATE_NAME,
134        "version": VERSION,
135        "path": format!(
136            "{}::{}",
137            EVENTS_MODULE, type_name,
138        ),
139        "parameters": [
140            {
141                "x-rust-type": {
142                    "crate": info.crate_name,
143                    "version": info.version,
144                    "path": info.path,
145                }
146            }
147        ],
148    })
149}
150
151// NOTE: We only add `x-rust-type` to the outermost types such as `EventReport`.
152// Ideally, `StepEventKind` and other inner types below would also carry
153// `x-rust-type` in their schemas. However, schemars 0.8 does not provide a way
154// to intercept or transform a derived schema, so adding `x-rust-type` requires
155// a fully manual `JsonSchema` impl. Manual impls are quite fragile, and changes
156// to the shape of the type can silently break the schema.
157//
158// In practice, this is acceptable because these inner types are always accessed
159// through the top-level types which *do* carry `x-rust-type`, so typify
160// replaces the entire type tree.