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