1use std::fmt::Write;
2
3use schemars::{
4 JsonSchema,
5 r#gen::SchemaSettings,
6 schema::{
7 InstanceType, Metadata, RootSchema, Schema, SchemaObject, SingleOrVec, SubschemaValidation,
8 },
9 visit::{Visitor, visit_schema_object},
10};
11use serde_json::Value;
12
13use crate::{Project, format_full_name};
14
15fn simple_enum_variant(outer_schema: &Schema) -> Option<(String, String)> {
16 if let Schema::Object(schema) = outer_schema {
17 let Some([Value::String(variant)]) = schema.enum_values.as_deref() else {
18 return None;
19 };
20 let Some(Metadata {
21 description: Some(descr),
22 ..
23 }) = schema.metadata.as_deref()
24 else {
25 return None;
26 };
27 Some((variant.clone(), descr.clone()))
28 } else {
29 None
30 }
31}
32
33#[derive(Debug, Clone, Default)]
34struct TweakSchema {
35 remove_descr: bool,
36}
37
38impl Visitor for TweakSchema {
39 fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
40 if schema.format.as_deref() == Some("uint8") {
42 schema.number().maximum = Some(255.0);
43 }
44 if let Some(SubschemaValidation {
46 one_of: Some(variants),
47 ..
48 }) = schema.subschemas.as_deref()
49 {
50 if let Some(v) = variants
51 .iter()
52 .map(simple_enum_variant)
53 .collect::<Option<Vec<_>>>()
54 {
55 schema.subschemas = None;
56 schema.enum_values = Some(v.iter().map(|(name, _)| (&name[..]).into()).collect());
57 schema.instance_type = Some(SingleOrVec::Single(Box::new(InstanceType::String)));
58 let mut descr = schema.metadata().description.clone().unwrap_or_default();
59 descr += "\n\n### Values\n\n";
60 for (n, d) in v {
61 let body = d.replace("\n\n", "\n\n ");
62 write!(&mut descr, "`{n}`\n: {body}\n\n").unwrap();
63 }
64 schema.metadata().description = Some(descr);
65 }
66 }
67 if self.remove_descr {
70 let mut empty = false;
71 if let Some(m) = schema.metadata.as_deref_mut() {
72 m.description = None;
73 empty = m == &Metadata::default();
74 }
75 if empty {
76 schema.metadata = None;
77 }
78 }
79 if let Some(r) = schema.reference.as_mut() {
81 if r.starts_with("#/definitions/Array_for_") {
82 "#/definitions/Array".clone_into(r);
83 }
84 }
85 visit_schema_object(self, schema);
87 }
88}
89
90pub(crate) fn schema_for<T: JsonSchema>(remove_descr: bool) -> RootSchema {
91 SchemaSettings::draft2019_09()
92 .with_visitor(TweakSchema { remove_descr })
93 .into_generator()
94 .into_root_schema_for::<T>()
95}
96
97pub(crate) fn project_schema(remove_descr: bool) -> RootSchema {
98 let mut root = schema_for::<Project>(remove_descr);
99 root.schema.metadata().title = Some(format_full_name());
100 root.schema.metadata().id =
101 Some("https://github.com/gmggroup/omf-rust/blob/main/omf.schema.json".to_owned());
102 let array_def = root.definitions.get("Array_for_Boolean").unwrap().clone();
103 root.definitions
104 .retain(|name, _| !name.starts_with("Array_for"));
105 root.definitions.insert("Array".to_owned(), array_def);
106 root
107}
108
109pub fn json_schema() -> RootSchema {
110 project_schema(true)
111}
112
113#[cfg(test)]
114pub(crate) mod tests {
115 use schemars::schema::RootSchema;
116
117 use crate::schema::json_schema;
118
119 const SCHEMA: &str = "omf.schema.json";
120
121 #[ignore = "used to get schema"]
122 #[test]
123 fn update_schema() {
124 std::fs::write(
125 SCHEMA,
126 serde_json::to_string_pretty(&json_schema())
127 .unwrap()
128 .as_bytes(),
129 )
130 .unwrap();
131 crate::schema_doc::update_schema_docs();
132 }
133
134 #[ignore = "used to get schema docs"]
135 #[test]
136 fn update_schema_docs() {
137 crate::schema_doc::update_schema_docs();
138 #[cfg(feature = "parquet")]
139 crate::file::parquet::schemas::dump_parquet_schemas();
140 }
141
142 #[test]
143 fn schema() {
144 let schema = json_schema();
145 let expected: RootSchema =
146 serde_json::from_reader(std::fs::File::open(SCHEMA).unwrap()).unwrap();
147 assert!(schema == expected, "schema has changed");
148 }
149}