use crate::schema::RenderResult;
use indexmap::IndexMap;
use miette::miette;
use schematic_types::*;
use std::collections::{HashMap, VecDeque};
use std::mem;
pub struct TemplateOptions {
pub comments: bool,
pub comment_fields: Vec<String>,
pub comment_prefix: String,
pub custom_values: HashMap<String, Schema>,
pub expand_fields: Vec<String>,
pub footer: String,
pub header: String,
pub hide_fields: Vec<String>,
pub indent_char: String,
pub newline_between_fields: bool,
pub only_fields: Vec<String>,
}
impl Default for TemplateOptions {
fn default() -> Self {
Self {
comments: true,
comment_fields: vec![],
comment_prefix: "# ".into(),
custom_values: HashMap::new(),
expand_fields: vec![],
footer: String::new(),
header: String::new(),
hide_fields: vec![],
indent_char: " ".into(),
newline_between_fields: true,
only_fields: vec![],
}
}
}
pub fn lit_to_string(lit: &LiteralValue) -> String {
match lit {
LiteralValue::Bool(inner) => inner.to_string(),
LiteralValue::F32(inner) => inner.to_string(),
LiteralValue::F64(inner) => inner.to_string(),
LiteralValue::Int(inner) => inner.to_string(),
LiteralValue::UInt(inner) => inner.to_string(),
LiteralValue::String(inner) => format!("\"{inner}\""),
}
}
pub fn is_nested_type(schema: &SchemaType) -> bool {
match schema {
SchemaType::Struct(sct) => !sct.fields.is_empty(),
SchemaType::Union(uni) => {
if uni.has_null() && uni.variants_types.len() == 2 {
uni.variants_types
.iter()
.find(|v| !v.is_null())
.is_some_and(|v| is_nested_type(v))
} else {
false
}
}
_ => false,
}
}
pub struct TemplateContext {
pub depth: usize,
pub options: TemplateOptions,
stack: VecDeque<String>,
}
impl TemplateContext {
pub fn new(options: TemplateOptions) -> Self {
Self {
depth: 0,
options,
stack: VecDeque::new(),
}
}
pub fn indent(&self) -> String {
if self.depth == 0 {
String::new()
} else {
self.options.indent_char.repeat(self.depth)
}
}
pub fn gap(&self) -> &str {
if self.options.newline_between_fields {
"\n\n"
} else {
"\n"
}
}
pub fn create_field_comment(&self, field: &SchemaField) -> String {
if !self.options.comments {
return String::new();
}
let mut lines = vec![];
let indent = self.indent();
let prefix = self.get_comment_prefix();
let mut push = |line: String| {
lines.push(format!("{indent}{prefix}{line}"));
};
if let Some(comment) = &field.comment {
comment
.trim()
.split('\n')
.for_each(|c| push(c.trim().to_owned()));
}
if let Some(deprecated) = &field.deprecated {
push(if deprecated.is_empty() {
"@deprecated".into()
} else {
format!("@deprecated {deprecated}")
});
}
if let Some(env_var) = &field.env_var
&& !env_var.is_empty()
{
push(format!("@env {env_var}"));
}
if let SchemaType::Enum(enu) = &field.schema.ty
&& let Ok(enum_values) = render_enum_values(enu)
&& !enum_values.is_empty()
{
push(format!("@values {enum_values}"));
}
if lines.is_empty() {
return String::new();
}
let mut out = lines.join("\n");
out.push('\n');
out
}
pub fn create_field(&self, field: &SchemaField, property: String) -> String {
let key = self.get_stack_key();
format!(
"{}{}{}{property}",
self.create_field_comment(field),
self.indent(),
if self.options.comment_fields.contains(&key) {
self.get_comment_prefix()
} else {
""
},
)
}
pub fn get_comment_prefix(&self) -> &str {
&self.options.comment_prefix
}
pub fn get_stack_key(&self) -> String {
let mut key = String::new();
let last_index = self.stack.len() - 1;
for (index, item) in self.stack.iter().enumerate() {
key.push_str(item);
if index != last_index {
key.push('.');
}
}
key
}
pub fn get_stack_value(&self) -> Option<Schema> {
let key = self.get_stack_key();
self.options.custom_values.get(&key).cloned()
}
pub fn is_expanded(&self, key: &String) -> bool {
self.options.expand_fields.contains(key)
}
pub fn is_hidden(&self, field: &SchemaField) -> bool {
let key = self.get_stack_key();
field.hidden
|| self.options.hide_fields.contains(&key)
|| !self.options.only_fields.is_empty() && !self.options.only_fields.contains(&key)
}
pub fn push_stack(&mut self, name: &str) {
self.stack.push_back(name.to_owned());
}
pub fn pop_stack(&mut self) {
self.stack.pop_back();
}
pub fn resolve_schema(&self, initial: &Schema, schemas: &IndexMap<String, Schema>) -> Schema {
if let SchemaType::Reference(name) = &initial.ty {
if let Some(schema) = schemas.get(name) {
return schema.to_owned();
}
}
initial.to_owned()
}
pub fn validate_schema_variant<'a>(
&self,
custom: Option<&'a Schema>,
fallback: &'a Schema,
) -> &'a Schema {
if let Some(custom) = custom {
if mem::discriminant(&custom.ty) == mem::discriminant(&fallback.ty) {
return custom;
} else {
panic!(
"Received an invalid custom value for `{}`, mismatched schema types.\n\nExpected: {:#?}\n\nReceived: {:#?}",
self.get_stack_key(),
fallback,
custom
);
}
}
fallback
}
}
pub fn render_array(_array: &ArrayType) -> RenderResult {
Ok("[]".into())
}
pub fn render_boolean(boolean: &BooleanType) -> RenderResult {
if let Some(default) = &boolean.default {
return Ok(lit_to_string(default));
}
Ok("false".into())
}
pub fn render_enum(enu: &EnumType) -> RenderResult {
let index = enu.default_index.unwrap_or(0);
if let Some(value) = enu.values.get(index) {
return Ok(lit_to_string(value));
}
render_null()
}
pub fn render_enum_values(enu: &EnumType) -> RenderResult {
let values: Vec<String> = match &enu.variants {
Some(variants) => variants
.iter()
.filter_map(|(_, variant)| {
if variant.hidden {
None
} else if let SchemaType::Literal(lit) = &variant.schema.ty {
Some(lit_to_string(&lit.value))
} else {
None
}
})
.collect(),
None => enu.values.iter().map(lit_to_string).collect(),
};
Ok(values.join(" | "))
}
pub fn render_float(float: &FloatType) -> RenderResult {
if let Some(default) = &float.default {
return Ok(lit_to_string(default));
}
Ok("0.0".into())
}
pub fn render_integer(integer: &IntegerType) -> RenderResult {
if let Some(default) = &integer.default {
return Ok(lit_to_string(default));
}
Ok("0".into())
}
pub fn render_literal(literal: &LiteralType) -> RenderResult {
Ok(lit_to_string(&literal.value))
}
pub fn render_null() -> RenderResult {
Ok("null".into())
}
pub fn render_object(_object: &ObjectType) -> RenderResult {
Ok("{}".into())
}
pub fn render_reference(reference: &str) -> RenderResult {
Ok(reference.into())
}
pub const EMPTY_STRING: &str = "\"\"";
pub fn render_string(string: &StringType) -> RenderResult {
if let Some(default) = &string.default {
return Ok(lit_to_string(default));
}
Ok(EMPTY_STRING.into())
}
pub fn render_tuple(
tuple: &TupleType,
mut render: impl FnMut(&Schema) -> RenderResult,
) -> RenderResult {
let mut items = vec![];
for item in &tuple.items_types {
items.push(render(item)?);
}
Ok(format!("[{}]", items.join(", ")))
}
pub fn render_union(
uni: &UnionType,
mut render: impl FnMut(&Schema) -> RenderResult,
) -> RenderResult {
if let Some(index) = &uni.default_index {
if let Some(variant) = uni.variants_types.get(*index) {
return render(variant);
}
}
if uni.has_null() {
if let Some(variant) = uni.variants_types.iter().find(|v| !v.is_null()) {
return render(variant);
}
}
if let Some(variant) = uni.variants_types.first() {
return render(variant);
}
render_null()
}
pub fn render_unknown() -> RenderResult {
render_null()
}
pub fn validate_root(schemas: &IndexMap<String, Schema>) -> miette::Result<Schema> {
let Some(schema) = schemas.values().last() else {
return Err(miette!(
"At least 1 schema is required to generate a template."
));
};
if !schema.is_struct() {
return Err(miette!("The last registered schema must be a struct type."));
};
Ok(schema.to_owned())
}