use crate::shapes::OneOrMany;
use crate::{config_enum, config_struct, is_false};
use moon_common::Id;
use rustc_hash::FxHashMap;
use schematic::{Config, ValidateError, validate};
use serde_json::Value;
macro_rules! var_setting {
($name:ident, $ty:ty) => {
config_struct!(
#[derive(Config)]
pub struct $name {
/// The default value of the variable if none was provided.
#[setting(alias = "defaultValue")]
pub default: $ty,
#[serde(skip_serializing_if = "is_false")]
pub internal: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub order: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
}
);
};
}
var_setting!(TemplateVariableArraySetting, Vec<Value>);
var_setting!(TemplateVariableBoolSetting, bool);
var_setting!(TemplateVariableNumberSetting, isize);
var_setting!(TemplateVariableObjectSetting, FxHashMap<String, Value>);
var_setting!(TemplateVariableStringSetting, String);
config_struct!(
#[derive(Config)]
pub struct TemplateVariableEnumValueConfig {
pub label: String,
pub value: String,
}
);
config_enum!(
#[derive(Config)]
#[serde(untagged)]
pub enum TemplateVariableEnumValue {
String(String),
#[setting(nested)]
Object(TemplateVariableEnumValueConfig),
}
);
config_enum!(
#[derive(Config)]
#[serde(untagged)]
pub enum TemplateVariableEnumDefault {
String(String),
#[setting(default)]
List(Vec<String>),
}
);
impl TemplateVariableEnumDefault {
pub fn to_vec(&self) -> Vec<&String> {
match self {
Self::String(value) => vec![value],
Self::List(list) => list.iter().collect(),
}
}
}
fn validate_enum_default<C>(
default_value: &PartialTemplateVariableEnumDefault,
partial: &PartialTemplateVariableEnumSetting,
_context: &C,
_finalize: bool,
) -> Result<(), ValidateError> {
if let Some(values) = &partial.values {
if let PartialTemplateVariableEnumDefault::List(list) = default_value {
if !partial.multiple.is_some_and(|m| m) && !list.is_empty() {
return Err(ValidateError::new(
"multiple default values is not allowed unless `multiple` is enabled",
));
}
}
let values = values
.iter()
.flat_map(|v| match v {
PartialTemplateVariableEnumValue::String(value) => Some(value),
PartialTemplateVariableEnumValue::Object(cfg) => cfg.value.as_ref(),
})
.collect::<Vec<_>>();
let matches = match default_value {
PartialTemplateVariableEnumDefault::String(inner) => values.contains(&inner),
PartialTemplateVariableEnumDefault::List(list) => {
list.iter().all(|v| values.contains(&v))
}
};
if !matches {
return Err(ValidateError::new(
"invalid default value, must be a value configured in `values`",
));
}
}
Ok(())
}
config_struct!(
#[derive(Config)]
pub struct TemplateVariableEnumSetting {
#[setting(alias = "defaultValue", nested, validate = validate_enum_default)]
pub default: TemplateVariableEnumDefault,
#[serde(default, skip_serializing_if = "is_false")]
pub internal: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub multiple: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub order: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt: Option<String>,
#[setting(nested)]
pub values: Vec<TemplateVariableEnumValue>,
}
);
impl TemplateVariableEnumSetting {
pub fn get_labels(&self) -> Vec<&String> {
self.values
.iter()
.map(|v| match v {
TemplateVariableEnumValue::String(value) => value,
TemplateVariableEnumValue::Object(cfg) => &cfg.label,
})
.collect()
}
pub fn get_values(&self) -> Vec<&String> {
self.values
.iter()
.map(|v| match v {
TemplateVariableEnumValue::String(value) => value,
TemplateVariableEnumValue::Object(cfg) => &cfg.value,
})
.collect()
}
pub fn is_multiple(&self) -> bool {
self.multiple.is_some_and(|v| v)
}
}
config_enum!(
#[derive(Config)]
#[serde(tag = "type")]
pub enum TemplateVariable {
#[setting(nested)]
Array(TemplateVariableArraySetting),
#[setting(nested)]
Boolean(TemplateVariableBoolSetting),
#[setting(nested)]
Enum(TemplateVariableEnumSetting),
#[setting(nested)]
Number(TemplateVariableNumberSetting),
#[setting(nested)]
Object(TemplateVariableObjectSetting),
#[setting(nested)]
String(TemplateVariableStringSetting),
}
);
impl TemplateVariable {
pub fn get_order(&self) -> usize {
let order = match self {
Self::Array(cfg) => cfg.order.as_ref(),
Self::Boolean(cfg) => cfg.order.as_ref(),
Self::Enum(cfg) => cfg.order.as_ref(),
Self::Number(cfg) => cfg.order.as_ref(),
Self::Object(cfg) => cfg.order.as_ref(),
Self::String(cfg) => cfg.order.as_ref(),
};
order.copied().unwrap_or(100)
}
pub fn get_prompt(&self) -> Option<&String> {
match self {
Self::Array(cfg) => cfg.prompt.as_ref(),
Self::Boolean(cfg) => cfg.prompt.as_ref(),
Self::Enum(cfg) => cfg.prompt.as_ref(),
Self::Number(cfg) => cfg.prompt.as_ref(),
Self::Object(cfg) => cfg.prompt.as_ref(),
Self::String(cfg) => cfg.prompt.as_ref(),
}
}
pub fn is_internal(&self) -> bool {
match self {
Self::Array(cfg) => cfg.internal,
Self::Boolean(cfg) => cfg.internal,
Self::Enum(cfg) => cfg.internal,
Self::Number(cfg) => cfg.internal,
Self::Object(cfg) => cfg.internal,
Self::String(cfg) => cfg.internal,
}
}
pub fn is_multiple(&self) -> bool {
match self {
Self::Enum(cfg) => cfg.is_multiple(),
_ => false,
}
}
pub fn is_required(&self) -> bool {
match self {
Self::Array(cfg) => cfg.required,
Self::Boolean(cfg) => cfg.required,
Self::Number(cfg) => cfg.required,
Self::Object(cfg) => cfg.required,
Self::String(cfg) => cfg.required,
_ => None,
}
.is_some_and(|v| v)
}
}
config_struct!(
#[derive(Config)]
pub struct TemplateConfig {
#[setting(rename = "$schema")]
pub schema: String,
#[setting(validate = validate::not_empty)]
pub description: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub destination: Option<String>,
#[serde(default, skip_serializing_if = "OneOrMany::is_empty")]
pub extends: OneOrMany<Id>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<Id>,
#[setting(validate = validate::not_empty)]
pub title: String,
#[setting(nested)]
#[serde(default, skip_serializing_if = "FxHashMap::is_empty")]
pub variables: FxHashMap<String, TemplateVariable>,
}
);