feat(validator): manifest-validator (F02) + bad-manifest фикстуры (F04)
Правила: id не ru.shturman.*, shturman_api поддержан, len(tiles)<=ui_tiles, vehicle_read из каталога.
Фикстуры good + bad-{id-reserved,api,tiles-quota,vehicle-read}; 6 тестов.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Alexander <akotenev2003@gmail.com>
This commit is contained in:
Generated
+7
@@ -3567,6 +3567,13 @@ dependencies = [
|
|||||||
"zbus 4.4.0",
|
"zbus 4.4.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shturman-manifest-validator"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"shturman-sdk",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shturman-power"
|
name = "shturman-power"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ members = [
|
|||||||
"crates/core/shturman-settings",
|
"crates/core/shturman-settings",
|
||||||
"crates/core/shturman-power",
|
"crates/core/shturman-power",
|
||||||
"crates/apps/shturman-shell",
|
"crates/apps/shturman-shell",
|
||||||
|
"crates/tools/shturman-manifest-validator",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "shturman-manifest-validator"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
shturman-sdk = { path = "../../shturman-sdk" }
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
//! Валидатор манифеста плагина (F02). `shturman-manifest-validator <manifest.yaml>`.
|
||||||
|
|
||||||
|
mod validate;
|
||||||
|
|
||||||
|
use std::process::ExitCode;
|
||||||
|
|
||||||
|
fn main() -> ExitCode {
|
||||||
|
let Some(path) = std::env::args().nth(1) else {
|
||||||
|
eprintln!("usage: shturman-manifest-validator <manifest.yaml>");
|
||||||
|
return ExitCode::from(2);
|
||||||
|
};
|
||||||
|
let yaml = match std::fs::read_to_string(&path) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("не прочитать {path}: {e}");
|
||||||
|
return ExitCode::from(2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match validate::validate_yaml(&yaml) {
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("✗ {e}");
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
Ok(errs) if errs.is_empty() => {
|
||||||
|
println!("✓ манифест валиден: {path}");
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
}
|
||||||
|
Ok(errs) => {
|
||||||
|
eprintln!("✗ нарушений: {}", errs.len());
|
||||||
|
for e in errs {
|
||||||
|
eprintln!(" - {e}");
|
||||||
|
}
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
//! Правила валидации манифеста поверх схемы (plugin-sdk §2/§3, F §3). Схема — `shturman_sdk::Manifest`.
|
||||||
|
|
||||||
|
use shturman_sdk::Manifest;
|
||||||
|
|
||||||
|
/// Каталог стандартных сигналов (data-model §4). `vehicle_read` допускает только их.
|
||||||
|
const VEHICLE_SIGNALS: &[&str] = &[
|
||||||
|
"speed",
|
||||||
|
"rpm",
|
||||||
|
"engine_load",
|
||||||
|
"coolant_temp",
|
||||||
|
"intake_temp",
|
||||||
|
"maf",
|
||||||
|
"throttle",
|
||||||
|
"intake_pressure",
|
||||||
|
"fuel_level",
|
||||||
|
"module_voltage",
|
||||||
|
"ambient_temp",
|
||||||
|
"oil_temp",
|
||||||
|
"fuel_rate",
|
||||||
|
"run_time",
|
||||||
|
"mil_on",
|
||||||
|
"dtc_count",
|
||||||
|
"distance_mil",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Поддерживаемые мажоры API (plugin-sdk §7).
|
||||||
|
const SUPPORTED_API: &[&str] = &["1"];
|
||||||
|
|
||||||
|
/// Проверить распарсенный манифест. Пустой вектор — валиден.
|
||||||
|
pub fn validate(m: &Manifest) -> Vec<String> {
|
||||||
|
let mut errs = Vec::new();
|
||||||
|
|
||||||
|
// id `ru.shturman.*` закрыт за first-party (F §3)
|
||||||
|
if m.plugin.id.starts_with("ru.shturman.") {
|
||||||
|
errs.push(format!(
|
||||||
|
"id '{}' использует зарезервированный префикс ru.shturman.* (закрыт за first-party)",
|
||||||
|
m.plugin.id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// shturman_api поддержан
|
||||||
|
if !SUPPORTED_API.contains(&m.plugin.shturman_api.as_str()) {
|
||||||
|
errs.push(format!(
|
||||||
|
"shturman_api '{}' не поддержан (поддерживаются: {SUPPORTED_API:?})",
|
||||||
|
m.plugin.shturman_api
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// квота тайлов: len(tiles) ≤ ui_tiles (plugin-sdk §2)
|
||||||
|
let tiles = m.extension_points.tiles.len() as u32;
|
||||||
|
if tiles > m.capabilities.ui_tiles {
|
||||||
|
errs.push(format!(
|
||||||
|
"тайлов {tiles} больше квоты ui_tiles {}",
|
||||||
|
m.capabilities.ui_tiles
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// vehicle_read только из каталога data-model
|
||||||
|
for sig in &m.capabilities.vehicle_read {
|
||||||
|
if !VEHICLE_SIGNALS.contains(&sig.as_str()) {
|
||||||
|
errs.push(format!("vehicle_read '{sig}' нет в каталоге data-model"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Распарсить YAML (схема) и проверить правила. `Err` — ошибка парсинга; `Ok(errs)` — нарушения правил.
|
||||||
|
pub fn validate_yaml(yaml: &str) -> Result<Vec<String>, String> {
|
||||||
|
let m = Manifest::from_yaml(yaml).map_err(|e| format!("парсинг манифеста: {e}"))?;
|
||||||
|
Ok(validate(&m))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn fixture(name: &str) -> String {
|
||||||
|
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/../../../fixtures/manifests/");
|
||||||
|
std::fs::read_to_string(format!("{dir}{name}"))
|
||||||
|
.unwrap_or_else(|e| panic!("фикстура {name}: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn good_passes() {
|
||||||
|
assert!(validate_yaml(&fixture("good.yaml")).unwrap().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reserved_id_rejected() {
|
||||||
|
let errs = validate_yaml(&fixture("bad-id-reserved.yaml")).unwrap();
|
||||||
|
assert!(errs.iter().any(|e| e.contains("ru.shturman")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unsupported_api_rejected() {
|
||||||
|
let errs = validate_yaml(&fixture("bad-api.yaml")).unwrap();
|
||||||
|
assert!(errs.iter().any(|e| e.contains("shturman_api")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tiles_over_quota_rejected() {
|
||||||
|
let errs = validate_yaml(&fixture("bad-tiles-quota.yaml")).unwrap();
|
||||||
|
assert!(errs.iter().any(|e| e.contains("квоты")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_vehicle_read_rejected() {
|
||||||
|
let errs = validate_yaml(&fixture("bad-vehicle-read.yaml")).unwrap();
|
||||||
|
assert!(errs.iter().any(|e| e.contains("vehicle_read")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn malformed_yaml_errors() {
|
||||||
|
assert!(validate_yaml("::: not yaml :::").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Неподдерживаемая мажорная версия API (plugin-sdk §7).
|
||||||
|
plugin:
|
||||||
|
id: dev.example.future
|
||||||
|
name: "Из будущего"
|
||||||
|
version: 0.1.0
|
||||||
|
shturman_api: "99"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# id в зарезервированном префиксе ru.shturman.* — закрыт за first-party (F §3).
|
||||||
|
plugin:
|
||||||
|
id: ru.shturman.evil
|
||||||
|
name: "Подделка"
|
||||||
|
version: 0.1.0
|
||||||
|
shturman_api: "1"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Тайлов больше, чем квота ui_tiles (plugin-sdk §2).
|
||||||
|
plugin:
|
||||||
|
id: dev.example.greedy
|
||||||
|
name: "Жадный"
|
||||||
|
version: 0.1.0
|
||||||
|
shturman_api: "1"
|
||||||
|
capabilities:
|
||||||
|
ui_tiles: 1
|
||||||
|
extension_points:
|
||||||
|
tiles:
|
||||||
|
- id: a
|
||||||
|
title: "A"
|
||||||
|
- id: b
|
||||||
|
title: "B"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# vehicle_read с сигналом вне каталога data-model.
|
||||||
|
plugin:
|
||||||
|
id: dev.example.bogus
|
||||||
|
name: "Выдумщик"
|
||||||
|
version: 0.1.0
|
||||||
|
shturman_api: "1"
|
||||||
|
capabilities:
|
||||||
|
vehicle_read: [rocket_boost]
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
plugin:
|
||||||
|
id: dev.example.fuel-tracker
|
||||||
|
name: "Учёт расхода"
|
||||||
|
version: 0.1.0
|
||||||
|
shturman_api: "1"
|
||||||
|
capabilities:
|
||||||
|
vehicle_read: [speed, maf, fuel_level, fuel_rate]
|
||||||
|
ui_tiles: 1
|
||||||
|
extension_points:
|
||||||
|
tiles:
|
||||||
|
- id: consumption
|
||||||
|
title: "Расход"
|
||||||
Reference in New Issue
Block a user