use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Schema {
pub schema_version: u32,
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub params: BTreeMap<String, ParamSpec>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub attaches_to: Vec<String>,
#[serde(default)]
pub body: BodyShape,
#[serde(default)]
pub verbatim_label: bool,
#[serde(default)]
pub capabilities: Capabilities,
#[serde(default)]
pub hooks: HookSet,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub handler: Option<HandlerSpec>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ParamSpec {
#[serde(rename = "type")]
pub ty: ParamType,
#[serde(default)]
pub required: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub values: Vec<EnumValue>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum ParamType {
String,
Bool,
Int,
Float,
Enum,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EnumValue {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BodyShape {
#[serde(default = "BodyKind::default_kind")]
pub kind: BodyKind,
#[serde(default)]
pub presence: BodyPresence,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl Default for BodyShape {
fn default() -> Self {
Self {
kind: BodyKind::None,
presence: BodyPresence::Optional,
description: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum BodyKind {
None,
Text,
Lex,
}
impl BodyKind {
fn default_kind() -> Self {
Self::None
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum BodyPresence {
Optional,
Required,
}
impl Default for BodyPresence {
fn default() -> Self {
Self::Optional
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Capabilities {
#[serde(default)]
pub fs: bool,
#[serde(default)]
pub net: bool,
}
impl Capabilities {
pub fn is_pure(&self) -> bool {
*self == Self::default()
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HookSet {
#[serde(default)]
pub label: bool,
#[serde(default)]
pub validate: bool,
#[serde(default)]
pub resolve: bool,
#[serde(default)]
pub ir_build: bool,
#[serde(default)]
pub hover: bool,
#[serde(default)]
pub completion: bool,
#[serde(default)]
pub code_action: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub render: Vec<RenderHook>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct RenderHook(pub String);
impl RenderHook {
pub fn new(format: impl Into<String>) -> Self {
Self(format.into())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HandlerSpec {
pub transport: HandlerTransport,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub command: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeout_ms: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum HandlerTransport {
Native,
Subprocess,
Wasm,
}
#[cfg(test)]
mod tests {
use super::*;
fn comment_schema() -> Schema {
let mut params = BTreeMap::new();
params.insert(
"role".into(),
ParamSpec {
ty: ParamType::Enum,
required: true,
default: None,
description: None,
pattern: None,
values: vec![
EnumValue {
name: "author".into(),
description: None,
},
EnumValue {
name: "editor".into(),
description: None,
},
],
},
);
Schema {
schema_version: 1,
label: "acme.commenting".into(),
description: Some("A comment thread.".into()),
params,
attaches_to: vec!["paragraph".into(), "session".into()],
body: BodyShape {
kind: BodyKind::Lex,
presence: BodyPresence::Required,
description: None,
},
verbatim_label: false,
capabilities: Capabilities {
fs: false,
net: false,
},
hooks: HookSet {
validate: true,
hover: true,
render: vec![RenderHook::new("html"), RenderHook::new("markdown")],
..HookSet::default()
},
handler: Some(HandlerSpec {
transport: HandlerTransport::Subprocess,
command: vec!["acme-comment-handler".into()],
timeout_ms: Some(2000),
}),
}
}
#[test]
fn schema_round_trips_through_json() {
let s = comment_schema();
let serialised = serde_json::to_string(&s).unwrap();
let back: Schema = serde_json::from_str(&serialised).unwrap();
assert_eq!(back, s);
}
#[test]
fn capabilities_is_pure_for_zero_fs_zero_net() {
assert!(Capabilities::default().is_pure());
assert!(!Capabilities {
fs: true,
net: false
}
.is_pure());
assert!(!Capabilities {
fs: false,
net: true
}
.is_pure());
}
#[test]
fn hookset_default_is_all_off() {
let hs = HookSet::default();
assert!(!hs.validate);
assert!(!hs.resolve);
assert!(!hs.ir_build);
assert!(hs.render.is_empty());
}
#[test]
fn hookset_ir_build_round_trips_through_json() {
let hs = HookSet {
ir_build: true,
..HookSet::default()
};
let serialised = serde_json::to_string(&hs).unwrap();
assert!(
serialised.contains("\"ir_build\":true"),
"ir_build must serialise: {serialised}"
);
let back: HookSet = serde_json::from_str(&serialised).unwrap();
assert!(back.ir_build);
let legacy = r#"{"label":false,"validate":false,"resolve":false,"hover":false,"completion":false,"code_action":false}"#;
let parsed: HookSet = serde_json::from_str(legacy).unwrap();
assert!(
!parsed.ir_build,
"legacy JSON must default ir_build to false"
);
}
#[test]
fn body_shape_default_is_none_optional() {
let bs = BodyShape::default();
assert_eq!(bs.kind, BodyKind::None);
assert_eq!(bs.presence, BodyPresence::Optional);
}
}