feat(sdk): схема манифеста (plugin-sdk §2)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Alexander <akotenev2003@gmail.com>
This commit is contained in:
2026-06-24 12:13:13 +03:00
parent d8eebc96ce
commit 297970901a
4 changed files with 160 additions and 1 deletions
+1 -1
View File
@@ -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"
+12
View File
@@ -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" }
+6
View File
@@ -0,0 +1,6 @@
//! Публичный SDK Штурмана (F01): клиентские обёртки над `ipc`-proxy + схема манифеста.
//! First-party апы и сторонние плагины строятся на нём (architecture §1, principles #9, plugin-sdk).
pub mod manifest;
pub use manifest::Manifest;
+141
View File
@@ -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<String>,
#[serde(default)]
pub assistant_intents: Vec<String>,
#[serde(default)]
pub ui_tiles: u32,
#[serde(default)]
pub ui_screens: u32,
#[serde(default)]
pub storage: bool,
#[serde(default)]
pub network: Option<Network>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Network {
/// host-allowlist (намерение; строгая гарантия — forced-proxy, plugin-sdk §3).
#[serde(default)]
pub hosts: Vec<String>,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct ExtensionPoints {
#[serde(default)]
pub tiles: Vec<Tile>,
#[serde(default)]
pub intents: Option<Intents>,
}
#[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<Self, serde_yaml::Error> {
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());
}
}