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.