trunk_recorder_plugin/
schema.rs1use schemars::generate::SchemaSettings;
38use schemars::JsonSchema;
39use serde_json::{Map, Value};
40
41pub fn schema_for<T: JsonSchema>() -> Value {
43 let generator = SchemaSettings::draft2020_12()
44 .with(|s| {
45 s.inline_subschemas = true;
46 s.meta_schema = None;
47 })
48 .into_generator();
49 let mut v = generator.into_root_schema_for::<T>().to_value();
50 normalize(&mut v);
51 if let Some(o) = v.as_object_mut() {
52 o.remove("title");
55 o.remove("description");
56 }
57 v
58}
59
60pub fn normalize(v: &mut Value) {
62 let Some(o) = v.as_object_mut() else { return };
63 if let Some(Value::Array(types)) = o.get("type") {
65 let rest: Vec<Value> = types.iter().filter(|t| t.as_str() != Some("null")).cloned().collect();
66 if rest.len() == 1 {
67 o.insert("type".into(), rest[0].clone());
68 }
69 }
70 if let Some(Value::Array(any)) = o.get("anyOf") {
72 let rest: Vec<Value> = any.iter().filter(|s| s.get("type").and_then(Value::as_str) != Some("null")).cloned().collect();
73 if rest.len() == 1 {
74 o.remove("anyOf");
75 if let Value::Object(inner) = &rest[0] {
76 for (k, x) in inner {
77 o.entry(k.clone()).or_insert_with(|| x.clone());
78 }
79 }
80 }
81 }
82 if let Some(Value::Array(one)) = o.get("oneOf") {
84 let mut values = Vec::new();
85 let mut labels = Vec::new();
86 let mut plain = true;
87 for s in one {
88 let vs: Vec<Value> = match (s.get("const"), s.get("enum")) {
89 (Some(c), _) => vec![c.clone()],
90 (None, Some(Value::Array(e))) => e.clone(),
91 _ => {
92 plain = false;
93 break;
94 }
95 };
96 for x in vs {
97 let label = s.get("title").or(s.get("description")).and_then(Value::as_str).map(str::to_string);
98 labels.push(label.map(Value::String).unwrap_or_else(|| x.clone()));
99 values.push(x);
100 }
101 }
102 if plain {
103 o.remove("oneOf");
104 o.insert("type".into(), "string".into());
105 if labels != values {
106 o.insert("x-enum-labels".into(), Value::Array(labels));
107 }
108 o.insert("enum".into(), Value::Array(values));
109 }
110 }
111 if let Some(d) = o.get("description").and_then(Value::as_str) {
113 let unwrapped = d.split("\n\n").map(|p| p.split_whitespace().collect::<Vec<_>>().join(" ")).collect::<Vec<_>>().join("\n\n");
114 o.insert("description".into(), unwrapped.into());
115 }
116 if !o.contains_key("title") {
118 if let Some(d) = o.get("description").and_then(Value::as_str).map(str::to_string) {
119 match d.split_once("\n\n") {
120 Some((title, rest)) => {
121 o.insert("title".into(), title.trim().into());
122 o.insert("description".into(), rest.trim().into());
123 }
124 None if d.len() <= 60 && !d.trim_end().ends_with('.') => {
125 o.insert("title".into(), d.trim().into());
126 o.remove("description");
127 }
128 None => {}
129 }
130 }
131 }
132 if o.get("default").is_some_and(Value::is_null) {
134 o.remove("default");
135 }
136 if let Some(f) = o.get("format").and_then(Value::as_str) {
138 if f.starts_with("int") || f.starts_with("uint") || f == "float" || f == "double" {
139 o.remove("format");
140 }
141 }
142 if let Some(Value::Object(props)) = o.get("properties") {
144 let order: Vec<Value> = props.keys().map(|k| Value::String(k.clone())).collect();
145 o.insert("x-order".into(), Value::Array(order));
146 }
147 for key in ["properties", "items"] {
148 match o.get_mut(key) {
149 Some(Value::Object(props)) if key == "properties" => props.values_mut().for_each(normalize),
150 Some(items @ Value::Object(_)) => normalize(items),
151 _ => {}
152 }
153 }
154 if let Some(Value::Object(props)) = o.get_mut("properties") {
156 let marked: Vec<String> = props
157 .iter_mut()
158 .filter_map(|(k, f)| f.as_object_mut()?.remove("x-required").filter(|v| v == &Value::Bool(true)).map(|_| k.clone()))
159 .collect();
160 if !marked.is_empty() {
161 let req = o.entry("required").or_insert_with(|| Value::Array(vec![]));
162 if let Value::Array(r) = req {
163 for k in marked {
164 if !r.contains(&Value::String(k.clone())) {
165 r.push(Value::String(k));
166 }
167 }
168 }
169 }
170 }
171 if o.get("required").and_then(Value::as_array).is_some_and(|r| r.is_empty()) {
173 o.remove("required");
174 }
175}
176
177#[derive(Clone, Copy, Debug, Default, serde::Deserialize)]
179pub struct NoConfig {}
180
181impl JsonSchema for NoConfig {
182 fn schema_name() -> std::borrow::Cow<'static, str> {
183 "NoConfig".into()
184 }
185 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
186 let mut m = Map::new();
187 m.insert("type".into(), "object".into());
188 m.insert("properties".into(), Value::Object(Map::new()));
189 schemars::Schema::from(m)
190 }
191}
192
193pub(crate) fn is_empty(schema: &Value) -> bool {
194 schema.get("properties").and_then(Value::as_object).is_none_or(|p| p.is_empty())
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use serde::{Deserialize, Serialize};
201 use serde_json::json;
202
203 #[derive(Serialize, Deserialize, JsonSchema, Default)]
204 #[serde(rename_all = "camelCase", default)]
205 #[allow(dead_code)]
206 struct Settings {
207 server: String,
212 #[schemars(extend("x-secret" = true))]
214 api_key: Option<String>,
215 mode: Mode,
216 #[schemars(range(min = 1, max = 10))]
217 retries: u32,
218 }
219
220 #[derive(Serialize, Deserialize, JsonSchema, Default)]
221 #[serde(rename_all = "lowercase")]
222 #[allow(dead_code)]
223 enum Mode {
224 #[default]
225 Fast,
226 Slow,
228 }
229
230 #[derive(Serialize, Deserialize, JsonSchema, Default)]
231 #[serde(rename_all = "camelCase", default)]
232 #[allow(dead_code)]
233 struct Marked {
234 #[schemars(extend("x-required" = true))]
236 server: String,
237 streams: Vec<Stream>,
238 }
239
240 #[derive(Serialize, Deserialize, JsonSchema, Default)]
241 #[serde(rename_all = "camelCase", default)]
242 #[allow(dead_code)]
243 struct Stream {
244 #[schemars(extend("x-system" = true, "x-required" = true))]
246 short_name: String,
247 port: u16,
248 }
249
250 #[test]
251 fn required_and_system_fields_come_through() {
252 let s = schema_for::<Marked>();
253 assert_eq!(s["required"], json!(["server"]));
254 let item = &s["properties"]["streams"]["items"];
255 assert_eq!(item["properties"]["shortName"]["x-system"], true);
256 assert_eq!(item["required"], json!(["shortName"]));
257 assert!(item["properties"]["shortName"].get("x-required").is_none());
258 assert!(item.get("required").is_some() && s["properties"]["streams"].get("required").is_none());
259 }
260
261 #[test]
262 fn normalizes_what_schemars_makes() {
263 let s = schema_for::<Settings>();
264 let p = &s["properties"];
265 assert_eq!(p["server"]["title"], "Upload server");
266 assert_eq!(p["server"]["description"], "Where calls go.");
267 assert_eq!(p["apiKey"]["type"], "string");
268 assert_eq!(p["apiKey"]["title"], "API key");
269 assert_eq!(p["apiKey"]["x-secret"], true);
270 assert!(p["apiKey"].get("default").is_none());
271 assert_eq!(p["mode"]["enum"], json!(["fast", "slow"]));
272 assert_eq!(p["mode"]["x-enum-labels"], json!(["fast", "Slow and steady"]));
273 assert_eq!(p["mode"]["default"], "fast");
274 assert_eq!(p["retries"]["maximum"], 10);
275 assert!(p["retries"].get("format").is_none());
276 assert!(s.get("title").is_none());
277 assert_eq!(s["x-order"], json!(["server", "apiKey", "mode", "retries"]));
278 }
279}