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::pin::Pin;
use std::sync::{Arc, Weak};
use serde::de::IntoDeserializer;
use serde::Serialize;
use sqlx::{Row, SqlitePool};
use tokio::sync::{RwLock, RwLockReadGuard};
use utoipa::gen::serde_json;
use sslo_lib::error::SsloError;
use sslo_lib::db::{IndexedDataRow, OrderBy, WhereFieldsEqual};
use sslo_lib::ndpc2::collection::NdpcCollection;
use sslo_lib::ndpc::collection::{Collection, CollectionLike};
use crate::db2::configuration::ConfigurationDbData;
use crate::db2::{DatabaseManager, DatabaseManagerData};
@ -196,7 +199,7 @@ impl PresetItem {
}
/// 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 {
@ -223,11 +226,7 @@ impl PresetItem {
None => {}
Some(row) => match row.ndpc.as_ref() {
None => {},
Some(ndpc_str) => {
if let Err(e) = ndpc.state_import_json_str(&ndpc_str) {
log::error!("Failed applying ndpc string from database {}", e);
}
}
Some(ndpc_str) => ndpc.state_import_str(ndpc_str),
}
}
}
@ -235,7 +234,7 @@ impl PresetItem {
// link parent collection
let parent = self.parent().await;
if let Some(parent) = parent {
let pc: Box<dyn CollectionLike> = parent.npdc().await;
let pc: CollPreset = parent.npdc().await;
ndpc.derive_from(&pc);
}
ndpc
@ -243,12 +242,12 @@ impl PresetItem {
}
/// 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 pool = item_data.pool.clone();
match item_data.row.as_mut() {
Some(row) => {
row.ndpc = Some(ndpc.state_export_json_str());
row.ndpc = Some(ndpc.state_export_str());
let res = row.upsert(&pool).await;
log::info!("Updating {}", row.display());
res

View file

@ -4,7 +4,7 @@ use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::response::{Html, Response};
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::db2::configuration::presets::{PresetItem, PresetItemOrder, PresetTable};
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::Cancel, "Cancel Editing".to_string(), "cancelPresetEdit".to_string());
if let Some(preset) = &preset {
let coll: Box<dyn CollectionLike> = preset.npdc().await;
dia.push_form(&coll.html(""));
let coll = preset.npdc().await;
dia.push_form(&coll.html());
}
html.push_body(&dia.to_str());
}

View file

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

View file

@ -1,4 +1,6 @@
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::param_enum_single::ParamEnumSingle;
use sslo_lib::ndpc::param_i64::ParamI64;
@ -189,35 +191,22 @@ macro_rules! make_weather {
pub struct CollPreset {
meta: CollectionMetaData,
}
impl CollectionLike for CollPreset {
fn meta(&self) -> &CollectionMetaData { &self.meta }
fn meta_mut(&mut self) -> &mut CollectionMetaData { &mut self.meta}
meta: NdpcParameterMetaData,
label: String,
}
impl CollPreset {
pub async fn new(db: Option<DatabaseManager>, label: String) -> Box<Self> {
// instantiate the collection
let mut preset = Box::new(Self {
meta: CollectionMetaData::new("preset", 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 async fn new(db: Option<DatabaseManager>, label: String) -> Self {
Self {
meta: NdpcParameterMetaData::new(),
label,
}
}
pub fn html_view(&self) -> 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
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> {

View file

@ -1,4 +1,5 @@
use sslo_lib::error::SsloError;
use sslo_lib::ndpc2::parameter::{NdpcParameter, NdpcParameterMetaData};
use sslo_lib::ndpc::parameter::{ParameterLike, ParameterMetaData};
use crate::db2::content::classes::ClassesTable;
use crate::db2::DatabaseManager;
@ -92,4 +93,41 @@ impl ParameterLike for ParamCarClasses {
}
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 parameter;
mod param_str;
mod param_i64;
mod param_enum;
use crate::error::SsloError;
use crate::ndpc2::parameter::{NdpcParaMetaData, NdpcParameter, NdpcParameterIO};
use crate::ndpc2::parameter::{NdpcParameterMetaData, NdpcParameter, NdpcParameterIO};
struct Parameter1 {
meta: NdpcParaMetaData,
meta: NdpcParameterMetaData,
value: String,
}
impl NdpcParameter for Parameter1 {
@ -13,8 +16,8 @@ impl NdpcParameter for Parameter1 {
fn label(&self) -> &str {"Parameter 1"}
fn description(&self) -> &str {"This is only for testing"}
fn unit(&self) -> Option<&str> {None}
fn meta(&self) -> &NdpcParaMetaData {&self.meta}
fn meta_mut(&mut self) -> &mut NdpcParaMetaData {&mut self.meta}
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();

View file

@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize};
use utoipa::gen::serde_json;
use utoipa::ToSchema;
use super::parameter::{NdpcParameter, NdpcParameterIO};
@ -6,8 +7,10 @@ use super::parameter::{NdpcParameter, NdpcParameterIO};
#[derive(ToSchema)]
pub struct NdpcCollectionIO {
ucid: String,
child_parameters: Vec<NdpcParameterIO>,
#[schema(no_recursion)] // avoid recursion within utoipa
child_collections: Vec<NdpcCollectionIO>,
child_parameters: Vec<NdpcParameterIO>
}
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) {
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 struct NdpcParaMetaData {
pub struct NdpcParameterMetaData {
access: NdpcAccess,
inheritance: NdpcValueInheritance,
ancestor: Option<NdpcAncestor>
}
impl NdpcParameterMetaData {
pub fn new() -> Self { Self {
access: NdpcAccess::Locked,
inheritance: NdpcValueInheritance::Inherit,
ancestor: None,
}}
}
pub trait NdpcParameter : Send {
/// Every parameter shall have a unique parameter id
@ -53,8 +61,8 @@ pub trait NdpcParameter : Send {
/// An optional unit for the parameter
fn unit(&self) -> Option<&str>;
fn meta(&self) -> &NdpcParaMetaData;
fn meta_mut(&mut self) -> &mut NdpcParaMetaData;
fn meta(&self) -> &NdpcParameterMetaData;
fn meta_mut(&mut self) -> &mut NdpcParameterMetaData;
/// Shall set the parameter value
/// If the passed value is invalid, the parameter shall remain unchanged