use crate::{Error, Layout, SchemaVersion, primitives};
use serde_json::Value;
use specta::{Format, Types, datatype::NamedDataType};
use std::{borrow::Cow, collections::BTreeMap, path::Path};
#[derive(Debug, Clone)]
pub struct JsonSchema {
pub schema_version: SchemaVersion,
pub layout: Layout,
pub title: Option<String>,
pub description: Option<String>,
}
impl Default for JsonSchema {
fn default() -> Self {
Self {
schema_version: SchemaVersion::default(),
layout: Layout::default(),
title: None,
description: None,
}
}
}
impl JsonSchema {
pub fn new() -> Self {
Self::default()
}
pub fn schema_version(mut self, version: SchemaVersion) -> Self {
self.schema_version = version;
self
}
pub fn layout(mut self, layout: Layout) -> Self {
self.layout = layout;
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn export(&self, types: &Types, format: impl Format) -> Result<String, Error> {
let value = self.export_as_value(types, format)?;
Ok(serde_json::to_string_pretty(&value)?)
}
pub fn export_as_value(&self, types: &Types, format: impl Format) -> Result<Value, Error> {
let exporter = self.clone();
let formatted_types = format_types(&exporter, types, &format)?;
let types = formatted_types.as_ref();
match exporter.layout {
Layout::SingleFile => exporter.export_single_file(types),
Layout::Files => Err(Error::ConversionError(
"Use export_to() for Files layout".to_string(),
)),
}
}
pub fn export_to(
&self,
path: impl AsRef<Path>,
types: &Types,
format: impl Format,
) -> Result<(), Error> {
let exporter = self.clone();
let formatted_types = format_types(&exporter, types, &format)?;
let types = formatted_types.as_ref();
let path = path.as_ref();
match exporter.layout {
Layout::SingleFile => {
let json = exporter.export_single_file(types)?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, serde_json::to_string_pretty(&json)?)?;
Ok(())
}
Layout::Files => exporter.export_files(path, types),
}
}
fn export_single_file(&self, types: &Types) -> Result<Value, Error> {
let mut definitions = BTreeMap::new();
for ndt in types.into_sorted_iter() {
let schema = primitives::export(self, types, &ndt)?;
let name = ndt.name.to_string();
definitions.insert(name, schema);
}
let defs_key = self.schema_version.definitions_key();
let mut root = serde_json::json!({
"$schema": self.schema_version.uri(),
defs_key: definitions,
});
if let Some(title) = &self.title {
root.as_object_mut()
.unwrap()
.insert("title".to_string(), Value::String(title.clone()));
}
if let Some(description) = &self.description {
root.as_object_mut().unwrap().insert(
"description".to_string(),
Value::String(description.clone()),
);
}
Ok(root)
}
fn export_files(&self, base_path: &Path, types: &Types) -> Result<(), Error> {
std::fs::create_dir_all(base_path)?;
let mut by_module: BTreeMap<String, Vec<NamedDataType>> = BTreeMap::new();
for ndt in types.into_sorted_iter() {
let module = ndt.module_path.to_string().replace("::", "/");
by_module.entry(module).or_default().push(ndt.clone());
}
for (module, ndts) in by_module {
let module_dir = if module.is_empty() {
base_path.to_path_buf()
} else {
base_path.join(&module)
};
std::fs::create_dir_all(&module_dir)?;
for ndt in &ndts {
let schema = primitives::export(self, types, ndt)?;
let filename = format!("{}.schema.json", ndt.name);
let file_path = module_dir.join(filename);
let mut root = serde_json::json!({
"$schema": self.schema_version.uri(),
});
if let Some(obj) = schema.as_object() {
for (k, v) in obj {
root.as_object_mut().unwrap().insert(k.clone(), v.clone());
}
}
std::fs::write(file_path, serde_json::to_string_pretty(&root)?)?;
}
}
Ok(())
}
}
fn format_types<'a>(
exporter: &JsonSchema,
types: &'a Types,
format: &dyn Format,
) -> Result<Cow<'a, Types>, Error> {
let mapped_types = format
.map_types(types)
.map_err(|err| Error::format("type graph formatter failed", err))?;
Ok(Cow::Owned(mapped_types.into_owned()))
}