dev_scope/models/
mod.rs

1use crate::models::core::{ModelMetadata, ModelRoot};
2use anyhow::anyhow;
3
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use serde_yaml::Value;
7use tracing::warn;
8
9mod core;
10mod v1alpha;
11
12pub mod prelude {
13    pub use crate::models::core::*;
14    pub use crate::models::v1alpha::prelude::*;
15    pub use crate::models::{HelpMetadata, ScopeModel};
16}
17
18pub trait HelpMetadata {
19    fn metadata(&self) -> &ModelMetadata;
20    fn full_name(&self) -> String;
21    fn name(&self) -> &str {
22        &self.metadata().name
23    }
24    fn file_path(&self) -> String {
25        self.metadata().file_path()
26    }
27    fn containing_dir(&self) -> String {
28        self.metadata().containing_dir()
29    }
30    fn exec_path(&self) -> String {
31        self.metadata().exec_path()
32    }
33    fn description(&self) -> String {
34        self.metadata().description()
35    }
36}
37
38pub trait ScopeModel<S>: HelpMetadata {
39    fn api_version(&self) -> String;
40    fn kind(&self) -> String;
41    fn spec(&self) -> &S;
42}
43
44pub trait InternalScopeModel<S, R>:
45    JsonSchema + Serialize + for<'a> Deserialize<'a> + ScopeModel<S>
46where
47    R: for<'a> Deserialize<'a>,
48{
49    fn int_api_version() -> String;
50    fn int_kind() -> String;
51    fn known_type(input: &ModelRoot<Value>) -> anyhow::Result<Option<R>> {
52        if Self::int_api_version().to_lowercase() == input.api_version.to_lowercase()
53            && Self::int_kind().to_lowercase() == input.kind.to_lowercase()
54        {
55            let value = serde_json::to_value(input)?;
56            if let Err(e) = Self::validate_resource(&value) {
57                warn!(target: "user", "Resource '{}' didn't match the schema for {}. {}", input.full_name(), Self::int_kind(), e);
58            }
59            return Ok(Some(serde_json::from_value::<R>(value)?));
60        }
61        Ok(None)
62    }
63
64    fn validate_resource(input: &serde_json::Value) -> anyhow::Result<()> {
65        let mut schema_gen = make_schema_generator();
66        let schema = schema_gen.root_schema_for::<Self>();
67        let schema_json = serde_json::to_value(&schema)?;
68        let compiled_schema = jsonschema::JSONSchema::compile(&schema_json)
69            .expect("internal json schema to be valid");
70        if let Err(err_iter) = compiled_schema.validate(input) {
71            let mut errors = Vec::new();
72            for err in err_iter {
73                errors.push(err.to_string());
74            }
75            return Err(anyhow!(errors.join("\n")));
76        };
77
78        Ok(())
79    }
80
81    #[cfg(test)]
82    fn examples() -> Vec<String>;
83
84    #[cfg(test)]
85    fn create_and_validate(
86        schema_gen: &mut schemars::gen::SchemaGenerator,
87        out_dir: &str,
88        merged_schema: &str,
89    ) -> anyhow::Result<()> {
90        let schema = schema_gen.root_schema_for::<Self>();
91        let schema_json = serde_json::to_string_pretty(&schema)?;
92
93        let path_prefix: String = Self::int_api_version()
94            .split(&['.', '/'])
95            .rev()
96            .collect::<Vec<_>>()
97            .join(".");
98
99        std::fs::write(
100            format!("{}/{}.{}.json", out_dir, path_prefix, Self::int_kind()),
101            &schema_json,
102        )?;
103
104        for example in Self::examples() {
105            validate_schema::<Self>(&schema_json, &example)?;
106            validate_schema::<Self>(merged_schema, &example)?;
107        }
108        Ok(())
109    }
110}
111
112pub(crate) fn make_schema_generator() -> schemars::gen::SchemaGenerator {
113    let settings = schemars::gen::SchemaSettings::draft2019_09().with(|s| {
114        s.option_nullable = true;
115    });
116    settings.into_generator()
117}
118
119#[cfg(test)]
120fn validate_schema<T>(schema_json: &str, example_path: &str) -> anyhow::Result<()>
121where
122    T: schemars::JsonSchema + for<'a> serde::Deserialize<'a> + Serialize,
123{
124    let example = std::fs::read_to_string(format!(
125        "{}/examples/{}",
126        env!("CARGO_MANIFEST_DIR"),
127        example_path
128    ))
129    .unwrap();
130    let parsed: T = serde_yaml::from_str(&example)?;
131
132    let schema = serde_json::from_str(schema_json)?;
133
134    let compiled_schema = jsonschema::JSONSchema::compile(&schema).expect("A valid schema");
135
136    let parsed_json = serde_json::to_value(&parsed)?;
137    if let Err(err_iter) = compiled_schema.validate(&parsed_json) {
138        println!("{}", serde_json::to_string_pretty(&parsed_json).unwrap());
139        for e in err_iter {
140            println!("error: {}", e);
141        }
142        unreachable!();
143    };
144
145    Ok(())
146}
147
148#[cfg(test)]
149mod schema_gen {
150    use crate::models::v1alpha::prelude::*;
151    use crate::models::InternalScopeModel;
152
153    use schemars::JsonSchema;
154    use serde::{Deserialize, Serialize};
155
156    #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)]
157    #[serde(untagged)]
158    enum ScopeTypes {
159        ReportLocation(V1AlphaReportLocation),
160        ReportDefinition(V1AlphaReportDefinition),
161        KnownError(V1AlphaKnownError),
162        DoctorGroup(V1AlphaDoctorGroup),
163    }
164
165    #[test]
166    fn create_and_validate_schemas() {
167        let out_dir = format!("{}/schema", env!("CARGO_MANIFEST_DIR"));
168        std::fs::remove_dir_all(&out_dir).ok();
169        std::fs::create_dir_all(&out_dir).unwrap();
170
171        let mut schema_gen = crate::models::make_schema_generator();
172        let merged_schema = schema_gen.root_schema_for::<ScopeTypes>();
173        let merged_schema_json = serde_json::to_string_pretty(&merged_schema).unwrap();
174        std::fs::write(format!("{}/merged.json", out_dir), &merged_schema_json).unwrap();
175
176        V1AlphaReportLocation::create_and_validate(&mut schema_gen, &out_dir, &merged_schema_json)
177            .unwrap();
178        V1AlphaReportDefinition::create_and_validate(
179            &mut schema_gen,
180            &out_dir,
181            &merged_schema_json,
182        )
183        .unwrap();
184        V1AlphaKnownError::create_and_validate(&mut schema_gen, &out_dir, &merged_schema_json)
185            .unwrap();
186        V1AlphaDoctorGroup::create_and_validate(&mut schema_gen, &out_dir, &merged_schema_json)
187            .unwrap();
188    }
189}