use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use super::{EditorConfig, Input};
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
#[allow(clippy::upper_case_acronyms)]
pub struct VSCodeConfig {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) inputs: Vec<Input>,
#[serde(default)]
pub(crate) servers: BTreeMap<String, VSCodeServer>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[allow(clippy::upper_case_acronyms)]
pub struct VSCodeServer {
#[serde(rename = "type", default, skip_serializing_if = "String::is_empty")]
pub(crate) kind: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub(crate) command: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) args: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) env: Option<BTreeMap<String, String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) headers: Option<BTreeMap<String, String>>,
}
impl EditorConfig for VSCodeConfig {
type Server = VSCodeServer;
fn has_server(&self, name: &str) -> bool {
self.servers.contains_key(name)
}
fn add_server(&mut self, name: String, server: Self::Server) {
self.servers.insert(name, server);
}
fn remove_server(&mut self, name: &str) {
self.servers.remove(name);
}
fn server_names(&self) -> Box<dyn Iterator<Item = &str> + '_> {
Box::new(self.servers.keys().map(String::as_str))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_config_serializes_with_only_servers() {
let cfg = VSCodeConfig::default();
let s = serde_json::to_string(&cfg).expect("serialize");
assert_eq!(s, r#"{"servers":{}}"#);
}
#[test]
fn stdio_server_field_order_is_type_command_args_env() {
let mut env = BTreeMap::new();
env.insert("PATH".into(), "/usr/bin".into());
env.insert("DEBUG".into(), "1".into());
let server = VSCodeServer {
kind: "stdio".into(),
command: "/bin/app".into(),
args: Some(vec!["mcp".into(), "start".into()]),
env: Some(env),
url: None,
headers: None,
};
let s = serde_json::to_string(&server).expect("serialize");
assert_eq!(
s,
r#"{"type":"stdio","command":"/bin/app","args":["mcp","start"],"env":{"DEBUG":"1","PATH":"/usr/bin"}}"#
);
}
#[test]
fn empty_type_skipped_on_write() {
let server = VSCodeServer {
kind: String::new(),
command: "/bin/app".into(),
args: None,
env: None,
url: None,
headers: None,
};
let s = serde_json::to_string(&server).expect("serialize");
assert_eq!(s, r#"{"command":"/bin/app"}"#);
}
#[test]
fn url_and_headers_round_trip() {
let mut headers = BTreeMap::new();
headers.insert("Authorization".into(), "Bearer x".into());
let server = VSCodeServer {
kind: "sse".into(),
command: String::new(),
args: None,
env: None,
url: Some("https://example.com/mcp".into()),
headers: Some(headers),
};
let s = serde_json::to_string(&server).expect("serialize");
assert_eq!(
s,
r#"{"type":"sse","url":"https://example.com/mcp","headers":{"Authorization":"Bearer x"}}"#
);
let parsed: VSCodeServer = serde_json::from_str(&s).expect("parse");
assert_eq!(parsed.kind, "sse");
assert_eq!(parsed.command, "");
assert_eq!(parsed.url.as_deref(), Some("https://example.com/mcp"));
assert_eq!(
parsed.headers.as_ref().and_then(|h| h.get("Authorization")),
Some(&"Bearer x".to_string())
);
}
#[test]
fn inputs_round_trip_preserves_both_password_states() {
let raw = r#"{
"inputs": [
{"type": "promptString", "id": "api-key", "description": "API key", "password": true},
{"type": "promptString", "id": "username", "description": "Username", "password": false}
],
"servers": {
"existing": {"type": "stdio", "command": "/bin/x"}
}
}"#;
let cfg: VSCodeConfig = serde_json::from_str(raw).expect("parse");
assert_eq!(cfg.inputs.len(), 2);
assert!(cfg.inputs[0].password);
assert!(!cfg.inputs[1].password);
assert!(cfg.has_server("existing"));
let s = serde_json::to_string(&cfg).expect("serialize");
assert!(
!s.contains(r#""password":false"#),
"password=false must be omitted, got {s}"
);
assert!(
s.contains(r#""password":true"#),
"password=true must be present, got {s}"
);
let cfg2: VSCodeConfig = serde_json::from_str(&s).expect("reparse");
assert_eq!(cfg2.inputs.len(), 2);
assert!(cfg2.inputs[0].password);
assert!(!cfg2.inputs[1].password);
assert!(cfg2.has_server("existing"));
}
#[test]
fn add_remove_server_round_trip() {
let mut cfg = VSCodeConfig::default();
assert!(!cfg.has_server("foo"));
cfg.add_server(
"foo".into(),
VSCodeServer {
kind: "stdio".into(),
command: "/x".into(),
args: None,
env: None,
url: None,
headers: None,
},
);
assert!(cfg.has_server("foo"));
cfg.remove_server("foo");
assert!(!cfg.has_server("foo"));
}
}