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}