use crate::codegen::style::Style;
use crate::codegen::utils::folder_tree::FolderTree;
use crate::{Definition, ModelDef, ModelType, Type, TypeReference};
use anyhow::anyhow;
use indexmap::IndexMap;
use path_absolutize::Absolutize;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
pub struct Context {
definitions: IndexMap<PathBuf, Definition>,
folder_tree: FolderTree,
root_folder: PathBuf,
style: Option<Style>,
}
impl Context {
pub fn new_from_folder(folder: &PathBuf) -> anyhow::Result<Self> {
let folder = folder.absolutize().unwrap().as_ref().to_path_buf();
let folder = &folder;
let config_value = Self::load_spec_config(folder)?;
let style = {
let mut style: Option<Style> = None;
if let Some(config) = config_value.as_ref() {
if let Some(style_value) = config.get("style").cloned() {
let style_value = serde_json::from_value::<Style>(style_value)?;
style = Some(style_value)
}
}
style
};
let mut definitions = IndexMap::new();
let mut spec_folder = FolderTree::new();
for entry in WalkDir::new(folder).sort_by_file_name() {
let entry = entry.unwrap();
let spec = entry.path();
if !spec.is_file() {
continue;
}
if !spec
.extension()
.map(|ext| ext == "yaml")
.unwrap_or_default()
{
continue;
}
if spec
.file_stem()
.map(|s| s.to_string_lossy().eq("spec_config"))
.unwrap_or_default()
{
continue;
}
let relative_path = spec.strip_prefix(folder).unwrap();
spec_folder.insert(relative_path);
let def = Definition::load_from_yaml(&spec)?;
definitions.insert(relative_path.to_owned(), def);
}
let context = Self {
definitions,
folder_tree: spec_folder,
root_folder: folder.clone(),
style,
};
let mut violations = context.validate_style();
violations.extend(context.validate_examples());
if !violations.is_empty() {
for violation in violations {
println!("{violation}");
}
anyhow::bail!("validation failed");
}
Ok(context)
}
pub fn root_folder(&self) -> &PathBuf {
&self.root_folder
}
pub fn folder_tree(&self) -> &FolderTree {
&self.folder_tree
}
pub fn get_definition(&self, path: impl AsRef<Path>) -> anyhow::Result<&Definition> {
let path = self.to_relative_path(path.as_ref());
Ok(self.definitions.get(&path).unwrap())
}
pub fn get_model_def_for_reference(
&self,
type_ref: &TypeReference,
spec_path: &Path,
) -> anyhow::Result<&ModelDef> {
match &type_ref.namespace {
Some(namespace) => {
let namespace_spec = self.get_include_path(namespace, spec_path)?;
let def = self.get_definition(namespace_spec)?;
def.get_model(&type_ref.target)
}
None => {
let def = self.get_definition(spec_path)?;
def.get_model(&type_ref.target)
}
}
.ok_or_else(|| anyhow!("model {:?} not find", type_ref))
}
pub fn iter_specs(&self) -> impl Iterator<Item = (&PathBuf, &Definition)> {
self.definitions.iter()
}
pub fn get_include_path(&self, namespace: &str, spec_path: &Path) -> anyhow::Result<PathBuf> {
let def = self.get_definition(spec_path)?;
let include = def
.get_include(namespace)
.ok_or_else(|| anyhow::anyhow!("{} not found", namespace))?;
let relative_path = &include.path;
let included_def_path = spec_path.parent().unwrap().join(relative_path);
Ok(self.to_relative_path(&included_def_path))
}
pub fn load_include_def(
&self,
namespace: &str,
spec_path: &Path,
) -> anyhow::Result<&Definition> {
let path = self.get_include_path(namespace, spec_path)?;
self.get_definition(path)
}
fn to_relative_path(&self, path: &Path) -> PathBuf {
use path_absolutize::*;
let path = path.absolutize_virtually(&self.root_folder).unwrap();
pathdiff::diff_paths(path, &self.root_folder).unwrap()
}
fn validate_style(&self) -> Vec<String> {
let mut violations = vec![];
let Some(style) = self.style.as_ref() else {
return violations;
};
for (spec, def) in self.definitions.iter() {
if style.is_excluded(spec) {
continue;
}
for model in def.models.iter() {
let model_violations = style.validate_model(model);
if model_violations.is_empty() {
continue;
}
for model_violation in model_violations {
violations.push(format!(
"spec:{:?} model:{} {}",
spec, model.name, model_violation
));
}
}
}
violations
}
fn validate_examples(&self) -> Vec<String> {
let mut violations = vec![];
for (spec, def) in self.definitions.iter() {
for model in def.models.iter() {
violations.extend(
self.validate_example_for_model(model, spec)
.into_iter()
.map(|v| format!("{spec:?} {} {v}", model.name)),
);
}
}
violations
}
fn validate_example_for_model(&self, model: &ModelDef, spec: &PathBuf) -> Vec<String> {
let mut violations = vec![];
for example in model.examples.iter() {
if example.format.ne("json") {
continue;
}
match serde_json::from_str::<serde_json::Value>(&example.value) {
Err(e) => {
violations.push(format!("{} invalid json: {e:?}", model.name));
}
Ok(value) => {
violations.extend(self.validate_example_for_model_type(
&model.type_,
&value,
spec,
));
}
}
}
violations
}
fn validate_example_for_model_type(
&self,
model_type: &ModelType,
value: &serde_json::Value,
spec: &PathBuf,
) -> Vec<String> {
match &model_type {
crate::ModelType::Enum { .. } => {
}
crate::ModelType::Struct(st_) => {
let mut violations = vec![];
for field in &st_.fields {
violations.extend(
self.validate_value_for_type(
&value.get(&field.name).cloned().unwrap_or_default(),
&field.type_,
field.required,
spec,
)
.into_iter()
.map(|v| format!("field:{} {v}", field.name)),
);
}
return violations;
}
crate::ModelType::Virtual(_) => {
}
crate::ModelType::NewType { inner_type } => {
return self.validate_value_for_type(&value, &inner_type.0, true, spec)
}
crate::ModelType::Const { .. } => {
}
}
vec![]
}
pub fn validate_value_for_type(
&self,
value: &serde_json::Value,
ty_: &Type,
required: bool,
spec: &PathBuf,
) -> Vec<String> {
if !required && value.is_null() {
return vec![];
}
match ty_ {
Type::Bool => {
if !value.is_boolean() {
return vec![format!("expect bool, got {:?}", value)];
}
}
Type::I8 => {
if !value.is_i64() || value.as_i64().unwrap() > i8::MAX as i64 {
return vec![format!("expect i8, got {:?}", value)];
}
}
Type::I16 => {
if !value.is_i64() || value.as_i64().unwrap() > i16::MAX as i64 {
return vec![format!("expect i16, got {:?}", value)];
}
}
Type::I32 => {
if !value.is_i64() || value.as_i64().unwrap() > i32::MAX as i64 {
return vec![format!("expect i32, got {:?}", value)];
}
}
Type::I64 => {
if !value.is_i64() {
return vec![format!("expect i64, got {:?}", value)];
}
}
Type::F64 => {
if !value.is_f64() {
return vec![format!("expect f64, got {:?}", value)];
}
}
Type::Decimal => {
if !value.is_string() {
return vec![format!("expect decimal in string, got {:?}", value)];
}
}
Type::BigInt => {
if !value.is_string() {
return vec![format!("expect bigint in string, got {:?}", value)];
}
}
Type::Bytes => {
if !value.is_string() {
return vec![format!("expect bytes in string, got {:?}", value)];
}
}
Type::String => {
if !value.is_string() {
return vec![format!("expect string, got {:?}", value)];
}
}
Type::List { item_type } => {
if !value.is_array() {
return vec![format!("expect array, got {:?}", value)];
}
let mut violations = vec![];
for item in value.as_array().unwrap() {
violations.extend(self.validate_value_for_type(item, &item_type, true, spec));
}
return violations;
}
Type::Map { value_type } => {
if !value.is_object() {
return vec![format!("expect object, got {:?}", value)];
}
let mut violations = vec![];
for (_key, item) in value.as_object().unwrap() {
violations.extend(self.validate_value_for_type(item, &value_type, true, spec));
}
return violations;
}
Type::Reference(type_ref) => {
let model_def = self.get_model_def_for_reference(type_ref, spec).unwrap();
return self.validate_example_for_model_type(&model_def.type_, value, spec);
}
Type::Json => {
}
}
vec![]
}
}
impl Context {
fn load_spec_config(
spec_folder: &PathBuf,
) -> anyhow::Result<Option<serde_json::Map<String, serde_json::Value>>> {
let config_file = spec_folder.join("spec_config.yaml");
if !config_file.exists() {
return Ok(None);
}
let config_content = std::fs::read_to_string(config_file)
.map_err(|_| anyhow::anyhow!("not able to read spec_config.yaml from folder"))?;
let config_value =
serde_yaml::from_str::<serde_json::Map<String, serde_json::Value>>(&config_content)?;
Ok(Some(config_value))
}
}