//! Схема манифеста плагина (plugin-sdk §2). На старте — простая форма списка интентов //! (i18n-ready, не i18n-now: per-locale секции — позже). Валидатор (домен F, План 5) добавляет //! правила поверх схемы (квоты, каталог сигналов, закрытый `ru.shturman.*`). use serde::Deserialize; /// Корень манифеста (`manifest.yaml`). #[derive(Debug, Clone, Deserialize)] #[serde(deny_unknown_fields)] pub struct Manifest { pub plugin: Plugin, #[serde(default)] pub capabilities: Capabilities, #[serde(default)] pub extension_points: ExtensionPoints, } #[derive(Debug, Clone, Deserialize)] #[serde(deny_unknown_fields)] pub struct Plugin { /// reverse-DNS id; `ru.shturman.*` закрыт за first-party (проверяет валидатор, F §3). pub id: String, pub name: String, pub version: String, #[serde(default)] pub description: String, #[serde(default)] pub author: String, /// Мажорная версия API (совместимость, plugin-sdk §7). pub shturman_api: String, } #[derive(Debug, Clone, Default, Deserialize)] #[serde(deny_unknown_fields)] pub struct Capabilities { /// Только сигналы из каталога data-model (проверяет валидатор). #[serde(default)] pub vehicle_read: Vec, #[serde(default)] pub assistant_intents: Vec, #[serde(default)] pub ui_tiles: u32, #[serde(default)] pub ui_screens: u32, #[serde(default)] pub storage: bool, #[serde(default)] pub network: Option, } #[derive(Debug, Clone, Deserialize)] #[serde(deny_unknown_fields)] pub struct Network { /// host-allowlist (намерение; строгая гарантия — forced-proxy, plugin-sdk §3). #[serde(default)] pub hosts: Vec, } #[derive(Debug, Clone, Default, Deserialize)] #[serde(deny_unknown_fields)] pub struct ExtensionPoints { #[serde(default)] pub tiles: Vec, #[serde(default)] pub intents: Option, } #[derive(Debug, Clone, Deserialize)] #[serde(deny_unknown_fields)] pub struct Tile { pub id: String, #[serde(default)] pub title: String, } #[derive(Debug, Clone, Deserialize)] #[serde(deny_unknown_fields)] pub struct Intents { /// Путь объекта `IntentHandler` (все фразы → сюда, plugin-sdk §2). pub handler: String, } impl Manifest { /// Распарсить манифест из YAML (schema-уровень; правила — у валидатора). pub fn from_yaml(s: &str) -> Result { serde_yaml::from_str(s) } } #[cfg(test)] mod tests { use super::*; const GOOD: &str = r#" plugin: id: dev.example.fuel-tracker name: "Учёт расхода" version: 0.1.0 description: "Пробег и средний расход" author: "Имя" shturman_api: "1" capabilities: vehicle_read: [speed, maf, fuel_level, fuel_rate] assistant_intents: - "сколько я проехал" - "средний расход" ui_tiles: 1 ui_screens: 1 storage: true extension_points: tiles: - id: consumption title: "Расход" intents: handler: /dev/example/fuel_tracker/intents "#; #[test] fn parses_valid_manifest() { let m = Manifest::from_yaml(GOOD).unwrap(); assert_eq!(m.plugin.id, "dev.example.fuel-tracker"); assert_eq!(m.plugin.shturman_api, "1"); assert_eq!( m.capabilities.vehicle_read, vec!["speed", "maf", "fuel_level", "fuel_rate"] ); assert_eq!(m.capabilities.ui_tiles, 1); assert!(m.capabilities.storage); assert_eq!(m.extension_points.tiles.len(), 1); assert_eq!(m.extension_points.tiles[0].id, "consumption"); } #[test] fn rejects_missing_required_id() { let bad = r#" plugin: name: "x" version: 0.1.0 shturman_api: "1" "#; assert!(Manifest::from_yaml(bad).is_err()); } #[test] fn rejects_malformed_yaml() { assert!(Manifest::from_yaml(": : not yaml : :").is_err()); } #[test] fn rejects_unknown_field() { // опечатка в ключе capability на границе доверия (F §3) не должна молча проглатываться let bad = r#" plugin: id: dev.example.x name: "x" version: 0.1.0 shturman_api: "1" capabilities: vehicl_read: [speed] "#; assert!(Manifest::from_yaml(bad).is_err()); } }