use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct HostManifest {
#[serde(default)]
pub externals: Vec<ManifestExternal>,
#[serde(default)]
pub types: Vec<SemanticTypeDef>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ManifestExternal {
pub name: String,
#[serde(default)]
pub params: Vec<ManifestParam>,
#[serde(default)]
pub returns: TypeRef,
#[serde(default)]
pub kind: ExternalKind,
#[serde(default)]
pub doc: Option<String>,
#[serde(default)]
pub widgets: Vec<ArgGroupWidget>,
#[serde(default)]
pub path: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArgGroupWidget {
pub group: Vec<u32>,
#[serde(rename = "type")]
pub ty: String,
#[serde(default)]
pub surface: Option<String>,
#[serde(default)]
pub context: std::collections::BTreeMap<String, u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ManifestParam {
pub name: String,
#[serde(default)]
pub ty: TypeRef,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SemanticTypeDef {
pub name: String,
pub base: BaseType,
#[serde(default)]
pub constraint: Option<Constraint>,
#[serde(default)]
pub values: Option<ValueSource>,
#[serde(default)]
pub widget: Option<WidgetDecl>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WidgetDecl {
pub kind: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ValueItem {
pub value: String,
pub label: String,
#[serde(default)]
pub detail: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "source")]
pub enum ValueSource {
Static { items: Vec<ValueItem> },
Host,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum Constraint {
Enum { values: Vec<String> },
Regex { pattern: String },
Range {
#[serde(default)]
min: Option<i64>,
#[serde(default)]
max: Option<i64>,
},
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct TypeRef(pub String);
impl TypeRef {
#[must_use]
pub fn is_unspecified(&self) -> bool {
self.0.trim().is_empty()
}
#[must_use]
pub fn as_base(&self) -> Option<BaseType> {
BaseType::from_keyword(self.0.trim())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BaseType {
String,
Int,
Float,
Bool,
Void,
}
impl BaseType {
#[must_use]
pub fn from_keyword(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"string" => Some(Self::String),
"int" => Some(Self::Int),
"float" => Some(Self::Float),
"bool" => Some(Self::Bool),
"void" => Some(Self::Void),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExternalKind {
Query,
Effect,
Presentation,
#[default]
Plain,
}
impl ExternalKind {
#[must_use]
pub fn from_tag(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"query" => Some(Self::Query),
"effect" => Some(Self::Effect),
"presentation" => Some(Self::Presentation),
"plain" => Some(Self::Plain),
_ => None,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DocBlock {
pub doc: Option<String>,
pub params: Vec<(String, TypeRef)>,
pub returns: Option<TypeRef>,
pub kind: Option<ExternalKind>,
}
#[cfg(test)]
mod value_source_tests {
use super::{SemanticTypeDef, ValueSource};
#[test]
fn static_value_source_json_roundtrip() {
let json = r#"{
"name": "switch_id",
"base": "int",
"values": { "source": "static", "items": [
{ "value": "5", "label": "HarborGate", "detail": "Switch #5" },
{ "value": "9", "label": "Vault" }
] }
}"#;
let def: SemanticTypeDef = serde_json::from_str(json).expect("parse");
let items = match def.values {
Some(ValueSource::Static { items }) => items,
_ => Vec::new(),
};
assert_eq!(items.len(), 2, "two static items parsed");
assert_eq!(items[0].value, "5");
assert_eq!(items[0].label, "HarborGate");
assert_eq!(items[0].detail.as_deref(), Some("Switch #5"));
assert_eq!(items[1].detail, None);
}
#[test]
fn host_value_source_and_omitted_values_parse() {
let host: SemanticTypeDef = serde_json::from_str(
r#"{ "name": "item_id", "base": "int", "values": { "source": "host" } }"#,
)
.expect("parse host");
assert!(matches!(host.values, Some(ValueSource::Host)));
let none: SemanticTypeDef =
serde_json::from_str(r#"{ "name": "x", "base": "int" }"#).expect("parse bare");
assert!(none.values.is_none());
}
}