use serde_json::Value;
use std::collections::BTreeMap;
use crate::errors::ValidationError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FieldType {
String,
Number,
Boolean,
Array(Box<FieldType>),
Enum(Vec<String>),
Count,
Value,
}
impl FieldType {
pub fn display_name(&self) -> String {
match self {
FieldType::String => "string".to_string(),
FieldType::Number => "number".to_string(),
FieldType::Boolean => "boolean".to_string(),
FieldType::Array(_) => "array".to_string(),
FieldType::Enum(values) => values.join("|"),
FieldType::Count => "count".to_string(),
FieldType::Value => "value".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct FieldMeta {
pub name: &'static str,
pub cli_name: String,
pub description: Option<&'static str>,
pub field_type: FieldType,
pub required: bool,
pub default: Option<Value>,
pub alias: Option<char>,
pub deprecated: bool,
pub env_name: Option<&'static str>,
}
pub trait IncurSchema: Sized {
fn fields() -> Vec<FieldMeta>;
fn from_raw(raw: &BTreeMap<String, Value>) -> std::result::Result<Self, ValidationError>;
fn field_names() -> Vec<&'static str> {
Self::fields().iter().map(|f| f.name).collect()
}
}
impl IncurSchema for () {
fn fields() -> Vec<FieldMeta> {
Vec::new()
}
fn from_raw(_raw: &BTreeMap<String, Value>) -> std::result::Result<Self, ValidationError> {
Ok(())
}
}
pub fn to_json_schema(fields: &[FieldMeta]) -> Value {
let mut properties = serde_json::Map::new();
let mut required: Vec<Value> = Vec::new();
for field in fields {
let mut prop = serde_json::Map::new();
match &field.field_type {
FieldType::String => {
prop.insert("type".to_string(), Value::from("string"));
}
FieldType::Number | FieldType::Count => {
prop.insert("type".to_string(), Value::from("number"));
}
FieldType::Boolean => {
prop.insert("type".to_string(), Value::from("boolean"));
}
FieldType::Array(inner) => {
prop.insert("type".to_string(), Value::from("array"));
prop.insert("items".to_string(), to_json_schema_type(inner));
}
FieldType::Enum(values) => {
prop.insert("type".to_string(), Value::from("string"));
prop.insert(
"enum".to_string(),
Value::Array(values.iter().map(|v| Value::from(v.as_str())).collect()),
);
}
FieldType::Value => {}
}
if let Some(desc) = field.description {
prop.insert("description".to_string(), Value::from(desc));
}
if let Some(ref default) = field.default {
prop.insert("default".to_string(), default.clone());
}
properties.insert(field.cli_name.clone(), Value::Object(prop));
if field.required {
required.push(Value::from(field.cli_name.clone()));
}
}
let mut schema = serde_json::Map::new();
schema.insert("type".to_string(), Value::from("object"));
schema.insert("properties".to_string(), Value::Object(properties));
if !required.is_empty() {
schema.insert("required".to_string(), Value::Array(required));
}
Value::Object(schema)
}
fn to_json_schema_type(ft: &FieldType) -> Value {
match ft {
FieldType::String => serde_json::json!({ "type": "string" }),
FieldType::Number | FieldType::Count => serde_json::json!({ "type": "number" }),
FieldType::Boolean => serde_json::json!({ "type": "boolean" }),
FieldType::Array(inner) => {
serde_json::json!({ "type": "array", "items": to_json_schema_type(inner) })
}
FieldType::Enum(values) => serde_json::json!({ "type": "string", "enum": values }),
FieldType::Value => serde_json::json!({}),
}
}
pub fn to_kebab(name: &str) -> String {
let mut result = String::with_capacity(name.len());
for (i, ch) in name.chars().enumerate() {
if ch == '_' {
result.push('-');
} else if ch.is_uppercase() {
if i > 0 {
result.push('-');
}
result.push(ch.to_lowercase().next().unwrap_or(ch));
} else {
result.push(ch);
}
}
result
}
pub fn to_snake(name: &str) -> String {
name.replace('-', "_")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_to_kebab() {
assert_eq!(to_kebab("filter_output"), "filter-output");
assert_eq!(to_kebab("tokenLimit"), "token-limit");
assert_eq!(to_kebab("simple"), "simple");
}
#[test]
fn test_to_snake() {
assert_eq!(to_snake("filter-output"), "filter_output");
assert_eq!(to_snake("simple"), "simple");
}
}