From 297970901ac18dde2c2de998814a153da94e4449 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 24 Jun 2026 12:13:13 +0300 Subject: [PATCH] =?UTF-8?q?feat(sdk):=20=D1=81=D1=85=D0=B5=D0=BC=D0=B0=20?= =?UTF-8?q?=D0=BC=D0=B0=D0=BD=D0=B8=D1=84=D0=B5=D1=81=D1=82=D0=B0=20(plugi?= =?UTF-8?q?n-sdk=20=C2=A72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 Signed-off-by: Alexander --- Cargo.toml | 2 +- crates/shturman-sdk/Cargo.toml | 12 +++ crates/shturman-sdk/src/lib.rs | 6 ++ crates/shturman-sdk/src/manifest.rs | 141 ++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 crates/shturman-sdk/Cargo.toml create mode 100644 crates/shturman-sdk/src/lib.rs create mode 100644 crates/shturman-sdk/src/manifest.rs diff --git a/Cargo.toml b/Cargo.toml index cd34518..be02a16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ resolver = "2" # Члены растут по планам реализации. crates/core, crates/apps, crates/tools — # группировка привилегированного ядра / first-party-апов / dev-инструментов (architecture §3). -members = ["crates/shturman-common", "crates/shturman-ipc"] +members = ["crates/shturman-common", "crates/shturman-ipc", "crates/shturman-sdk"] [workspace.package] edition = "2021" diff --git a/crates/shturman-sdk/Cargo.toml b/crates/shturman-sdk/Cargo.toml new file mode 100644 index 0000000..d1eca2c --- /dev/null +++ b/crates/shturman-sdk/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "shturman-sdk" +version = "0.0.0" +edition.workspace = true +license.workspace = true + +[dependencies] +zbus.workspace = true +serde.workspace = true +serde_yaml.workspace = true +tokio.workspace = true +shturman-ipc = { path = "../shturman-ipc" } diff --git a/crates/shturman-sdk/src/lib.rs b/crates/shturman-sdk/src/lib.rs new file mode 100644 index 0000000..f72b6db --- /dev/null +++ b/crates/shturman-sdk/src/lib.rs @@ -0,0 +1,6 @@ +//! Публичный SDK Штурмана (F01): клиентские обёртки над `ipc`-proxy + схема манифеста. +//! First-party апы и сторонние плагины строятся на нём (architecture §1, principles #9, plugin-sdk). + +pub mod manifest; + +pub use manifest::Manifest; diff --git a/crates/shturman-sdk/src/manifest.rs b/crates/shturman-sdk/src/manifest.rs new file mode 100644 index 0000000..5825bb5 --- /dev/null +++ b/crates/shturman-sdk/src/manifest.rs @@ -0,0 +1,141 @@ +//! Схема манифеста плагина (plugin-sdk §2). На старте — простая форма списка интентов +//! (i18n-ready, не i18n-now: per-locale секции — позже). Валидатор (домен F, План 5) добавляет +//! правила поверх схемы (квоты, каталог сигналов, закрытый `ru.shturman.*`). + +use serde::Deserialize; + +/// Корень манифеста (`manifest.yaml`). +#[derive(Debug, Clone, Deserialize)] +pub struct Manifest { + pub plugin: Plugin, + #[serde(default)] + pub capabilities: Capabilities, + #[serde(default)] + pub extension_points: ExtensionPoints, +} + +#[derive(Debug, Clone, Deserialize)] +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)] +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)] +pub struct Network { + /// host-allowlist (намерение; строгая гарантия — forced-proxy, plugin-sdk §3). + #[serde(default)] + pub hosts: Vec, +} + +#[derive(Debug, Clone, Default, Deserialize)] +pub struct ExtensionPoints { + #[serde(default)] + pub tiles: Vec, + #[serde(default)] + pub intents: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Tile { + pub id: String, + #[serde(default)] + pub title: String, +} + +#[derive(Debug, Clone, Deserialize)] +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()); + } +}