omf/
schema.rs

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        // Add a maximum for uint8 values.
41        if schema.format.as_deref() == Some("uint8") {
42            schema.number().maximum = Some(255.0);
43        }
44        // Move descriptions of simple enum values into the parent.
45        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        // Optionally remove descriptions. These get transformed into the documentation,
68        // they're a bit too complex to be readable in the schema itself.
69        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        // Change references to the generics Array_for_* to just Array.
80        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        // Then delegate to default implementation to visit any subschemas.
86        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}