use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::HashMap;
use std::path::Path;
use crate::scanner::config::{ConfigField, ConfigScanner, ConfigurationStruct};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigSchema {
#[serde(rename = "properties")]
pub plugins: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginSchema {
pub prefix: String,
pub properties: HashMap<String, PropertySchema>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PropertySchema {
pub name: String,
pub type_info: TypeInfo,
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<Value>,
#[serde(default)]
pub required: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum TypeInfo {
String {
#[serde(skip_serializing_if = "Option::is_none")]
enum_values: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
min_length: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
max_length: Option<usize>,
},
Integer {
#[serde(skip_serializing_if = "Option::is_none")]
min: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
max: Option<i64>,
},
Float {
#[serde(skip_serializing_if = "Option::is_none")]
min: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
max: Option<f64>,
},
Boolean,
Array {
item_type: Box<TypeInfo>,
},
Object {
properties: HashMap<String, PropertySchema>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum Value {
String(String),
Integer(i64),
Float(f64),
Boolean(bool),
Array(Vec<Value>),
Table(HashMap<String, Value>),
}
#[derive(Clone)]
pub struct SchemaProvider {
schema: ConfigSchema,
}
impl SchemaProvider {
const SCHEMA_URL: &'static str = "https://summer-rs.github.io/config-schema.json";
pub fn new() -> Self {
Self {
schema: ConfigSchema {
plugins: HashMap::new(),
},
}
}
pub async fn load() -> anyhow::Result<Self> {
match Self::load_from_url(Self::SCHEMA_URL).await {
Ok(schema) => {
tracing::info!("Successfully loaded schema from {}", Self::SCHEMA_URL);
Ok(Self { schema })
}
Err(e) => {
tracing::warn!("Failed to load schema from URL: {}, using fallback", e);
Ok(Self::with_fallback_schema())
}
}
}
pub async fn load_with_workspace(workspace_path: &Path) -> anyhow::Result<Self> {
let mut provider = Self::load().await?;
if let Some(schema_path) = Self::find_schema_in_target(workspace_path) {
tracing::info!("Loading schema from target directory: {:?}", schema_path);
match Self::load_local_schema_file(&schema_path) {
Ok(local_schemas) => {
tracing::info!("Loaded {} local schemas from target", local_schemas.len());
for (prefix, schema) in local_schemas {
provider.schema.plugins.insert(prefix, schema);
}
return Ok(provider);
}
Err(e) => {
tracing::warn!("Failed to load schema from target: {}", e);
}
}
}
let local_schema_path = workspace_path.join(".summer-lsp.schema.json");
if local_schema_path.exists() {
tracing::info!("Loading local schema from: {:?}", local_schema_path);
match Self::load_local_schema_file(&local_schema_path) {
Ok(local_schemas) => {
tracing::info!("Loaded {} local schemas from file", local_schemas.len());
for (prefix, schema) in local_schemas {
provider.schema.plugins.insert(prefix, schema);
}
return Ok(provider);
}
Err(e) => {
tracing::warn!("Failed to load local schema file: {}", e);
}
}
}
tracing::info!("Scanning local configurations in: {:?}", workspace_path);
let scanner = ConfigScanner::new();
match scanner.scan_configurations(workspace_path) {
Ok(configurations) => {
tracing::info!("Found {} local configuration structs", configurations.len());
for config in configurations {
let schema_json = Self::configuration_to_schema(&config);
provider
.schema
.plugins
.insert(config.prefix.clone(), schema_json);
tracing::debug!("Added local configuration: {}", config.prefix);
}
}
Err(e) => {
tracing::warn!("Failed to scan local configurations: {}", e);
}
}
Ok(provider)
}
fn find_schema_in_target(workspace_path: &Path) -> Option<std::path::PathBuf> {
let target_dir = workspace_path.join("target");
if !target_dir.exists() {
return None;
}
let mut schema_files = Vec::new();
if let Ok(entries) = std::fs::read_dir(&target_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let build_dir = path.join("build");
if build_dir.exists() {
if let Ok(build_entries) = std::fs::read_dir(&build_dir) {
for build_entry in build_entries.flatten() {
let out_dir = build_entry.path().join("out");
let schema_path = out_dir.join("summer-lsp.schema.json");
if schema_path.exists() {
if let Ok(metadata) = std::fs::metadata(&schema_path) {
if let Ok(modified) = metadata.modified() {
schema_files.push((schema_path, modified));
}
}
}
}
}
}
}
}
}
if schema_files.is_empty() {
return None;
}
if schema_files.len() == 1 {
return Some(schema_files[0].0.clone());
}
tracing::info!("Found {} schema files, merging...", schema_files.len());
schema_files.sort_by(|a, b| b.1.cmp(&a.1));
match Self::merge_schema_files(&schema_files) {
Ok(merged_path) => Some(merged_path),
Err(e) => {
tracing::warn!("Failed to merge schema files: {}, using latest", e);
Some(schema_files[0].0.clone())
}
}
}
fn merge_schema_files(
schema_files: &[(std::path::PathBuf, std::time::SystemTime)],
) -> anyhow::Result<std::path::PathBuf> {
use std::fs;
let mut merged_properties = serde_json::Map::new();
for (path, _) in schema_files {
tracing::debug!("Merging schema from: {:?}", path);
let content = fs::read_to_string(path)?;
let schema: serde_json::Value = serde_json::from_str(&content)?;
if let Some(properties) = schema.get("properties").and_then(|p| p.as_object()) {
for (key, value) in properties {
merged_properties.insert(key.clone(), value.clone());
}
}
}
let merged_schema = serde_json::json!({
"type": "object",
"properties": merged_properties
});
let temp_dir = std::env::temp_dir();
let merged_path = temp_dir.join("summer-lsp-merged.schema.json");
fs::write(&merged_path, serde_json::to_string_pretty(&merged_schema)?)?;
tracing::info!(
"Merged {} schema files ({} configs) into: {:?}",
schema_files.len(),
merged_properties.len(),
merged_path
);
Ok(merged_path)
}
fn load_local_schema_file(path: &Path) -> anyhow::Result<HashMap<String, serde_json::Value>> {
let content = std::fs::read_to_string(path)?;
let schema: serde_json::Value = serde_json::from_str(&content)?;
let mut schemas = HashMap::new();
if let Some(properties) = schema.get("properties").and_then(|p| p.as_object()) {
for (key, value) in properties {
schemas.insert(key.clone(), value.clone());
}
}
Ok(schemas)
}
fn configuration_to_schema(config: &ConfigurationStruct) -> serde_json::Value {
let mut properties = serde_json::Map::new();
for field in &config.fields {
let field_schema = Self::field_to_schema(field);
properties.insert(field.name.clone(), field_schema);
}
json!({
"type": "object",
"properties": properties,
"description": format!("Configuration for {}", config.name)
})
}
fn field_to_schema(field: &ConfigField) -> serde_json::Value {
let mut schema = serde_json::Map::new();
let (field_type, is_optional) = if field.optional {
let inner_type = field
.type_name
.strip_prefix("Option<")
.and_then(|s| s.strip_suffix('>'))
.unwrap_or(&field.type_name);
(inner_type, true)
} else {
(field.type_name.as_str(), false)
};
let json_type = match field_type {
"String" | "str" | "&str" => "string",
"bool" => "boolean",
"i8" | "i16" | "i32" | "i64" | "i128" | "u8" | "u16" | "u32" | "u64" | "u128"
| "isize" | "usize" => "integer",
"f32" | "f64" => "number",
t if t.starts_with("Vec<") => "array",
t if t.starts_with("HashMap<") || t.starts_with("BTreeMap<") => "object",
_ => "string", };
schema.insert("type".to_string(), json!(json_type));
if let Some(desc) = &field.description {
schema.insert("description".to_string(), json!(desc));
}
if is_optional {
let desc = schema
.get("description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
schema.insert(
"description".to_string(),
json!(if desc.is_empty() {
"Optional field".to_string()
} else {
format!("{} (optional)", desc)
}),
);
}
serde_json::Value::Object(schema)
}
async fn load_from_url(url: &str) -> anyhow::Result<ConfigSchema> {
let response = reqwest::get(url).await?;
let schema = response.json::<ConfigSchema>().await?;
Ok(schema)
}
fn with_fallback_schema() -> Self {
let fallback_schema = Self::create_fallback_schema();
Self {
schema: fallback_schema,
}
}
fn create_fallback_schema() -> ConfigSchema {
let mut plugins = HashMap::new();
let web_schema = json!({
"type": "object",
"properties": {
"host": {
"type": "string",
"description": "Web server host address",
"default": "0.0.0.0"
},
"port": {
"type": "integer",
"description": "Web server port",
"default": 8080,
"minimum": 1,
"maximum": 65535
}
}
});
plugins.insert("web".to_string(), web_schema);
let redis_schema = json!({
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "Redis connection URL",
"default": "redis://localhost:6379"
}
}
});
plugins.insert("redis".to_string(), redis_schema);
ConfigSchema { plugins }
}
pub fn has_plugin(&self, prefix: &str) -> bool {
self.schema.plugins.contains_key(prefix)
}
pub fn get_all_prefixes(&self) -> Vec<String> {
self.schema.plugins.keys().cloned().collect()
}
pub fn get_plugin_schema(&self, prefix: &str) -> Option<&serde_json::Value> {
self.schema.plugins.get(prefix)
}
pub fn has_property(&self, prefix: &str, property: &str) -> bool {
if let Some(plugin_schema) = self.schema.plugins.get(prefix) {
if let Some(properties) = plugin_schema.get("properties") {
if let Some(props_obj) = properties.as_object() {
return props_obj.contains_key(property);
}
}
}
false
}
pub fn get_plugin(&self, prefix: &str) -> Option<PluginSchema> {
let plugin_json = self.schema.plugins.get(prefix)?;
let defs = plugin_json.get("$defs").unwrap_or(&serde_json::Value::Null);
let properties_json = plugin_json.get("properties")?.as_object()?;
let mut properties = HashMap::new();
for (key, value) in properties_json {
if let Some(property_schema) = Self::parse_property_schema_with_defs(key, value, defs) {
properties.insert(key.clone(), property_schema);
}
}
Some(PluginSchema {
prefix: prefix.to_string(),
properties,
})
}
fn parse_property_schema(name: &str, value: &serde_json::Value) -> Option<PropertySchema> {
if value.get("$ref").is_some() {
return None;
}
let type_info = Self::parse_type_info(value)?;
let description = value
.get("description")
.and_then(|d| d.as_str())
.unwrap_or("")
.to_string();
let default = value.get("default").and_then(Self::parse_value);
let required = value
.get("required")
.and_then(|r| r.as_bool())
.unwrap_or(false);
let deprecated = value
.get("deprecated")
.and_then(|d| d.as_str())
.map(|s| s.to_string());
let example = value
.get("example")
.and_then(|e| e.as_str())
.map(|s| s.to_string());
Some(PropertySchema {
name: name.to_string(),
type_info,
description,
default,
required,
deprecated,
example,
})
}
fn parse_property_schema_with_defs(
name: &str,
value: &serde_json::Value,
defs: &serde_json::Value,
) -> Option<PropertySchema> {
if let Some(ref_path) = value.get("$ref").and_then(|r| r.as_str()) {
if let Some(def_name) = ref_path.strip_prefix("#/$defs/") {
if let Some(def_value) = defs.get(def_name) {
return Self::parse_property_schema_with_defs(name, def_value, defs);
}
}
return None;
}
let type_info = Self::parse_type_info_with_defs(value, defs)?;
let description = value
.get("description")
.and_then(|d| d.as_str())
.unwrap_or("")
.to_string();
let default = value.get("default").and_then(Self::parse_value);
let required = value
.get("required")
.and_then(|r| r.as_bool())
.unwrap_or(false);
let deprecated = value
.get("deprecated")
.and_then(|d| d.as_str())
.map(|s| s.to_string());
let example = value
.get("example")
.and_then(|e| e.as_str())
.map(|s| s.to_string());
Some(PropertySchema {
name: name.to_string(),
type_info,
description,
default,
required,
deprecated,
example,
})
}
fn parse_type_info(value: &serde_json::Value) -> Option<TypeInfo> {
Self::parse_type_info_with_defs(value, &serde_json::Value::Null)
}
fn parse_type_info_with_defs(
value: &serde_json::Value,
_defs: &serde_json::Value,
) -> Option<TypeInfo> {
if let Some(one_of) = value.get("oneOf").and_then(|o| o.as_array()) {
let enum_values: Vec<String> = one_of
.iter()
.filter_map(|item| item.get("const").and_then(|c| c.as_str()))
.map(|s| s.to_string())
.collect();
if !enum_values.is_empty() {
return Some(TypeInfo::String {
enum_values: Some(enum_values),
min_length: None,
max_length: None,
});
}
}
if let Some(enum_array) = value.get("enum").and_then(|e| e.as_array()) {
let enum_values: Vec<String> = enum_array
.iter()
.filter_map(|v| v.as_str())
.map(|s| s.to_string())
.collect();
if !enum_values.is_empty() {
return Some(TypeInfo::String {
enum_values: Some(enum_values),
min_length: None,
max_length: None,
});
}
}
let type_str = value.get("type")?.as_str()?;
match type_str {
"string" => {
let enum_values = value.get("enum").and_then(|e| e.as_array()).map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
});
let min_length = value
.get("minLength")
.and_then(|m| m.as_u64())
.map(|n| n as usize);
let max_length = value
.get("maxLength")
.and_then(|m| m.as_u64())
.map(|n| n as usize);
Some(TypeInfo::String {
enum_values,
min_length,
max_length,
})
}
"integer" => {
let min = value.get("minimum").and_then(|m| m.as_i64());
let max = value.get("maximum").and_then(|m| m.as_i64());
Some(TypeInfo::Integer { min, max })
}
"number" => {
let min = value.get("minimum").and_then(|m| m.as_f64());
let max = value.get("maximum").and_then(|m| m.as_f64());
Some(TypeInfo::Float { min, max })
}
"boolean" => Some(TypeInfo::Boolean),
"array" => {
let items = value.get("items")?;
let item_type = Self::parse_type_info(items)?;
Some(TypeInfo::Array {
item_type: Box::new(item_type),
})
}
"object" => {
let properties_json = value.get("properties")?.as_object()?;
let mut properties = HashMap::new();
for (key, val) in properties_json {
if let Some(prop_schema) = Self::parse_property_schema(key, val) {
properties.insert(key.clone(), prop_schema);
}
}
Some(TypeInfo::Object { properties })
}
_ => None,
}
}
fn parse_value(value: &serde_json::Value) -> Option<Value> {
match value {
serde_json::Value::String(s) => Some(Value::String(s.clone())),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Some(Value::Integer(i))
} else {
n.as_f64().map(Value::Float)
}
}
serde_json::Value::Bool(b) => Some(Value::Boolean(*b)),
serde_json::Value::Array(arr) => {
let values: Option<Vec<Value>> = arr.iter().map(Self::parse_value).collect();
values.map(Value::Array)
}
serde_json::Value::Object(obj) => {
let mut table = HashMap::new();
for (k, v) in obj {
if let Some(val) = Self::parse_value(v) {
table.insert(k.clone(), val);
}
}
Some(Value::Table(table))
}
serde_json::Value::Null => None,
}
}
}
impl Default for SchemaProvider {
fn default() -> Self {
Self::with_fallback_schema()
}
}
impl SchemaProvider {
pub fn from_schema(schema: ConfigSchema) -> Self {
Self { schema }
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[tokio::test]
async fn test_load_real_schema() {
let provider = SchemaProvider::load().await.unwrap();
assert!(provider.has_plugin("logger"), "Should have logger plugin");
assert!(provider.has_plugin("grpc"), "Should have grpc plugin");
assert!(provider.has_plugin("web"), "Should have web plugin");
assert!(provider.has_plugin("redis"), "Should have redis plugin");
let grpc_schema = provider.get_plugin("grpc");
assert!(grpc_schema.is_some(), "Should be able to get grpc schema");
if let Some(grpc) = grpc_schema {
assert!(
grpc.properties.contains_key("graceful"),
"GRPC should have 'graceful' property"
);
assert!(
grpc.properties.contains_key("port"),
"GRPC should have 'port' property"
);
}
let logger_schema = provider.get_plugin("logger");
assert!(
logger_schema.is_some(),
"Should be able to get logger schema"
);
if let Some(logger) = logger_schema {
println!(
"Logger properties found: {:?}",
logger.properties.keys().collect::<Vec<_>>()
);
assert!(
logger.properties.contains_key("level"),
"Logger should have 'level' property. Found: {:?}",
logger.properties.keys().collect::<Vec<_>>()
);
assert!(
logger.properties.contains_key("format"),
"Logger should have 'format' property"
);
}
}
#[test]
fn test_parse_property_schema() {
let json = serde_json::json!({
"type": "string",
"description": "Test property",
"default": "test_value",
"enum": ["value1", "value2", "value3"]
});
let property = SchemaProvider::parse_property_schema("test_prop", &json);
assert!(property.is_some());
let prop = property.unwrap();
assert_eq!(prop.name, "test_prop");
assert_eq!(prop.description, "Test property");
if let TypeInfo::String { enum_values, .. } = &prop.type_info {
assert!(enum_values.is_some());
let enums = enum_values.as_ref().unwrap();
assert_eq!(enums.len(), 3);
assert!(enums.contains(&"value1".to_string()));
} else {
panic!("Expected String type");
}
assert!(prop.default.is_some());
if let Some(Value::String(s)) = &prop.default {
assert_eq!(s, "test_value");
} else {
panic!("Expected String default value");
}
}
#[test]
fn test_parse_integer_type() {
let json = serde_json::json!({
"type": "integer",
"minimum": 1,
"maximum": 65535
});
let type_info = SchemaProvider::parse_type_info(&json);
assert!(type_info.is_some());
if let Some(TypeInfo::Integer { min, max }) = type_info {
assert_eq!(min, Some(1));
assert_eq!(max, Some(65535));
} else {
panic!("Expected Integer type");
}
}
#[test]
fn test_parse_boolean_type() {
let json = serde_json::json!({
"type": "boolean"
});
let type_info = SchemaProvider::parse_type_info(&json);
assert!(type_info.is_some());
assert!(matches!(type_info.unwrap(), TypeInfo::Boolean));
}
#[test]
fn test_find_schema_in_target() {
use std::fs;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let workspace_path = temp_dir.path();
let target_dir = workspace_path.join("target/debug/build/my-package/out");
fs::create_dir_all(&target_dir).unwrap();
let schema_path = target_dir.join("summer-lsp.schema.json");
let schema_content = serde_json::json!({
"properties": {
"test-config": {
"type": "object",
"properties": {
"field1": {
"type": "string",
"description": "Test field"
}
}
}
}
});
fs::write(
&schema_path,
serde_json::to_string_pretty(&schema_content).unwrap(),
)
.unwrap();
let found = SchemaProvider::find_schema_in_target(workspace_path);
assert!(found.is_some());
assert_eq!(found.unwrap(), schema_path);
}
#[test]
#[serial]
fn test_find_schema_in_target_multiple_profiles() {
use std::fs;
use std::thread;
use std::time::Duration;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let workspace_path = temp_dir.path();
let debug_dir = workspace_path.join("target/debug/build/my-package/out");
fs::create_dir_all(&debug_dir).unwrap();
let debug_schema = debug_dir.join("summer-lsp.schema.json");
fs::write(
&debug_schema,
serde_json::json!({
"properties": {
"debug-config": {
"type": "object"
}
}
})
.to_string(),
)
.unwrap();
thread::sleep(Duration::from_millis(10));
let release_dir = workspace_path.join("target/release/build/my-package/out");
fs::create_dir_all(&release_dir).unwrap();
let release_schema = release_dir.join("summer-lsp.schema.json");
fs::write(
&release_schema,
serde_json::json!({
"properties": {
"release-config": {
"type": "object"
}
}
})
.to_string(),
)
.unwrap();
let found = SchemaProvider::find_schema_in_target(workspace_path);
assert!(found.is_some());
let merged_path = found.unwrap();
let content = fs::read_to_string(&merged_path).unwrap();
let schema: serde_json::Value = serde_json::from_str(&content).unwrap();
let properties = schema
.get("properties")
.and_then(|p| p.as_object())
.unwrap();
assert_eq!(properties.len(), 2);
assert!(properties.contains_key("debug-config"));
assert!(properties.contains_key("release-config"));
}
#[test]
fn test_find_schema_in_target_not_exists() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let workspace_path = temp_dir.path();
let found = SchemaProvider::find_schema_in_target(workspace_path);
assert!(found.is_none());
}
#[test]
fn test_load_local_schema_file() {
use std::fs;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let schema_path = temp_dir.path().join("test-schema.json");
let schema_content = serde_json::json!({
"properties": {
"web": {
"type": "object",
"properties": {
"port": {
"type": "integer",
"default": 8080
}
}
},
"database": {
"type": "object",
"properties": {
"url": {
"type": "string"
}
}
}
}
});
fs::write(
&schema_path,
serde_json::to_string_pretty(&schema_content).unwrap(),
)
.unwrap();
let schemas = SchemaProvider::load_local_schema_file(&schema_path).unwrap();
assert_eq!(schemas.len(), 2);
assert!(schemas.contains_key("web"));
assert!(schemas.contains_key("database"));
}
#[test]
#[serial]
fn test_merge_schema_files() {
use std::fs;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let schema1_path = temp_dir.path().join("schema1.json");
let schema1_content = serde_json::json!({
"properties": {
"service-a": {
"type": "object",
"properties": {
"endpoint": {
"type": "string"
}
}
}
}
});
fs::write(
&schema1_path,
serde_json::to_string_pretty(&schema1_content).unwrap(),
)
.unwrap();
let schema2_path = temp_dir.path().join("schema2.json");
let schema2_content = serde_json::json!({
"properties": {
"service-b": {
"type": "object",
"properties": {
"port": {
"type": "integer"
}
}
}
}
});
fs::write(
&schema2_path,
serde_json::to_string_pretty(&schema2_content).unwrap(),
)
.unwrap();
let time1 = std::time::SystemTime::now();
let time2 = std::time::SystemTime::now();
let schema_files = vec![(schema1_path, time1), (schema2_path, time2)];
let merged_path = SchemaProvider::merge_schema_files(&schema_files).unwrap();
assert!(merged_path.exists());
let merged_content = fs::read_to_string(&merged_path).unwrap();
let merged_schema: serde_json::Value = serde_json::from_str(&merged_content).unwrap();
let properties = merged_schema
.get("properties")
.and_then(|p| p.as_object())
.unwrap();
assert_eq!(properties.len(), 2);
assert!(properties.contains_key("service-a"));
assert!(properties.contains_key("service-b"));
}
#[test]
#[serial]
fn test_find_schema_in_target_multiple_crates() {
use std::fs;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let workspace_path = temp_dir.path();
let crate1_dir = workspace_path.join("target/debug/build/crate1/out");
fs::create_dir_all(&crate1_dir).unwrap();
let schema1_path = crate1_dir.join("summer-lsp.schema.json");
fs::write(
&schema1_path,
serde_json::json!({
"properties": {
"service-a": {
"type": "object"
}
}
})
.to_string(),
)
.unwrap();
let crate2_dir = workspace_path.join("target/debug/build/crate2/out");
fs::create_dir_all(&crate2_dir).unwrap();
let schema2_path = crate2_dir.join("summer-lsp.schema.json");
fs::write(
&schema2_path,
serde_json::json!({
"properties": {
"service-b": {
"type": "object"
}
}
})
.to_string(),
)
.unwrap();
let found = SchemaProvider::find_schema_in_target(workspace_path);
assert!(found.is_some());
let merged_path = found.unwrap();
let content = fs::read_to_string(&merged_path).unwrap();
let schema: serde_json::Value = serde_json::from_str(&content).unwrap();
let properties = schema
.get("properties")
.and_then(|p| p.as_object())
.unwrap();
assert_eq!(properties.len(), 2);
assert!(properties.contains_key("service-a"));
assert!(properties.contains_key("service-b"));
}
}