1use serde::{Deserialize, Serialize};
4use serde_json::json;
5use std::collections::HashMap;
6use std::path::Path;
7
8use crate::scanner::config::{ConfigField, ConfigScanner, ConfigurationStruct};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ConfigSchema {
25 #[serde(rename = "properties")]
28 pub plugins: HashMap<String, serde_json::Value>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct PluginSchema {
36 pub prefix: String,
38 pub properties: HashMap<String, PropertySchema>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct PropertySchema {
47 pub name: String,
49 pub type_info: TypeInfo,
51 pub description: String,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub default: Option<Value>,
56 #[serde(default)]
58 pub required: bool,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub deprecated: Option<String>,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub example: Option<String>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69#[serde(tag = "type", rename_all = "lowercase")]
70pub enum TypeInfo {
71 String {
73 #[serde(skip_serializing_if = "Option::is_none")]
75 enum_values: Option<Vec<String>>,
76 #[serde(skip_serializing_if = "Option::is_none")]
78 min_length: Option<usize>,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 max_length: Option<usize>,
82 },
83 Integer {
85 #[serde(skip_serializing_if = "Option::is_none")]
87 min: Option<i64>,
88 #[serde(skip_serializing_if = "Option::is_none")]
90 max: Option<i64>,
91 },
92 Float {
94 #[serde(skip_serializing_if = "Option::is_none")]
96 min: Option<f64>,
97 #[serde(skip_serializing_if = "Option::is_none")]
99 max: Option<f64>,
100 },
101 Boolean,
103 Array {
105 item_type: Box<TypeInfo>,
107 },
108 Object {
110 properties: HashMap<String, PropertySchema>,
112 },
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
117#[serde(untagged)]
118pub enum Value {
119 String(String),
121 Integer(i64),
123 Float(f64),
125 Boolean(bool),
127 Array(Vec<Value>),
129 Table(HashMap<String, Value>),
131}
132
133#[derive(Clone)]
137pub struct SchemaProvider {
138 schema: ConfigSchema,
140}
141
142impl SchemaProvider {
143 const SCHEMA_URL: &'static str = "https://summer-rs.github.io/config-schema.json";
145
146 pub fn new() -> Self {
148 Self {
149 schema: ConfigSchema {
150 plugins: HashMap::new(),
151 },
152 }
153 }
154
155 pub async fn load() -> anyhow::Result<Self> {
159 match Self::load_from_url(Self::SCHEMA_URL).await {
161 Ok(schema) => {
162 tracing::info!("Successfully loaded schema from {}", Self::SCHEMA_URL);
163 Ok(Self { schema })
164 }
165 Err(e) => {
166 tracing::warn!("Failed to load schema from URL: {}, using fallback", e);
167 Ok(Self::with_fallback_schema())
169 }
170 }
171 }
172
173 pub async fn load_with_workspace(workspace_path: &Path) -> anyhow::Result<Self> {
190 let mut provider = Self::load().await?;
192
193 if let Some(schema_path) = Self::find_schema_in_target(workspace_path) {
195 tracing::info!("Loading schema from target directory: {:?}", schema_path);
196 match Self::load_local_schema_file(&schema_path) {
197 Ok(local_schemas) => {
198 tracing::info!("Loaded {} local schemas from target", local_schemas.len());
199 for (prefix, schema) in local_schemas {
200 provider.schema.plugins.insert(prefix, schema);
201 }
202 return Ok(provider);
203 }
204 Err(e) => {
205 tracing::warn!("Failed to load schema from target: {}", e);
206 }
207 }
208 }
209
210 let local_schema_path = workspace_path.join(".summer-lsp.schema.json");
212 if local_schema_path.exists() {
213 tracing::info!("Loading local schema from: {:?}", local_schema_path);
214 match Self::load_local_schema_file(&local_schema_path) {
215 Ok(local_schemas) => {
216 tracing::info!("Loaded {} local schemas from file", local_schemas.len());
217 for (prefix, schema) in local_schemas {
218 provider.schema.plugins.insert(prefix, schema);
219 }
220 return Ok(provider);
221 }
222 Err(e) => {
223 tracing::warn!("Failed to load local schema file: {}", e);
224 }
225 }
226 }
227
228 tracing::info!("Scanning local configurations in: {:?}", workspace_path);
230 let scanner = ConfigScanner::new();
231 match scanner.scan_configurations(workspace_path) {
232 Ok(configurations) => {
233 tracing::info!("Found {} local configuration structs", configurations.len());
234
235 for config in configurations {
237 let schema_json = Self::configuration_to_schema(&config);
238 provider
239 .schema
240 .plugins
241 .insert(config.prefix.clone(), schema_json);
242 tracing::debug!("Added local configuration: {}", config.prefix);
243 }
244 }
245 Err(e) => {
246 tracing::warn!("Failed to scan local configurations: {}", e);
247 }
248 }
249
250 Ok(provider)
251 }
252
253 fn find_schema_in_target(workspace_path: &Path) -> Option<std::path::PathBuf> {
259 let target_dir = workspace_path.join("target");
260 if !target_dir.exists() {
261 return None;
262 }
263
264 let mut schema_files = Vec::new();
266
267 if let Ok(entries) = std::fs::read_dir(&target_dir) {
268 for entry in entries.flatten() {
269 let path = entry.path();
270 if path.is_dir() {
271 let build_dir = path.join("build");
273 if build_dir.exists() {
274 if let Ok(build_entries) = std::fs::read_dir(&build_dir) {
275 for build_entry in build_entries.flatten() {
276 let out_dir = build_entry.path().join("out");
277 let schema_path = out_dir.join("summer-lsp.schema.json");
278 if schema_path.exists() {
279 if let Ok(metadata) = std::fs::metadata(&schema_path) {
280 if let Ok(modified) = metadata.modified() {
281 schema_files.push((schema_path, modified));
282 }
283 }
284 }
285 }
286 }
287 }
288 }
289 }
290 }
291
292 if schema_files.is_empty() {
293 return None;
294 }
295
296 if schema_files.len() == 1 {
298 return Some(schema_files[0].0.clone());
299 }
300
301 tracing::info!("Found {} schema files, merging...", schema_files.len());
303
304 schema_files.sort_by(|a, b| b.1.cmp(&a.1));
306
307 match Self::merge_schema_files(&schema_files) {
309 Ok(merged_path) => Some(merged_path),
310 Err(e) => {
311 tracing::warn!("Failed to merge schema files: {}, using latest", e);
312 Some(schema_files[0].0.clone())
314 }
315 }
316 }
317
318 fn merge_schema_files(
323 schema_files: &[(std::path::PathBuf, std::time::SystemTime)],
324 ) -> anyhow::Result<std::path::PathBuf> {
325 use std::fs;
326
327 let mut merged_properties = serde_json::Map::new();
328
329 for (path, _) in schema_files {
331 tracing::debug!("Merging schema from: {:?}", path);
332 let content = fs::read_to_string(path)?;
333 let schema: serde_json::Value = serde_json::from_str(&content)?;
334
335 if let Some(properties) = schema.get("properties").and_then(|p| p.as_object()) {
336 for (key, value) in properties {
337 merged_properties.insert(key.clone(), value.clone());
339 }
340 }
341 }
342
343 let merged_schema = serde_json::json!({
345 "type": "object",
346 "properties": merged_properties
347 });
348
349 let temp_dir = std::env::temp_dir();
351 let merged_path = temp_dir.join("summer-lsp-merged.schema.json");
352 fs::write(&merged_path, serde_json::to_string_pretty(&merged_schema)?)?;
353
354 tracing::info!(
355 "Merged {} schema files ({} configs) into: {:?}",
356 schema_files.len(),
357 merged_properties.len(),
358 merged_path
359 );
360
361 Ok(merged_path)
362 }
363
364 fn load_local_schema_file(path: &Path) -> anyhow::Result<HashMap<String, serde_json::Value>> {
366 let content = std::fs::read_to_string(path)?;
367 let schema: serde_json::Value = serde_json::from_str(&content)?;
368
369 let mut schemas = HashMap::new();
370
371 if let Some(properties) = schema.get("properties").and_then(|p| p.as_object()) {
373 for (key, value) in properties {
374 schemas.insert(key.clone(), value.clone());
375 }
376 }
377
378 Ok(schemas)
379 }
380
381 fn configuration_to_schema(config: &ConfigurationStruct) -> serde_json::Value {
383 let mut properties = serde_json::Map::new();
384
385 for field in &config.fields {
386 let field_schema = Self::field_to_schema(field);
387 properties.insert(field.name.clone(), field_schema);
388 }
389
390 json!({
391 "type": "object",
392 "properties": properties,
393 "description": format!("Configuration for {}", config.name)
394 })
395 }
396
397 fn field_to_schema(field: &ConfigField) -> serde_json::Value {
399 let mut schema = serde_json::Map::new();
400
401 let (field_type, is_optional) = if field.optional {
403 let inner_type = field
405 .type_name
406 .strip_prefix("Option<")
407 .and_then(|s| s.strip_suffix('>'))
408 .unwrap_or(&field.type_name);
409 (inner_type, true)
410 } else {
411 (field.type_name.as_str(), false)
412 };
413
414 let json_type = match field_type {
416 "String" | "str" | "&str" => "string",
417 "bool" => "boolean",
418 "i8" | "i16" | "i32" | "i64" | "i128" | "u8" | "u16" | "u32" | "u64" | "u128"
419 | "isize" | "usize" => "integer",
420 "f32" | "f64" => "number",
421 t if t.starts_with("Vec<") => "array",
422 t if t.starts_with("HashMap<") || t.starts_with("BTreeMap<") => "object",
423 _ => "string", };
425
426 schema.insert("type".to_string(), json!(json_type));
427
428 if let Some(desc) = &field.description {
430 schema.insert("description".to_string(), json!(desc));
431 }
432
433 if is_optional {
435 let desc = schema
436 .get("description")
437 .and_then(|v| v.as_str())
438 .unwrap_or("")
439 .to_string();
440 schema.insert(
441 "description".to_string(),
442 json!(if desc.is_empty() {
443 "Optional field".to_string()
444 } else {
445 format!("{} (optional)", desc)
446 }),
447 );
448 }
449
450 serde_json::Value::Object(schema)
451 }
452
453 async fn load_from_url(url: &str) -> anyhow::Result<ConfigSchema> {
455 let response = reqwest::get(url).await?;
456 let schema = response.json::<ConfigSchema>().await?;
457 Ok(schema)
458 }
459
460 fn with_fallback_schema() -> Self {
462 let fallback_schema = Self::create_fallback_schema();
463 Self {
464 schema: fallback_schema,
465 }
466 }
467
468 fn create_fallback_schema() -> ConfigSchema {
472 let mut plugins = HashMap::new();
473
474 let web_schema = json!({
476 "type": "object",
477 "properties": {
478 "host": {
479 "type": "string",
480 "description": "Web server host address",
481 "default": "0.0.0.0"
482 },
483 "port": {
484 "type": "integer",
485 "description": "Web server port",
486 "default": 8080,
487 "minimum": 1,
488 "maximum": 65535
489 }
490 }
491 });
492 plugins.insert("web".to_string(), web_schema);
493
494 let redis_schema = json!({
496 "type": "object",
497 "properties": {
498 "url": {
499 "type": "string",
500 "description": "Redis connection URL",
501 "default": "redis://localhost:6379"
502 }
503 }
504 });
505 plugins.insert("redis".to_string(), redis_schema);
506
507 ConfigSchema { plugins }
508 }
509
510 pub fn has_plugin(&self, prefix: &str) -> bool {
514 self.schema.plugins.contains_key(prefix)
515 }
516
517 pub fn get_all_prefixes(&self) -> Vec<String> {
521 self.schema.plugins.keys().cloned().collect()
522 }
523
524 pub fn get_plugin_schema(&self, prefix: &str) -> Option<&serde_json::Value> {
528 self.schema.plugins.get(prefix)
529 }
530
531 pub fn has_property(&self, prefix: &str, property: &str) -> bool {
535 if let Some(plugin_schema) = self.schema.plugins.get(prefix) {
536 if let Some(properties) = plugin_schema.get("properties") {
538 if let Some(props_obj) = properties.as_object() {
539 return props_obj.contains_key(property);
540 }
541 }
542 }
543 false
544 }
545
546 pub fn get_plugin(&self, prefix: &str) -> Option<PluginSchema> {
550 let plugin_json = self.schema.plugins.get(prefix)?;
551
552 let defs = plugin_json.get("$defs").unwrap_or(&serde_json::Value::Null);
554
555 let properties_json = plugin_json.get("properties")?.as_object()?;
557 let mut properties = HashMap::new();
558
559 for (key, value) in properties_json {
560 if let Some(property_schema) = Self::parse_property_schema_with_defs(key, value, defs) {
561 properties.insert(key.clone(), property_schema);
562 }
563 }
564
565 Some(PluginSchema {
566 prefix: prefix.to_string(),
567 properties,
568 })
569 }
570
571 fn parse_property_schema(name: &str, value: &serde_json::Value) -> Option<PropertySchema> {
573 if value.get("$ref").is_some() {
575 return None;
578 }
579
580 let type_info = Self::parse_type_info(value)?;
581 let description = value
582 .get("description")
583 .and_then(|d| d.as_str())
584 .unwrap_or("")
585 .to_string();
586
587 let default = value.get("default").and_then(Self::parse_value);
588 let required = value
589 .get("required")
590 .and_then(|r| r.as_bool())
591 .unwrap_or(false);
592 let deprecated = value
593 .get("deprecated")
594 .and_then(|d| d.as_str())
595 .map(|s| s.to_string());
596 let example = value
597 .get("example")
598 .and_then(|e| e.as_str())
599 .map(|s| s.to_string());
600
601 Some(PropertySchema {
602 name: name.to_string(),
603 type_info,
604 description,
605 default,
606 required,
607 deprecated,
608 example,
609 })
610 }
611
612 fn parse_property_schema_with_defs(
614 name: &str,
615 value: &serde_json::Value,
616 defs: &serde_json::Value,
617 ) -> Option<PropertySchema> {
618 if let Some(ref_path) = value.get("$ref").and_then(|r| r.as_str()) {
620 if let Some(def_name) = ref_path.strip_prefix("#/$defs/") {
622 if let Some(def_value) = defs.get(def_name) {
623 return Self::parse_property_schema_with_defs(name, def_value, defs);
625 }
626 }
627 return None;
629 }
630
631 let type_info = Self::parse_type_info_with_defs(value, defs)?;
632 let description = value
633 .get("description")
634 .and_then(|d| d.as_str())
635 .unwrap_or("")
636 .to_string();
637
638 let default = value.get("default").and_then(Self::parse_value);
639 let required = value
640 .get("required")
641 .and_then(|r| r.as_bool())
642 .unwrap_or(false);
643 let deprecated = value
644 .get("deprecated")
645 .and_then(|d| d.as_str())
646 .map(|s| s.to_string());
647 let example = value
648 .get("example")
649 .and_then(|e| e.as_str())
650 .map(|s| s.to_string());
651
652 Some(PropertySchema {
653 name: name.to_string(),
654 type_info,
655 description,
656 default,
657 required,
658 deprecated,
659 example,
660 })
661 }
662
663 fn parse_type_info(value: &serde_json::Value) -> Option<TypeInfo> {
665 Self::parse_type_info_with_defs(value, &serde_json::Value::Null)
666 }
667
668 fn parse_type_info_with_defs(
670 value: &serde_json::Value,
671 _defs: &serde_json::Value,
672 ) -> Option<TypeInfo> {
673 if let Some(one_of) = value.get("oneOf").and_then(|o| o.as_array()) {
675 let enum_values: Vec<String> = one_of
677 .iter()
678 .filter_map(|item| item.get("const").and_then(|c| c.as_str()))
679 .map(|s| s.to_string())
680 .collect();
681
682 if !enum_values.is_empty() {
683 return Some(TypeInfo::String {
684 enum_values: Some(enum_values),
685 min_length: None,
686 max_length: None,
687 });
688 }
689 }
690
691 if let Some(enum_array) = value.get("enum").and_then(|e| e.as_array()) {
693 let enum_values: Vec<String> = enum_array
694 .iter()
695 .filter_map(|v| v.as_str())
696 .map(|s| s.to_string())
697 .collect();
698
699 if !enum_values.is_empty() {
700 return Some(TypeInfo::String {
701 enum_values: Some(enum_values),
702 min_length: None,
703 max_length: None,
704 });
705 }
706 }
707
708 let type_str = value.get("type")?.as_str()?;
709
710 match type_str {
711 "string" => {
712 let enum_values = value.get("enum").and_then(|e| e.as_array()).map(|arr| {
713 arr.iter()
714 .filter_map(|v| v.as_str().map(|s| s.to_string()))
715 .collect()
716 });
717 let min_length = value
718 .get("minLength")
719 .and_then(|m| m.as_u64())
720 .map(|n| n as usize);
721 let max_length = value
722 .get("maxLength")
723 .and_then(|m| m.as_u64())
724 .map(|n| n as usize);
725
726 Some(TypeInfo::String {
727 enum_values,
728 min_length,
729 max_length,
730 })
731 }
732 "integer" => {
733 let min = value.get("minimum").and_then(|m| m.as_i64());
734 let max = value.get("maximum").and_then(|m| m.as_i64());
735
736 Some(TypeInfo::Integer { min, max })
737 }
738 "number" => {
739 let min = value.get("minimum").and_then(|m| m.as_f64());
740 let max = value.get("maximum").and_then(|m| m.as_f64());
741
742 Some(TypeInfo::Float { min, max })
743 }
744 "boolean" => Some(TypeInfo::Boolean),
745 "array" => {
746 let items = value.get("items")?;
747 let item_type = Self::parse_type_info(items)?;
748
749 Some(TypeInfo::Array {
750 item_type: Box::new(item_type),
751 })
752 }
753 "object" => {
754 let properties_json = value.get("properties")?.as_object()?;
755 let mut properties = HashMap::new();
756
757 for (key, val) in properties_json {
758 if let Some(prop_schema) = Self::parse_property_schema(key, val) {
759 properties.insert(key.clone(), prop_schema);
760 }
761 }
762
763 Some(TypeInfo::Object { properties })
764 }
765 _ => None,
766 }
767 }
768
769 fn parse_value(value: &serde_json::Value) -> Option<Value> {
771 match value {
772 serde_json::Value::String(s) => Some(Value::String(s.clone())),
773 serde_json::Value::Number(n) => {
774 if let Some(i) = n.as_i64() {
775 Some(Value::Integer(i))
776 } else {
777 n.as_f64().map(Value::Float)
778 }
779 }
780 serde_json::Value::Bool(b) => Some(Value::Boolean(*b)),
781 serde_json::Value::Array(arr) => {
782 let values: Option<Vec<Value>> = arr.iter().map(Self::parse_value).collect();
783 values.map(Value::Array)
784 }
785 serde_json::Value::Object(obj) => {
786 let mut table = HashMap::new();
787 for (k, v) in obj {
788 if let Some(val) = Self::parse_value(v) {
789 table.insert(k.clone(), val);
790 }
791 }
792 Some(Value::Table(table))
793 }
794 serde_json::Value::Null => None,
795 }
796 }
797}
798
799impl Default for SchemaProvider {
800 fn default() -> Self {
801 Self::with_fallback_schema()
802 }
803}
804
805impl SchemaProvider {
806 pub fn from_schema(schema: ConfigSchema) -> Self {
810 Self { schema }
811 }
812}
813
814#[cfg(test)]
815mod tests {
816 use super::*;
817 use serial_test::serial;
818
819 #[tokio::test]
820 async fn test_load_real_schema() {
821 let provider = SchemaProvider::load().await.unwrap();
823
824 assert!(provider.has_plugin("logger"), "Should have logger plugin");
826 assert!(provider.has_plugin("grpc"), "Should have grpc plugin");
827 assert!(provider.has_plugin("web"), "Should have web plugin");
828 assert!(provider.has_plugin("redis"), "Should have redis plugin");
829
830 let grpc_schema = provider.get_plugin("grpc");
832 assert!(grpc_schema.is_some(), "Should be able to get grpc schema");
833
834 if let Some(grpc) = grpc_schema {
835 assert!(
837 grpc.properties.contains_key("graceful"),
838 "GRPC should have 'graceful' property"
839 );
840
841 assert!(
843 grpc.properties.contains_key("port"),
844 "GRPC should have 'port' property"
845 );
846 }
847
848 let logger_schema = provider.get_plugin("logger");
850 assert!(
851 logger_schema.is_some(),
852 "Should be able to get logger schema"
853 );
854
855 if let Some(logger) = logger_schema {
856 println!(
858 "Logger properties found: {:?}",
859 logger.properties.keys().collect::<Vec<_>>()
860 );
861
862 assert!(
864 logger.properties.contains_key("level"),
865 "Logger should have 'level' property. Found: {:?}",
866 logger.properties.keys().collect::<Vec<_>>()
867 );
868 assert!(
869 logger.properties.contains_key("format"),
870 "Logger should have 'format' property"
871 );
872 }
873 }
874
875 #[test]
876 fn test_parse_property_schema() {
877 let json = serde_json::json!({
878 "type": "string",
879 "description": "Test property",
880 "default": "test_value",
881 "enum": ["value1", "value2", "value3"]
882 });
883
884 let property = SchemaProvider::parse_property_schema("test_prop", &json);
885 assert!(property.is_some());
886
887 let prop = property.unwrap();
888 assert_eq!(prop.name, "test_prop");
889 assert_eq!(prop.description, "Test property");
890
891 if let TypeInfo::String { enum_values, .. } = &prop.type_info {
893 assert!(enum_values.is_some());
894 let enums = enum_values.as_ref().unwrap();
895 assert_eq!(enums.len(), 3);
896 assert!(enums.contains(&"value1".to_string()));
897 } else {
898 panic!("Expected String type");
899 }
900
901 assert!(prop.default.is_some());
903 if let Some(Value::String(s)) = &prop.default {
904 assert_eq!(s, "test_value");
905 } else {
906 panic!("Expected String default value");
907 }
908 }
909
910 #[test]
911 fn test_parse_integer_type() {
912 let json = serde_json::json!({
913 "type": "integer",
914 "minimum": 1,
915 "maximum": 65535
916 });
917
918 let type_info = SchemaProvider::parse_type_info(&json);
919 assert!(type_info.is_some());
920
921 if let Some(TypeInfo::Integer { min, max }) = type_info {
922 assert_eq!(min, Some(1));
923 assert_eq!(max, Some(65535));
924 } else {
925 panic!("Expected Integer type");
926 }
927 }
928
929 #[test]
930 fn test_parse_boolean_type() {
931 let json = serde_json::json!({
932 "type": "boolean"
933 });
934
935 let type_info = SchemaProvider::parse_type_info(&json);
936 assert!(type_info.is_some());
937 assert!(matches!(type_info.unwrap(), TypeInfo::Boolean));
938 }
939
940 #[test]
941 fn test_find_schema_in_target() {
942 use std::fs;
943 use tempfile::TempDir;
944
945 let temp_dir = TempDir::new().unwrap();
947 let workspace_path = temp_dir.path();
948
949 let target_dir = workspace_path.join("target/debug/build/my-package/out");
951 fs::create_dir_all(&target_dir).unwrap();
952
953 let schema_path = target_dir.join("summer-lsp.schema.json");
954 let schema_content = serde_json::json!({
955 "properties": {
956 "test-config": {
957 "type": "object",
958 "properties": {
959 "field1": {
960 "type": "string",
961 "description": "Test field"
962 }
963 }
964 }
965 }
966 });
967 fs::write(
968 &schema_path,
969 serde_json::to_string_pretty(&schema_content).unwrap(),
970 )
971 .unwrap();
972
973 let found = SchemaProvider::find_schema_in_target(workspace_path);
975 assert!(found.is_some());
976 assert_eq!(found.unwrap(), schema_path);
977 }
978
979 #[test]
980 #[serial]
981 fn test_find_schema_in_target_multiple_profiles() {
982 use std::fs;
983 use std::thread;
984 use std::time::Duration;
985 use tempfile::TempDir;
986
987 let temp_dir = TempDir::new().unwrap();
989 let workspace_path = temp_dir.path();
990
991 let debug_dir = workspace_path.join("target/debug/build/my-package/out");
993 fs::create_dir_all(&debug_dir).unwrap();
994 let debug_schema = debug_dir.join("summer-lsp.schema.json");
995 fs::write(
996 &debug_schema,
997 serde_json::json!({
998 "properties": {
999 "debug-config": {
1000 "type": "object"
1001 }
1002 }
1003 })
1004 .to_string(),
1005 )
1006 .unwrap();
1007
1008 thread::sleep(Duration::from_millis(10));
1010
1011 let release_dir = workspace_path.join("target/release/build/my-package/out");
1012 fs::create_dir_all(&release_dir).unwrap();
1013 let release_schema = release_dir.join("summer-lsp.schema.json");
1014 fs::write(
1015 &release_schema,
1016 serde_json::json!({
1017 "properties": {
1018 "release-config": {
1019 "type": "object"
1020 }
1021 }
1022 })
1023 .to_string(),
1024 )
1025 .unwrap();
1026
1027 let found = SchemaProvider::find_schema_in_target(workspace_path);
1029 assert!(found.is_some());
1030
1031 let merged_path = found.unwrap();
1032 let content = fs::read_to_string(&merged_path).unwrap();
1033 let schema: serde_json::Value = serde_json::from_str(&content).unwrap();
1034
1035 let properties = schema
1036 .get("properties")
1037 .and_then(|p| p.as_object())
1038 .unwrap();
1039
1040 assert_eq!(properties.len(), 2);
1042 assert!(properties.contains_key("debug-config"));
1043 assert!(properties.contains_key("release-config"));
1044 }
1045
1046 #[test]
1047 fn test_find_schema_in_target_not_exists() {
1048 use tempfile::TempDir;
1049
1050 let temp_dir = TempDir::new().unwrap();
1051 let workspace_path = temp_dir.path();
1052
1053 let found = SchemaProvider::find_schema_in_target(workspace_path);
1055 assert!(found.is_none());
1056 }
1057
1058 #[test]
1059 fn test_load_local_schema_file() {
1060 use std::fs;
1061 use tempfile::TempDir;
1062
1063 let temp_dir = TempDir::new().unwrap();
1064 let schema_path = temp_dir.path().join("test-schema.json");
1065
1066 let schema_content = serde_json::json!({
1067 "properties": {
1068 "web": {
1069 "type": "object",
1070 "properties": {
1071 "port": {
1072 "type": "integer",
1073 "default": 8080
1074 }
1075 }
1076 },
1077 "database": {
1078 "type": "object",
1079 "properties": {
1080 "url": {
1081 "type": "string"
1082 }
1083 }
1084 }
1085 }
1086 });
1087
1088 fs::write(
1089 &schema_path,
1090 serde_json::to_string_pretty(&schema_content).unwrap(),
1091 )
1092 .unwrap();
1093
1094 let schemas = SchemaProvider::load_local_schema_file(&schema_path).unwrap();
1095 assert_eq!(schemas.len(), 2);
1096 assert!(schemas.contains_key("web"));
1097 assert!(schemas.contains_key("database"));
1098 }
1099
1100 #[test]
1101 #[serial]
1102 fn test_merge_schema_files() {
1103 use std::fs;
1104 use tempfile::TempDir;
1105
1106 let temp_dir = TempDir::new().unwrap();
1107
1108 let schema1_path = temp_dir.path().join("schema1.json");
1110 let schema1_content = serde_json::json!({
1111 "properties": {
1112 "service-a": {
1113 "type": "object",
1114 "properties": {
1115 "endpoint": {
1116 "type": "string"
1117 }
1118 }
1119 }
1120 }
1121 });
1122 fs::write(
1123 &schema1_path,
1124 serde_json::to_string_pretty(&schema1_content).unwrap(),
1125 )
1126 .unwrap();
1127
1128 let schema2_path = temp_dir.path().join("schema2.json");
1130 let schema2_content = serde_json::json!({
1131 "properties": {
1132 "service-b": {
1133 "type": "object",
1134 "properties": {
1135 "port": {
1136 "type": "integer"
1137 }
1138 }
1139 }
1140 }
1141 });
1142 fs::write(
1143 &schema2_path,
1144 serde_json::to_string_pretty(&schema2_content).unwrap(),
1145 )
1146 .unwrap();
1147
1148 let time1 = std::time::SystemTime::now();
1150 let time2 = std::time::SystemTime::now();
1151
1152 let schema_files = vec![(schema1_path, time1), (schema2_path, time2)];
1153
1154 let merged_path = SchemaProvider::merge_schema_files(&schema_files).unwrap();
1156 assert!(merged_path.exists());
1157
1158 let merged_content = fs::read_to_string(&merged_path).unwrap();
1160 let merged_schema: serde_json::Value = serde_json::from_str(&merged_content).unwrap();
1161
1162 let properties = merged_schema
1163 .get("properties")
1164 .and_then(|p| p.as_object())
1165 .unwrap();
1166
1167 assert_eq!(properties.len(), 2);
1169 assert!(properties.contains_key("service-a"));
1170 assert!(properties.contains_key("service-b"));
1171 }
1172
1173 #[test]
1174 #[serial]
1175 fn test_find_schema_in_target_multiple_crates() {
1176 use std::fs;
1177 use tempfile::TempDir;
1178
1179 let temp_dir = TempDir::new().unwrap();
1180 let workspace_path = temp_dir.path();
1181
1182 let crate1_dir = workspace_path.join("target/debug/build/crate1/out");
1184 fs::create_dir_all(&crate1_dir).unwrap();
1185 let schema1_path = crate1_dir.join("summer-lsp.schema.json");
1186 fs::write(
1187 &schema1_path,
1188 serde_json::json!({
1189 "properties": {
1190 "service-a": {
1191 "type": "object"
1192 }
1193 }
1194 })
1195 .to_string(),
1196 )
1197 .unwrap();
1198
1199 let crate2_dir = workspace_path.join("target/debug/build/crate2/out");
1200 fs::create_dir_all(&crate2_dir).unwrap();
1201 let schema2_path = crate2_dir.join("summer-lsp.schema.json");
1202 fs::write(
1203 &schema2_path,
1204 serde_json::json!({
1205 "properties": {
1206 "service-b": {
1207 "type": "object"
1208 }
1209 }
1210 })
1211 .to_string(),
1212 )
1213 .unwrap();
1214
1215 let found = SchemaProvider::find_schema_in_target(workspace_path);
1217 assert!(found.is_some());
1218
1219 let merged_path = found.unwrap();
1220 let content = fs::read_to_string(&merged_path).unwrap();
1221 let schema: serde_json::Value = serde_json::from_str(&content).unwrap();
1222
1223 let properties = schema
1224 .get("properties")
1225 .and_then(|p| p.as_object())
1226 .unwrap();
1227
1228 assert_eq!(properties.len(), 2);
1230 assert!(properties.contains_key("service-a"));
1231 assert!(properties.contains_key("service-b"));
1232 }
1233}