greentic_component/scaffold/
config_schema.rs1#![cfg(feature = "cli")]
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5use serde_json::{Value as JsonValue, json};
6
7use super::validate::ValidationError;
8
9static CONFIG_FIELD_RE: Lazy<Regex> =
10 Lazy::new(|| Regex::new(r"^[a-z][a-z0-9_]*$").expect("valid config field regex"));
11
12#[derive(Debug, Clone, PartialEq, Eq, Default)]
13pub struct ConfigSchemaInput {
14 pub fields: Vec<ConfigSchemaField>,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct ConfigSchemaField {
19 pub name: String,
20 pub field_type: ConfigSchemaFieldType,
21 pub required: bool,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum ConfigSchemaFieldType {
26 String,
27 Bool,
28 Integer,
29 Number,
30}
31
32impl ConfigSchemaInput {
33 pub fn manifest_schema(&self) -> JsonValue {
34 let mut properties = serde_json::Map::new();
35 let mut required = Vec::new();
36 for field in &self.fields {
37 properties.insert(field.name.clone(), field.field_type.json_schema());
38 if field.required {
39 required.push(JsonValue::String(field.name.clone()));
40 }
41 }
42
43 json!({
44 "type": "object",
45 "properties": properties,
46 "required": required,
47 "additionalProperties": false
48 })
49 }
50
51 pub fn component_schema_file(&self, component_name: &str) -> JsonValue {
52 let mut schema = self.manifest_schema();
53 if let Some(obj) = schema.as_object_mut() {
54 obj.insert(
55 "$schema".to_string(),
56 JsonValue::String("https://json-schema.org/draft/2020-12/schema".to_string()),
57 );
58 obj.insert(
59 "title".to_string(),
60 JsonValue::String(format!("{component_name} component configuration")),
61 );
62 }
63 schema
64 }
65
66 pub fn rust_schema_ir(&self) -> String {
67 if self.fields.is_empty() {
68 return r#"SchemaIr::Object {
69 properties: BTreeMap::new(),
70 required: Vec::new(),
71 additional: AdditionalProperties::Forbid,
72 }"#
73 .to_string();
74 }
75
76 let properties = self
77 .fields
78 .iter()
79 .map(|field| {
80 format!(
81 "(\"{}\".to_string(), {})",
82 field.name,
83 field.field_type.rust_schema_ir()
84 )
85 })
86 .collect::<Vec<_>>()
87 .join(",\n ");
88 let required = self
89 .fields
90 .iter()
91 .filter(|field| field.required)
92 .map(|field| format!("\"{}\".to_string()", field.name))
93 .collect::<Vec<_>>();
94 let required_expr = if required.is_empty() {
95 "Vec::new()".to_string()
96 } else {
97 format!("vec![{}]", required.join(", "))
98 };
99
100 format!(
101 r#"SchemaIr::Object {{
102 properties: BTreeMap::from([
103 {properties}
104 ]),
105 required: {required_expr},
106 additional: AdditionalProperties::Forbid,
107 }}"#
108 )
109 }
110}
111
112impl ConfigSchemaFieldType {
113 fn json_schema(self) -> JsonValue {
114 match self {
115 Self::String => json!({ "type": "string" }),
116 Self::Bool => json!({ "type": "boolean" }),
117 Self::Integer => json!({ "type": "integer" }),
118 Self::Number => json!({ "type": "number" }),
119 }
120 }
121
122 fn rust_schema_ir(self) -> &'static str {
123 match self {
124 Self::String => {
125 "SchemaIr::String { min_len: Some(0), max_len: None, regex: None, format: None }"
126 }
127 Self::Bool => "SchemaIr::Bool",
128 Self::Integer => "SchemaIr::Int { min: None, max: None }",
129 Self::Number => "SchemaIr::Float { min: None, max: None }",
130 }
131 }
132}
133
134pub fn parse_config_field(value: &str) -> Result<ConfigSchemaField, ValidationError> {
135 let mut parts = value.split(':').map(str::trim);
136 let name = parts.next().unwrap_or_default();
137 let field_type = parts.next().unwrap_or_default();
138 let required = parts.next().unwrap_or("optional");
139 if name.is_empty() || field_type.is_empty() || parts.next().is_some() {
140 return Err(ValidationError::InvalidConfigField(value.to_string()));
141 }
142 if !CONFIG_FIELD_RE.is_match(name) {
143 return Err(ValidationError::InvalidConfigFieldName(name.to_string()));
144 }
145 let field_type = parse_config_field_type(field_type)?;
146 let required = match required {
147 "required" => true,
148 "optional" => false,
149 other => {
150 return Err(ValidationError::InvalidConfigField(
151 value.replace(required, other),
152 ));
153 }
154 };
155 Ok(ConfigSchemaField {
156 name: name.to_string(),
157 field_type,
158 required,
159 })
160}
161
162fn parse_config_field_type(value: &str) -> Result<ConfigSchemaFieldType, ValidationError> {
163 match value {
164 "string" => Ok(ConfigSchemaFieldType::String),
165 "bool" | "boolean" => Ok(ConfigSchemaFieldType::Bool),
166 "int" | "integer" => Ok(ConfigSchemaFieldType::Integer),
167 "number" | "float" => Ok(ConfigSchemaFieldType::Number),
168 other => Err(ValidationError::InvalidConfigFieldType(other.to_string())),
169 }
170}