use crate::models::core::{ModelMetadata, ModelRoot};
use anyhow::anyhow;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_yaml::Value;
use tracing::warn;
mod core;
mod v1alpha;
pub mod prelude {
pub use crate::models::core::*;
pub use crate::models::v1alpha::prelude::*;
pub use crate::models::{HelpMetadata, ScopeModel};
}
pub trait HelpMetadata {
fn metadata(&self) -> &ModelMetadata;
fn full_name(&self) -> String;
fn name(&self) -> &str {
&self.metadata().name
}
fn file_path(&self) -> String {
self.metadata().file_path()
}
fn containing_dir(&self) -> String {
self.metadata().containing_dir()
}
fn exec_path(&self) -> String {
self.metadata().exec_path()
}
fn description(&self) -> String {
self.metadata().description()
}
}
pub trait ScopeModel<S>: HelpMetadata {
fn api_version(&self) -> String;
fn kind(&self) -> String;
fn spec(&self) -> &S;
}
pub trait InternalScopeModel<S, R>:
JsonSchema + Serialize + for<'a> Deserialize<'a> + ScopeModel<S>
where
R: for<'a> Deserialize<'a>,
{
fn int_api_version() -> String;
fn int_kind() -> String;
fn known_type(input: &ModelRoot<Value>) -> anyhow::Result<Option<R>> {
if Self::int_api_version().to_lowercase() == input.api_version.to_lowercase()
&& Self::int_kind().to_lowercase() == input.kind.to_lowercase()
{
let value = serde_json::to_value(input)?;
if let Err(e) = Self::validate_resource(&value) {
warn!(target: "user", "Resource '{}' didn't match the schema for {}. {}", input.full_name(), Self::int_kind(), e);
}
return Ok(Some(serde_json::from_value::<R>(value)?));
}
Ok(None)
}
fn validate_resource(input: &serde_json::Value) -> anyhow::Result<()> {
let mut schema_gen = make_schema_generator();
let schema = schema_gen.root_schema_for::<Self>();
let schema_json = serde_json::to_value(&schema)?;
let compiled_schema = jsonschema::JSONSchema::compile(&schema_json)
.expect("internal json schema to be valid");
if let Err(err_iter) = compiled_schema.validate(input) {
let mut errors = Vec::new();
for err in err_iter {
errors.push(err.to_string());
}
return Err(anyhow!(errors.join("\n")));
};
Ok(())
}
#[cfg(test)]
fn examples() -> Vec<String>;
#[cfg(test)]
fn create_and_validate(
schema_gen: &mut schemars::gen::SchemaGenerator,
out_dir: &str,
merged_schema: &str,
) -> anyhow::Result<()> {
let schema = schema_gen.root_schema_for::<Self>();
let schema_json = serde_json::to_string_pretty(&schema)?;
let path_prefix: String = Self::int_api_version()
.split(&['.', '/'])
.rev()
.collect::<Vec<_>>()
.join(".");
std::fs::write(
format!("{}/{}.{}.json", out_dir, path_prefix, Self::int_kind()),
&schema_json,
)?;
for example in Self::examples() {
validate_schema::<Self>(&schema_json, &example)?;
validate_schema::<Self>(merged_schema, &example)?;
}
Ok(())
}
}
pub(crate) fn make_schema_generator() -> schemars::gen::SchemaGenerator {
let settings = schemars::gen::SchemaSettings::draft2019_09().with(|s| {
s.option_nullable = true;
});
settings.into_generator()
}
#[cfg(test)]
fn validate_schema<T>(schema_json: &str, example_path: &str) -> anyhow::Result<()>
where
T: schemars::JsonSchema + for<'a> serde::Deserialize<'a> + Serialize,
{
let example = std::fs::read_to_string(format!(
"{}/examples/{}",
env!("CARGO_MANIFEST_DIR"),
example_path
))
.unwrap();
let parsed: T = serde_yaml::from_str(&example)?;
let schema = serde_json::from_str(schema_json)?;
let compiled_schema = jsonschema::JSONSchema::compile(&schema).expect("A valid schema");
let parsed_json = serde_json::to_value(&parsed)?;
if let Err(err_iter) = compiled_schema.validate(&parsed_json) {
println!("{}", serde_json::to_string_pretty(&parsed_json).unwrap());
for e in err_iter {
println!("error: {}", e);
}
unreachable!();
};
Ok(())
}
#[cfg(test)]
mod schema_gen {
use crate::models::v1alpha::prelude::*;
use crate::models::InternalScopeModel;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)]
#[serde(untagged)]
enum ScopeTypes {
ReportLocation(V1AlphaReportLocation),
ReportDefinition(V1AlphaReportDefinition),
KnownError(V1AlphaKnownError),
DoctorGroup(V1AlphaDoctorGroup),
}
#[test]
fn create_and_validate_schemas() {
let out_dir = format!("{}/schema", env!("CARGO_MANIFEST_DIR"));
std::fs::remove_dir_all(&out_dir).ok();
std::fs::create_dir_all(&out_dir).unwrap();
let mut schema_gen = crate::models::make_schema_generator();
let merged_schema = schema_gen.root_schema_for::<ScopeTypes>();
let merged_schema_json = serde_json::to_string_pretty(&merged_schema).unwrap();
std::fs::write(format!("{}/merged.json", out_dir), &merged_schema_json).unwrap();
V1AlphaReportLocation::create_and_validate(&mut schema_gen, &out_dir, &merged_schema_json)
.unwrap();
V1AlphaReportDefinition::create_and_validate(
&mut schema_gen,
&out_dir,
&merged_schema_json,
)
.unwrap();
V1AlphaKnownError::create_and_validate(&mut schema_gen, &out_dir, &merged_schema_json)
.unwrap();
V1AlphaDoctorGroup::create_and_validate(&mut schema_gen, &out_dir, &merged_schema_json)
.unwrap();
}
}