first using ndpc2 as preset collection [refs #19]

This commit is contained in:
Thomas Weinhold 2026-01-05 22:01:50 +01:00
commit 9283396661
11 changed files with 303 additions and 49 deletions

View file

@ -2,11 +2,14 @@ use std::collections::HashMap;
use std::future::Future; use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use serde::de::IntoDeserializer;
use serde::Serialize;
use sqlx::{Row, SqlitePool}; use sqlx::{Row, SqlitePool};
use tokio::sync::{RwLock, RwLockReadGuard}; use tokio::sync::{RwLock, RwLockReadGuard};
use utoipa::gen::serde_json; use utoipa::gen::serde_json;
use sslo_lib::error::SsloError; use sslo_lib::error::SsloError;
use sslo_lib::db::{IndexedDataRow, OrderBy, WhereFieldsEqual}; use sslo_lib::db::{IndexedDataRow, OrderBy, WhereFieldsEqual};
use sslo_lib::ndpc2::collection::NdpcCollection;
use sslo_lib::ndpc::collection::{Collection, CollectionLike}; use sslo_lib::ndpc::collection::{Collection, CollectionLike};
use crate::db2::configuration::ConfigurationDbData; use crate::db2::configuration::ConfigurationDbData;
use crate::db2::{DatabaseManager, DatabaseManagerData}; use crate::db2::{DatabaseManager, DatabaseManagerData};
@ -196,7 +199,7 @@ impl PresetItem {
} }
/// returns a copy of the NDPC preset /// returns a copy of the NDPC preset
pub fn npdc(&self) -> Pin<Box<dyn Future<Output = Box<CollPreset>> + Send + '_>> { pub fn npdc(&self) -> Pin<Box<dyn Future<Output = CollPreset> + Send + '_>> {
Box::pin(async move { Box::pin(async move {
@ -223,11 +226,7 @@ impl PresetItem {
None => {} None => {}
Some(row) => match row.ndpc.as_ref() { Some(row) => match row.ndpc.as_ref() {
None => {}, None => {},
Some(ndpc_str) => { Some(ndpc_str) => ndpc.state_import_str(ndpc_str),
if let Err(e) = ndpc.state_import_json_str(&ndpc_str) {
log::error!("Failed applying ndpc string from database {}", e);
}
}
} }
} }
} }
@ -235,7 +234,7 @@ impl PresetItem {
// link parent collection // link parent collection
let parent = self.parent().await; let parent = self.parent().await;
if let Some(parent) = parent { if let Some(parent) = parent {
let pc: Box<dyn CollectionLike> = parent.npdc().await; let pc: CollPreset = parent.npdc().await;
ndpc.derive_from(&pc); ndpc.derive_from(&pc);
} }
ndpc ndpc
@ -243,12 +242,12 @@ impl PresetItem {
} }
/// Extract values from the given ndpc object and store it into database /// Extract values from the given ndpc object and store it into database
pub async fn ndpc_import(&self, ndpc: Box<CollPreset>) -> Result<(), SsloError> { pub async fn ndpc_import(&self, ndpc: CollPreset) -> Result<(), SsloError> {
let mut item_data = self.0.write().await; let mut item_data = self.0.write().await;
let pool = item_data.pool.clone(); let pool = item_data.pool.clone();
match item_data.row.as_mut() { match item_data.row.as_mut() {
Some(row) => { Some(row) => {
row.ndpc = Some(ndpc.state_export_json_str()); row.ndpc = Some(ndpc.state_export_str());
let res = row.upsert(&pool).await; let res = row.upsert(&pool).await;
log::info!("Updating {}", row.display()); log::info!("Updating {}", row.display());
res res

View file

@ -4,7 +4,7 @@ use axum::extract::{Path, State};
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::{Html, Response}; use axum::response::{Html, Response};
use sslo_lib::html_modal::{Dialog, DialogButtonStyle}; use sslo_lib::html_modal::{Dialog, DialogButtonStyle};
use sslo_lib::ndpc::collection::CollectionLike; use sslo_lib::ndpc2::collection::NdpcCollection;
use crate::app_state::AppState; use crate::app_state::AppState;
use crate::db2::configuration::presets::{PresetItem, PresetItemOrder, PresetTable}; use crate::db2::configuration::presets::{PresetItem, PresetItemOrder, PresetTable};
use crate::http::HtmlTemplate; use crate::http::HtmlTemplate;
@ -86,8 +86,8 @@ async fn generate_html(app_state: &AppState,
dia.add_button(DialogButtonStyle::Save, "Save Preset".to_string(), "savePreset".to_string()); dia.add_button(DialogButtonStyle::Save, "Save Preset".to_string(), "savePreset".to_string());
dia.add_button(DialogButtonStyle::Cancel, "Cancel Editing".to_string(), "cancelPresetEdit".to_string()); dia.add_button(DialogButtonStyle::Cancel, "Cancel Editing".to_string(), "cancelPresetEdit".to_string());
if let Some(preset) = &preset { if let Some(preset) = &preset {
let coll: Box<dyn CollectionLike> = preset.npdc().await; let coll = preset.npdc().await;
dia.push_form(&coll.html("")); dia.push_form(&coll.html());
} }
html.push_body(&dia.to_str()); html.push_body(&dia.to_str());
} }

View file

@ -5,17 +5,18 @@ use axum::response::{IntoResponse, Response};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::task::id; use tokio::task::id;
use utoipa::ToSchema; use utoipa::ToSchema;
use sslo_lib::ndpc::collection::{Collection, CollectionLike}; use sslo_lib::ndpc2::collection::{NdpcCollection, NdpcCollectionIO};
use sslo_lib::ndpc::parameter::{ParameterLike, StateIO};
use sslo_lib::rest_api::ErrorResponse; use sslo_lib::rest_api::ErrorResponse;
use crate::app_state::AppState; use crate::app_state::AppState;
use crate::db2::configuration::presets::PresetItem; use crate::db2::configuration::presets::PresetItem;
use crate::http::http_user::HttpUserExtractor; use crate::http::http_user::HttpUserExtractor;
use crate::ndpc::coll_preset::CollPreset;
#[derive(ToSchema)] #[derive(ToSchema)]
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct PatchPresetRequest (Vec<StateIO>); pub struct PatchPresetRequest {
ndpc: Option<NdpcCollectionIO>,
}
#[derive(ToSchema)] #[derive(ToSchema)]
#[derive(Deserialize)] #[derive(Deserialize)]
@ -84,7 +85,7 @@ pub async fn handler_get(State(app_state): State<AppState>,
let resp = GetPresetResponse { let resp = GetPresetResponse {
id: preset.id().await, id: preset.id().await,
name: preset.name().await, name: preset.name().await,
html_edit: collection.html("PresetCollection"), html_edit: collection.html(),
html_view: collection.html_view(), html_view: collection.html_view(),
}; };
(StatusCode::OK, Json(resp)).into_response() (StatusCode::OK, Json(resp)).into_response()
@ -106,7 +107,7 @@ pub async fn handler_get(State(app_state): State<AppState>,
pub async fn handler_patch(State(app_state): State<AppState>, pub async fn handler_patch(State(app_state): State<AppState>,
HttpUserExtractor(http_user): HttpUserExtractor, HttpUserExtractor(http_user): HttpUserExtractor,
Path(id): Path<i64>, Path(id): Path<i64>,
Json(parameters): Json<PatchPresetRequest>, Json(request): Json<PatchPresetRequest>,
) -> Response { ) -> Response {
// get basics // get basics
@ -129,7 +130,10 @@ pub async fn handler_patch(State(app_state): State<AppState>,
// patch collection // patch collection
let mut collection = preset.npdc().await; let mut collection = preset.npdc().await;
collection.state_import(parameters.0); if let Some(ndpc_io) = request.ndpc {
collection.state_import(&ndpc_io);
}
// collection.state_import(&request);
// save to preset // save to preset
match preset.ndpc_import(collection).await { match preset.ndpc_import(collection).await {

View file

@ -1,4 +1,6 @@
use axum::response::Html; use axum::response::Html;
use sslo_lib::ndpc2::collection::NdpcCollection;
use sslo_lib::ndpc2::parameter::{NdpcParameter, NdpcParameterMetaData};
use sslo_lib::ndpc::collection::{Collection, CollectionLike, CollectionMetaData}; use sslo_lib::ndpc::collection::{Collection, CollectionLike, CollectionMetaData};
use sslo_lib::ndpc::param_enum_single::ParamEnumSingle; use sslo_lib::ndpc::param_enum_single::ParamEnumSingle;
use sslo_lib::ndpc::param_i64::ParamI64; use sslo_lib::ndpc::param_i64::ParamI64;
@ -189,35 +191,22 @@ macro_rules! make_weather {
pub struct CollPreset { pub struct CollPreset {
meta: CollectionMetaData, meta: NdpcParameterMetaData,
} label: String,
impl CollectionLike for CollPreset {
fn meta(&self) -> &CollectionMetaData { &self.meta }
fn meta_mut(&mut self) -> &mut CollectionMetaData { &mut self.meta}
} }
impl CollPreset { impl CollPreset {
pub async fn new(db: Option<DatabaseManager>, label: String) -> Box<Self> {
// instantiate the collection pub async fn new(db: Option<DatabaseManager>, label: String) -> Self {
let mut preset = Box::new(Self { Self {
meta: CollectionMetaData::new("preset", label), meta: NdpcParameterMetaData::new(),
}); label,
}
preset.add_collection(get_general());
preset.add_collection(get_rating());
preset.add_collection(get_server());
preset.add_collection(get_session());
preset.add_collection(get_car_classes(db).await);
preset.add_collection(get_weather());
preset
} }
pub fn html_view(&self) -> String { pub fn html_view(&self) -> String {
let mut html = "<div class=\"NdpcCollPresetView\">".to_string(); let mut html = "<div class=\"NdpcCollPresetView\">".to_string();
html += &format!("<div class=\"NdpcCollPresetViewTitle\">{}</div>", self.meta.label); html += &format!("<div class=\"NdpcCollPresetViewTitle\">{}</div>", self.label);
// general // general
html+= "<div>"; html+= "<div>";
@ -229,6 +218,27 @@ impl CollPreset {
} }
} }
impl NdpcCollection for CollPreset {
fn ucid(&self) -> &'static str { "preset" }
fn label(&self) -> &str { &self.label }
fn child_parameters(&self) -> Vec<&dyn NdpcParameter> {
vec![]
}
fn child_parameters_mut(&mut self) -> Vec<&mut dyn NdpcParameter> {
vec![]
}
fn child_collections(&self) -> Vec<&dyn NdpcCollection> {
vec![]
}
fn child_collections_mut(&mut self) -> Vec<&mut dyn NdpcCollection> {
vec![]
}
}
async fn get_car_classes(db: Option<DatabaseManager>) -> Box<Collection> { async fn get_car_classes(db: Option<DatabaseManager>) -> Box<Collection> {

View file

@ -1,4 +1,5 @@
use sslo_lib::error::SsloError; use sslo_lib::error::SsloError;
use sslo_lib::ndpc2::parameter::{NdpcParameter, NdpcParameterMetaData};
use sslo_lib::ndpc::parameter::{ParameterLike, ParameterMetaData}; use sslo_lib::ndpc::parameter::{ParameterLike, ParameterMetaData};
use crate::db2::content::classes::ClassesTable; use crate::db2::content::classes::ClassesTable;
use crate::db2::DatabaseManager; use crate::db2::DatabaseManager;
@ -92,4 +93,41 @@ impl ParameterLike for ParamCarClasses {
} }
Err(SsloError::NdpcValueMismatch(self.meta.upid().to_string(), value)) Err(SsloError::NdpcValueMismatch(self.meta.upid().to_string(), value))
} }
} }
// impl NdpcParameter for ParamCarClasses {
//
// fn upid(&self) -> &'static str { self.upid}
// fn meta(&self) -> &NdpcParameterMetaData {&self.meta}
// fn meta_mut(&mut self) -> &mut NdpcParameterMetaData {&mut self.meta}
// fn label(&self) -> &str { "Car Class" }
// fn description(&self) -> &str {"Select here a car class to drive"}
// fn unit(&self) -> Option<&str> {None}
//
// fn html_input(&self) -> String {
//
// let options: Vec<String> = self.classes.0.iter()
// .map(|item| {
// let selected = match item.0 == self.selected_class {true => " selected=\"true\"", false => ""};
// format!("<option value=\"{}\"{}/>{}</option>", item.0, selected, item.1)
// }).collect();
//
// format!("<select>{}</select>", options.join(""))
// }
//
// fn value_from_string(&mut self, value: &str) -> Result<(), SsloError> {
// match value.parse::<i64>() {
// Ok(mut value) => {
// self.selected_class = value;
// Ok(())
// },
// Err(e) => {
// return Err(SsloError::NdpcValueMismatch(self.upid().to_string(), value.to_string()))
// }
// }
// }
//
// fn value2str(&self) -> String {
// self.selected_class.to_string()
// }
// }

View file

@ -1,11 +1,14 @@
pub mod collection; pub mod collection;
pub mod parameter; pub mod parameter;
mod param_str;
mod param_i64;
mod param_enum;
use crate::error::SsloError; use crate::error::SsloError;
use crate::ndpc2::parameter::{NdpcParaMetaData, NdpcParameter, NdpcParameterIO}; use crate::ndpc2::parameter::{NdpcParameterMetaData, NdpcParameter, NdpcParameterIO};
struct Parameter1 { struct Parameter1 {
meta: NdpcParaMetaData, meta: NdpcParameterMetaData,
value: String, value: String,
} }
impl NdpcParameter for Parameter1 { impl NdpcParameter for Parameter1 {
@ -13,8 +16,8 @@ impl NdpcParameter for Parameter1 {
fn label(&self) -> &str {"Parameter 1"} fn label(&self) -> &str {"Parameter 1"}
fn description(&self) -> &str {"This is only for testing"} fn description(&self) -> &str {"This is only for testing"}
fn unit(&self) -> Option<&str> {None} fn unit(&self) -> Option<&str> {None}
fn meta(&self) -> &NdpcParaMetaData {&self.meta} fn meta(&self) -> &NdpcParameterMetaData {&self.meta}
fn meta_mut(&mut self) -> &mut NdpcParaMetaData {&mut self.meta} fn meta_mut(&mut self) -> &mut NdpcParameterMetaData {&mut self.meta}
fn value_from_string(&mut self, value: &str) -> Result<(), SsloError> { fn value_from_string(&mut self, value: &str) -> Result<(), SsloError> {
self.value = value.to_string(); self.value = value.to_string();

View file

@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use utoipa::gen::serde_json;
use utoipa::ToSchema; use utoipa::ToSchema;
use super::parameter::{NdpcParameter, NdpcParameterIO}; use super::parameter::{NdpcParameter, NdpcParameterIO};
@ -6,8 +7,10 @@ use super::parameter::{NdpcParameter, NdpcParameterIO};
#[derive(ToSchema)] #[derive(ToSchema)]
pub struct NdpcCollectionIO { pub struct NdpcCollectionIO {
ucid: String, ucid: String,
child_parameters: Vec<NdpcParameterIO>,
#[schema(no_recursion)] // avoid recursion within utoipa
child_collections: Vec<NdpcCollectionIO>, child_collections: Vec<NdpcCollectionIO>,
child_parameters: Vec<NdpcParameterIO>
} }
pub trait NdpcCollection : Send { pub trait NdpcCollection : Send {
@ -97,6 +100,21 @@ pub trait NdpcCollection : Send {
} }
} }
fn state_export_str(&self) -> String {
serde_json::to_string(&self.state_export()).unwrap_or_else(|e| {
log::error!("Failed to serialize ucid={}: {}", self.ucid(), e);
return "".to_string();
})
}
fn state_import_str(&mut self, string: &str) {
let io: NdpcCollectionIO = match serde_json::from_str(string) {
Ok(io) => io,
Err(e) => return,
};
self.state_import(&io);
}
fn state_import(&mut self, io: &NdpcCollectionIO) { fn state_import(&mut self, io: &NdpcCollectionIO) {
debug_assert!(io.ucid == self.ucid()); debug_assert!(io.ucid == self.ucid());

View file

@ -0,0 +1,63 @@
use crate::error::SsloError;
use crate::ndpc2::parameter::{NdpcParameter, NdpcParameterMetaData};
pub struct ParamEnumStaticStr {
upid: &'static str,
label: String,
description: String,
unit: Option<String>,
meta: NdpcParameterMetaData,
enum_items: Vec<&'static str>,
selected_item: usize,
}
impl ParamEnumStaticStr {
pub fn new(upid: &'static str, label: String, unit: Option<String>, enum_items: Vec<&'static str>, description: String) -> Self { Self {
upid,
label,
description,
unit,
meta: NdpcParameterMetaData::new(),
enum_items,
selected_item: 0,
} }
pub fn value(&self) -> &'static str { self.enum_items.get(self.selected_item).unwrap_or(&"") }
}
impl NdpcParameter for ParamEnumStaticStr {
fn upid(&self) -> &'static str { self.upid}
fn label(&self) -> &str { &self.label}
fn description(&self) -> &str { &self.description }
fn unit(&self) -> Option<&str> { match &self.unit {None=>None, Some(s)=>Some(s)} }
fn meta(&self) -> &NdpcParameterMetaData { &self.meta}
fn meta_mut(&mut self) -> &mut NdpcParameterMetaData { &mut self.meta }
fn value_from_string(&mut self, value: &str) -> Result<(), SsloError> {
match self.enum_items.iter().position(|&item| item == value) {
Some(pos) => {
self.selected_item = pos;
Ok(())
},
None => {
log::warn!("Cannot concert value={} to a valid enum item within upid={}", value, self.upid);
Err(SsloError::NdpcValueMismatch(self.upid.to_string(), value.to_string()))
}
}
}
fn value2str(&self) -> String {
self.enum_items.get(self.selected_item).unwrap_or(&"").to_string()
}
fn html_input(&self) -> String {
let options: Vec<String> = self.enum_items.iter().enumerate().map(|(index, item)| {
let selected = match self.selected_item == index {true => " selected=\"true\"", false => ""};
format!("<option value=\"{}\"{}/>{}</option>", item, selected, item)
}).collect();
format!("<select>{}</select>", options.join(""))
}
}

View file

@ -0,0 +1,65 @@
use crate::error::SsloError;
use crate::ndpc2::parameter::{NdpcParameter, NdpcParameterMetaData};
pub struct ParamI64 {
upid: &'static str,
label: String,
description: String,
unit: Option<String>,
meta: NdpcParameterMetaData,
value: i64,
value_min: i64,
value_max: i64,
}
impl ParamI64 {
pub fn new(upid: &'static str, label: String, unit: Option<String>, default: i64, min: i64, max: i64, description: String) -> Self { Self {
upid,
label,
description,
unit,
meta: NdpcParameterMetaData::new(),
value: default,
value_min: min,
value_max: max,
} }
pub fn value(&self) -> i64 { self.value }
}
impl NdpcParameter for ParamI64 {
fn upid(&self) -> &'static str { self.upid}
fn label(&self) -> &str { &self.label}
fn description(&self) -> &str { &self.description }
fn unit(&self) -> Option<&str> { match &self.unit {None=>None, Some(s)=>Some(s)} }
fn meta(&self) -> &NdpcParameterMetaData { &self.meta}
fn meta_mut(&mut self) -> &mut NdpcParameterMetaData { &mut self.meta }
fn value_from_string(&mut self, value: &str) -> Result<(), SsloError> {
match value.parse::<i64>() {
Ok(mut value) => {
if value < self.value_min {
log::warn!("Clipping value={} to allowed minimum={} for upid={}", value, self.value_min, self.upid);
value = self.value_min;
} else if value > self.value_max {
log::warn!("Clipping value={} to allowed maximum={} for upid={}", value, self.value_max, self.upid);
value = self.value_max;
}
self.value = value; Ok(())
},
Err(e) => {
Err(SsloError::NdpcValueMismatch(self.upid.to_string(), value.to_string()))
}
}
}
fn value2str(&self) -> String {
self.value.to_string()
}
fn html_input(&self) -> String {
format!("<input type=\"number\" value=\"{}\" min=\"{}\" max=\"{}\"/>",
self.value, self.value_min, self.value_max,
)
}
}

View file

@ -0,0 +1,46 @@
use crate::error::SsloError;
use crate::ndpc2::parameter::{NdpcParameter, NdpcParameterMetaData};
pub struct ParamString {
upid: &'static str,
label: String,
description: String,
unit: Option<String>,
meta: NdpcParameterMetaData,
value: String,
}
impl ParamString {
pub fn new(upid: &'static str, label: String, unit: Option<String>, default: String, description: String) -> Self { Self {
upid,
label,
description,
unit,
meta: NdpcParameterMetaData::new(),
value: default,
} }
pub fn value(&self) -> &str { &self.value }
}
impl NdpcParameter for ParamString {
fn upid(&self) -> &'static str { self.upid}
fn label(&self) -> &str { &self.label}
fn description(&self) -> &str { &self.description }
fn unit(&self) -> Option<&str> { match &self.unit {None=>None, Some(s)=>Some(s)} }
fn meta(&self) -> &NdpcParameterMetaData { &self.meta}
fn meta_mut(&mut self) -> &mut NdpcParameterMetaData { &mut self.meta }
fn value_from_string(&mut self, value: &str) -> Result<(), SsloError> {
self.value = value.to_string();
Ok(())
}
fn value2str(&self) -> String {
self.value.clone()
}
fn html_input(&self) -> String {
format!("<input type=\"text\" value=\"{}\"/>", self.value)
}
}

View file

@ -33,12 +33,20 @@ pub struct NdpcParameterIO {
pub inheritance: Option<NdpcValueInheritance>, pub inheritance: Option<NdpcValueInheritance>,
} }
pub struct NdpcParaMetaData { pub struct NdpcParameterMetaData {
access: NdpcAccess, access: NdpcAccess,
inheritance: NdpcValueInheritance, inheritance: NdpcValueInheritance,
ancestor: Option<NdpcAncestor> ancestor: Option<NdpcAncestor>
} }
impl NdpcParameterMetaData {
pub fn new() -> Self { Self {
access: NdpcAccess::Locked,
inheritance: NdpcValueInheritance::Inherit,
ancestor: None,
}}
}
pub trait NdpcParameter : Send { pub trait NdpcParameter : Send {
/// Every parameter shall have a unique parameter id /// Every parameter shall have a unique parameter id
@ -53,8 +61,8 @@ pub trait NdpcParameter : Send {
/// An optional unit for the parameter /// An optional unit for the parameter
fn unit(&self) -> Option<&str>; fn unit(&self) -> Option<&str>;
fn meta(&self) -> &NdpcParaMetaData; fn meta(&self) -> &NdpcParameterMetaData;
fn meta_mut(&mut self) -> &mut NdpcParaMetaData; fn meta_mut(&mut self) -> &mut NdpcParameterMetaData;
/// Shall set the parameter value /// Shall set the parameter value
/// If the passed value is invalid, the parameter shall remain unchanged /// If the passed value is invalid, the parameter shall remain unchanged