1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ConfigSchema {
11 pub plugins: HashMap<String, PluginSchema>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct PluginSchema {
20 pub prefix: String,
22 pub properties: HashMap<String, PropertySchema>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct PropertySchema {
31 pub name: String,
33 pub type_info: TypeInfo,
35 pub description: String,
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub default: Option<Value>,
40 #[serde(default)]
42 pub required: bool,
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub deprecated: Option<String>,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub example: Option<String>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53#[serde(tag = "type", rename_all = "lowercase")]
54pub enum TypeInfo {
55 String {
57 #[serde(skip_serializing_if = "Option::is_none")]
59 enum_values: Option<Vec<String>>,
60 #[serde(skip_serializing_if = "Option::is_none")]
62 min_length: Option<usize>,
63 #[serde(skip_serializing_if = "Option::is_none")]
65 max_length: Option<usize>,
66 },
67 Integer {
69 #[serde(skip_serializing_if = "Option::is_none")]
71 min: Option<i64>,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 max: Option<i64>,
75 },
76 Float {
78 #[serde(skip_serializing_if = "Option::is_none")]
80 min: Option<f64>,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 max: Option<f64>,
84 },
85 Boolean,
87 Array {
89 item_type: Box<TypeInfo>,
91 },
92 Object {
94 properties: HashMap<String, PropertySchema>,
96 },
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
101#[serde(untagged)]
102pub enum Value {
103 String(String),
105 Integer(i64),
107 Float(f64),
109 Boolean(bool),
111 Array(Vec<Value>),
113 Table(HashMap<String, Value>),
115}
116
117#[derive(Clone)]
121pub struct SchemaProvider {
122 schema: ConfigSchema,
124 cache: dashmap::DashMap<String, PluginSchema>,
126}
127
128impl SchemaProvider {
129 const SCHEMA_URL: &'static str = "https://spring-rs.github.io/config-schema.json";
131
132 pub fn new() -> Self {
134 Self {
135 schema: ConfigSchema {
136 plugins: HashMap::new(),
137 },
138 cache: dashmap::DashMap::new(),
139 }
140 }
141
142 pub async fn load() -> anyhow::Result<Self> {
146 match Self::load_from_url(Self::SCHEMA_URL).await {
148 Ok(schema) => {
149 tracing::info!("Successfully loaded schema from {}", Self::SCHEMA_URL);
150 Ok(Self {
151 schema,
152 cache: dashmap::DashMap::new(),
153 })
154 }
155 Err(e) => {
156 tracing::warn!("Failed to load schema from URL: {}, using fallback", e);
157 Ok(Self::with_fallback_schema())
159 }
160 }
161 }
162
163 async fn load_from_url(url: &str) -> anyhow::Result<ConfigSchema> {
165 let response = reqwest::get(url).await?;
166 let schema = response.json::<ConfigSchema>().await?;
167 Ok(schema)
168 }
169
170 fn with_fallback_schema() -> Self {
172 let fallback_schema = Self::create_fallback_schema();
173 Self {
174 schema: fallback_schema,
175 cache: dashmap::DashMap::new(),
176 }
177 }
178
179 fn create_fallback_schema() -> ConfigSchema {
183 let mut plugins = HashMap::new();
184
185 let mut web_properties = HashMap::new();
187 web_properties.insert(
188 "host".to_string(),
189 PropertySchema {
190 name: "host".to_string(),
191 type_info: TypeInfo::String {
192 enum_values: None,
193 min_length: None,
194 max_length: None,
195 },
196 description: "Web server host address".to_string(),
197 default: Some(Value::String("0.0.0.0".to_string())),
198 required: false,
199 deprecated: None,
200 example: Some("host = \"0.0.0.0\"".to_string()),
201 },
202 );
203 web_properties.insert(
204 "port".to_string(),
205 PropertySchema {
206 name: "port".to_string(),
207 type_info: TypeInfo::Integer {
208 min: Some(1),
209 max: Some(65535),
210 },
211 description: "Web server port".to_string(),
212 default: Some(Value::Integer(8080)),
213 required: false,
214 deprecated: None,
215 example: Some("port = 8080".to_string()),
216 },
217 );
218
219 plugins.insert(
220 "web".to_string(),
221 PluginSchema {
222 prefix: "web".to_string(),
223 properties: web_properties,
224 },
225 );
226
227 let mut redis_properties = HashMap::new();
229 redis_properties.insert(
230 "url".to_string(),
231 PropertySchema {
232 name: "url".to_string(),
233 type_info: TypeInfo::String {
234 enum_values: None,
235 min_length: None,
236 max_length: None,
237 },
238 description: "Redis connection URL".to_string(),
239 default: Some(Value::String("redis://localhost:6379".to_string())),
240 required: false,
241 deprecated: None,
242 example: Some("url = \"redis://localhost:6379\"".to_string()),
243 },
244 );
245
246 plugins.insert(
247 "redis".to_string(),
248 PluginSchema {
249 prefix: "redis".to_string(),
250 properties: redis_properties,
251 },
252 );
253
254 ConfigSchema { plugins }
255 }
256
257 pub fn get_plugin_schema(&self, prefix: &str) -> Option<PluginSchema> {
261 if let Some(cached) = self.cache.get(prefix) {
263 return Some(cached.clone());
264 }
265
266 if let Some(schema) = self.schema.plugins.get(prefix) {
268 let cloned = schema.clone();
269 self.cache.insert(prefix.to_string(), cloned.clone());
270 Some(cloned)
271 } else {
272 None
273 }
274 }
275
276 pub fn get_property_schema(&self, prefix: &str, property: &str) -> Option<PropertySchema> {
280 let plugin_schema = self.get_plugin_schema(prefix)?;
281 plugin_schema.properties.get(property).cloned()
282 }
283
284 pub fn get_all_prefixes(&self) -> Vec<String> {
288 self.schema.plugins.keys().cloned().collect()
289 }
290}
291
292impl Default for SchemaProvider {
293 fn default() -> Self {
294 Self::with_fallback_schema()
295 }
296}
297
298impl SchemaProvider {
299 pub fn from_schema(schema: ConfigSchema) -> Self {
303 Self {
304 schema,
305 cache: dashmap::DashMap::new(),
306 }
307 }
308}