use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub use crate::ipc_types::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SdkExtensionMetadata {
pub id: String,
pub name: String,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
#[serde(default)]
pub sdk_version: Option<String>,
#[serde(default = "default_extension_type")]
pub extension_type: String,
}
fn default_extension_type() -> String {
"native".to_string()
}
impl SdkExtensionMetadata {
pub fn new(id: impl Into<String>, name: impl Into<String>, version: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
version: version.into(),
description: None,
author: None,
sdk_version: Some(env!("CARGO_PKG_VERSION").to_string()),
extension_type: "native".to_string(),
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_author(mut self, author: impl Into<String>) -> Self {
self.author = Some(author.into());
self
}
pub fn with_type(mut self, extension_type: impl Into<String>) -> Self {
self.extension_type = extension_type.into();
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum SdkMetricDataType {
Float,
Integer,
Boolean,
#[default]
String,
Binary,
Enum {
options: Vec<String>,
},
}
impl From<SdkMetricDataType> for MetricDataType {
fn from(dt: SdkMetricDataType) -> Self {
match dt {
SdkMetricDataType::Float => MetricDataType::Float,
SdkMetricDataType::Integer => MetricDataType::Integer,
SdkMetricDataType::Boolean => MetricDataType::Boolean,
SdkMetricDataType::String => MetricDataType::String,
SdkMetricDataType::Binary => MetricDataType::Binary,
SdkMetricDataType::Enum { options } => MetricDataType::Enum { options },
}
}
}
impl From<MetricDataType> for SdkMetricDataType {
fn from(dt: MetricDataType) -> Self {
match dt {
MetricDataType::Float => SdkMetricDataType::Float,
MetricDataType::Integer => SdkMetricDataType::Integer,
MetricDataType::Boolean => SdkMetricDataType::Boolean,
MetricDataType::String => SdkMetricDataType::String,
MetricDataType::Binary => SdkMetricDataType::Binary,
MetricDataType::Enum { options } => SdkMetricDataType::Enum { options },
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SdkMetricDefinition {
pub name: String,
pub display_name: String,
pub data_type: SdkMetricDataType,
#[serde(default)]
pub unit: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub min: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max: Option<f64>,
#[serde(default)]
pub required: bool,
}
impl SdkMetricDefinition {
pub fn new(
name: impl Into<String>,
display_name: impl Into<String>,
data_type: SdkMetricDataType,
) -> Self {
Self {
name: name.into(),
display_name: display_name.into(),
data_type,
unit: String::new(),
min: None,
max: None,
required: false,
}
}
pub fn with_unit(mut self, unit: impl Into<String>) -> Self {
self.unit = unit.into();
self
}
pub fn with_min(mut self, min: f64) -> Self {
self.min = Some(min);
self
}
pub fn with_max(mut self, max: f64) -> Self {
self.max = Some(max);
self
}
pub fn with_required(mut self, required: bool) -> Self {
self.required = required;
self
}
}
impl From<SdkMetricDefinition> for MetricDescriptor {
fn from(def: SdkMetricDefinition) -> Self {
Self {
name: def.name,
display_name: def.display_name,
data_type: def.data_type.into(),
unit: def.unit,
min: def.min,
max: def.max,
required: def.required,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[derive(Default)]
pub enum SdkMetricValue {
Float(f64),
Integer(i64),
Boolean(bool),
String(String),
Binary(Vec<u8>),
#[default]
Null,
}
impl From<SdkMetricValue> for MetricValue {
fn from(v: SdkMetricValue) -> Self {
match v {
SdkMetricValue::Float(f) => MetricValue::Float(f),
SdkMetricValue::Integer(i) => MetricValue::Integer(i),
SdkMetricValue::Boolean(b) => MetricValue::Boolean(b),
SdkMetricValue::String(s) => MetricValue::String(s),
SdkMetricValue::Binary(b) => MetricValue::Binary(b),
SdkMetricValue::Null => MetricValue::Null,
}
}
}
impl From<MetricValue> for SdkMetricValue {
fn from(v: MetricValue) -> Self {
match v {
MetricValue::Float(f) => SdkMetricValue::Float(f),
MetricValue::Integer(i) => SdkMetricValue::Integer(i),
MetricValue::Boolean(b) => SdkMetricValue::Boolean(b),
MetricValue::String(s) => SdkMetricValue::String(s),
MetricValue::Binary(b) => SdkMetricValue::Binary(b),
MetricValue::Null => SdkMetricValue::Null,
}
}
}
impl From<f64> for SdkMetricValue {
fn from(v: f64) -> Self {
Self::Float(v)
}
}
impl From<i64> for SdkMetricValue {
fn from(v: i64) -> Self {
Self::Integer(v)
}
}
impl From<bool> for SdkMetricValue {
fn from(v: bool) -> Self {
Self::Boolean(v)
}
}
impl From<String> for SdkMetricValue {
fn from(v: String) -> Self {
Self::String(v)
}
}
impl From<&str> for SdkMetricValue {
fn from(v: &str) -> Self {
Self::String(v.to_string())
}
}
impl From<Vec<u8>> for SdkMetricValue {
fn from(v: Vec<u8>) -> Self {
Self::Binary(v)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SdkExtensionMetricValue {
pub name: String,
pub value: SdkMetricValue,
pub timestamp: i64,
}
impl SdkExtensionMetricValue {
pub fn new(name: impl Into<String>, value: SdkMetricValue) -> Self {
Self {
name: name.into(),
value,
timestamp: {
#[cfg(not(target_arch = "wasm32"))]
{
crate::ipc_types::current_timestamp_ms()
}
#[cfg(target_arch = "wasm32")]
{
crate::wasm::bindings::invoke_capability_raw(
"system_timestamp",
&serde_json::json!({}),
)
.ok()
.and_then(|v| v.get("timestamp_ms").and_then(|t| t.as_i64()))
.unwrap_or(0)
}
},
}
}
pub fn with_timestamp(name: impl Into<String>, value: SdkMetricValue, timestamp: i64) -> Self {
Self {
name: name.into(),
value,
timestamp,
}
}
}
impl From<SdkExtensionMetricValue> for ExtensionMetricValue {
fn from(v: SdkExtensionMetricValue) -> Self {
Self {
name: v.name,
value: v.value.into(),
timestamp: v.timestamp,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SdkParameterDefinition {
pub name: String,
#[serde(default)]
pub display_name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub param_type: SdkMetricDataType,
#[serde(default)]
pub required: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_value: Option<SdkMetricValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max: Option<f64>,
#[serde(default)]
pub options: Vec<String>,
}
impl SdkParameterDefinition {
pub fn new(name: impl Into<String>, param_type: SdkMetricDataType) -> Self {
Self {
name: name.into(),
display_name: String::new(),
description: String::new(),
param_type,
required: true,
default_value: None,
min: None,
max: None,
options: Vec::new(),
}
}
pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
self.display_name = display_name.into();
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
pub fn optional(mut self) -> Self {
self.required = false;
self
}
pub fn with_default(mut self, default: SdkMetricValue) -> Self {
self.default_value = Some(default);
self.required = false;
self
}
}
impl From<SdkParameterDefinition> for ParameterDefinition {
fn from(p: SdkParameterDefinition) -> Self {
Self {
name: p.name,
display_name: p.display_name,
description: p.description,
param_type: p.param_type.into(),
required: p.required,
default_value: p.default_value.map(|v| v.into()),
min: p.min,
max: p.max,
options: p.options,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SdkCommandDefinition {
pub name: String,
#[serde(default)]
pub display_name: String,
#[serde(default)]
pub payload_template: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub parameters: Vec<SdkParameterDefinition>,
#[serde(default)]
pub fixed_values: std::collections::HashMap<String, serde_json::Value>,
#[serde(default)]
pub samples: Vec<serde_json::Value>,
#[serde(default)]
pub parameter_groups: Vec<SdkParameterGroup>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SdkParameterGroup {
pub name: String,
#[serde(default)]
pub display_name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub parameters: Vec<String>,
}
impl SdkCommandDefinition {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
display_name: String::new(),
payload_template: String::new(),
description: String::new(),
parameters: Vec::new(),
fixed_values: std::collections::HashMap::new(),
samples: Vec::new(),
parameter_groups: Vec::new(),
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
pub fn param(mut self, param: SdkParameterDefinition) -> Self {
self.parameters.push(param);
self
}
}
impl From<SdkCommandDefinition> for CommandDescriptor {
fn from(c: SdkCommandDefinition) -> Self {
Self {
name: c.name,
display_name: c.display_name,
description: c.description,
payload_template: c.payload_template,
parameters: c.parameters.into_iter().map(|p| p.into()).collect(),
fixed_values: c.fixed_values,
samples: c.samples,
parameter_groups: c
.parameter_groups
.into_iter()
.map(|g| ParameterGroup {
name: g.name,
display_name: g.display_name,
description: g.description,
parameters: g.parameters,
})
.collect(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SdkExtensionError {
CommandNotFound(String),
InvalidArguments(String),
ExecutionFailed(String),
Timeout(String),
NotFound(String),
InvalidFormat(String),
LoadFailed(String),
SecurityError(String),
NotSupported(String),
ConfigurationError(String),
InternalError(String),
Other(String),
}
impl std::fmt::Display for SdkExtensionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CommandNotFound(cmd) => write!(f, "Command not found: {}", cmd),
Self::InvalidArguments(msg) => write!(f, "Invalid arguments: {}", msg),
Self::ExecutionFailed(msg) => write!(f, "Execution failed: {}", msg),
Self::Timeout(msg) => write!(f, "Timeout: {}", msg),
Self::NotFound(msg) => write!(f, "Not found: {}", msg),
Self::InvalidFormat(msg) => write!(f, "Invalid format: {}", msg),
Self::LoadFailed(msg) => write!(f, "Load failed: {}", msg),
Self::SecurityError(msg) => write!(f, "Security error: {}", msg),
Self::NotSupported(msg) => write!(f, "Not supported: {}", msg),
Self::ConfigurationError(msg) => write!(f, "Configuration error: {}", msg),
Self::InternalError(msg) => write!(f, "Internal error: {}", msg),
Self::Other(msg) => write!(f, "Error: {}", msg),
}
}
}
impl std::error::Error for SdkExtensionError {}
impl From<serde_json::Error> for SdkExtensionError {
fn from(e: serde_json::Error) -> Self {
Self::InvalidFormat(e.to_string())
}
}
impl From<SdkExtensionError> for ExtensionError {
fn from(e: SdkExtensionError) -> Self {
match e {
SdkExtensionError::CommandNotFound(s) => ExtensionError::CommandNotFound(s),
SdkExtensionError::InvalidArguments(s) => ExtensionError::InvalidArguments(s),
SdkExtensionError::ExecutionFailed(s) => ExtensionError::ExecutionFailed(s),
SdkExtensionError::Timeout(s) => ExtensionError::Timeout(s),
SdkExtensionError::NotFound(s) => ExtensionError::NotFound(s),
SdkExtensionError::InvalidFormat(s) => ExtensionError::InvalidFormat(s),
SdkExtensionError::LoadFailed(s) => ExtensionError::LoadFailed(s),
SdkExtensionError::SecurityError(s) => ExtensionError::SecurityError(s),
SdkExtensionError::NotSupported(s) => ExtensionError::NotSupported(s),
SdkExtensionError::ConfigurationError(s) => ExtensionError::ConfigurationError(s),
SdkExtensionError::InternalError(s) => ExtensionError::InternalError(s),
SdkExtensionError::Other(s) => ExtensionError::Other(s),
}
}
}
pub type SdkResult<T> = std::result::Result<T, SdkExtensionError>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FrontendManifest {
pub id: String,
pub version: String,
#[serde(default = "default_entrypoint")]
pub entrypoint: String,
pub style_entrypoint: Option<String>,
pub components: Vec<FrontendComponent>,
pub i18n: Option<I18nConfig>,
#[serde(default)]
pub dependencies: HashMap<String, String>,
}
fn default_entrypoint() -> String {
"index.js".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FrontendComponent {
pub name: String,
#[serde(rename = "type")]
pub component_type: FrontendComponentType,
pub display_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_size: Option<ComponentSize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_size: Option<ComponentSize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_size: Option<ComponentSize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub config_schema: Option<serde_json::Value>,
#[serde(default = "default_true")]
pub refreshable: bool,
#[serde(default)]
pub refresh_interval: u64,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum FrontendComponentType {
Card,
Widget,
Panel,
Dialog,
Settings,
}
#[derive(Debug, Clone, Serialize, Deserialize, Copy)]
pub struct ComponentSize {
pub width: u32,
pub height: u32,
}
impl ComponentSize {
pub fn new(width: u32, height: u32) -> Self {
Self { width, height }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct I18nConfig {
#[serde(default = "default_language")]
pub default_language: String,
pub supported_languages: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resources_path: Option<String>,
}
fn default_language() -> String {
"en".to_string()
}
pub struct FrontendManifestBuilder {
manifest: FrontendManifest,
}
impl FrontendManifestBuilder {
pub fn new(id: impl Into<String>, version: impl Into<String>) -> Self {
Self {
manifest: FrontendManifest {
id: id.into(),
version: version.into(),
entrypoint: default_entrypoint(),
style_entrypoint: None,
components: Vec::new(),
i18n: None,
dependencies: HashMap::new(),
},
}
}
pub fn entrypoint(mut self, path: impl Into<String>) -> Self {
self.manifest.entrypoint = path.into();
self
}
pub fn style_entrypoint(mut self, path: impl Into<String>) -> Self {
self.manifest.style_entrypoint = Some(path.into());
self
}
pub fn component(mut self, component: FrontendComponent) -> Self {
self.manifest.components.push(component);
self
}
pub fn card(mut self, name: impl Into<String>, display_name: impl Into<String>) -> Self {
self.manifest.components.push(FrontendComponent {
name: name.into(),
component_type: FrontendComponentType::Card,
display_name: display_name.into(),
description: None,
icon: None,
default_size: None,
min_size: None,
max_size: None,
config_schema: None,
refreshable: true,
refresh_interval: 0,
});
self
}
pub fn widget(mut self, name: impl Into<String>, display_name: impl Into<String>) -> Self {
self.manifest.components.push(FrontendComponent {
name: name.into(),
component_type: FrontendComponentType::Widget,
display_name: display_name.into(),
description: None,
icon: None,
default_size: None,
min_size: None,
max_size: None,
config_schema: None,
refreshable: true,
refresh_interval: 0,
});
self
}
pub fn panel(mut self, name: impl Into<String>, display_name: impl Into<String>) -> Self {
self.manifest.components.push(FrontendComponent {
name: name.into(),
component_type: FrontendComponentType::Panel,
display_name: display_name.into(),
description: None,
icon: None,
default_size: None,
min_size: None,
max_size: None,
config_schema: None,
refreshable: true,
refresh_interval: 0,
});
self
}
pub fn i18n(mut self, config: I18nConfig) -> Self {
self.manifest.i18n = Some(config);
self
}
pub fn dependency(mut self, name: impl Into<String>, version: impl Into<String>) -> Self {
self.manifest
.dependencies
.insert(name.into(), version.into());
self
}
pub fn build(self) -> FrontendManifest {
self.manifest
}
}
pub struct ArgParser<'a> {
args: &'a serde_json::Value,
}
impl<'a> ArgParser<'a> {
pub fn new(args: &'a serde_json::Value) -> Self {
Self { args }
}
pub fn get_string(&self, name: &str) -> SdkResult<String> {
self.args
.get(name)
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| {
SdkExtensionError::InvalidArguments(format!(
"Missing or invalid string argument: {}",
name
))
})
}
pub fn get_optional_string(&self, name: &str) -> Option<String> {
self.args
.get(name)
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
pub fn get_i64(&self, name: &str) -> SdkResult<i64> {
self.args.get(name).and_then(|v| v.as_i64()).ok_or_else(|| {
SdkExtensionError::InvalidArguments(format!(
"Missing or invalid integer argument: {}",
name
))
})
}
pub fn get_optional_i64(&self, name: &str) -> Option<i64> {
self.args.get(name).and_then(|v| v.as_i64())
}
pub fn get_f64(&self, name: &str) -> SdkResult<f64> {
self.args.get(name).and_then(|v| v.as_f64()).ok_or_else(|| {
SdkExtensionError::InvalidArguments(format!(
"Missing or invalid float argument: {}",
name
))
})
}
pub fn get_optional_f64(&self, name: &str) -> Option<f64> {
self.args.get(name).and_then(|v| v.as_f64())
}
pub fn get_bool(&self, name: &str) -> SdkResult<bool> {
self.args
.get(name)
.and_then(|v| v.as_bool())
.ok_or_else(|| {
SdkExtensionError::InvalidArguments(format!(
"Missing or invalid boolean argument: {}",
name
))
})
}
pub fn get_optional_bool(&self, name: &str) -> Option<bool> {
self.args.get(name).and_then(|v| v.as_bool())
}
pub fn get_object(&self, name: &str) -> SdkResult<&serde_json::Map<String, serde_json::Value>> {
self.args
.get(name)
.and_then(|v| v.as_object())
.ok_or_else(|| {
SdkExtensionError::InvalidArguments(format!(
"Missing or invalid object argument: {}",
name
))
})
}
pub fn get_array(&self, name: &str) -> SdkResult<&Vec<serde_json::Value>> {
self.args
.get(name)
.and_then(|v| v.as_array())
.ok_or_else(|| {
SdkExtensionError::InvalidArguments(format!(
"Missing or invalid array argument: {}",
name
))
})
}
pub fn parse<T: for<'de> Deserialize<'de>>(&self) -> SdkResult<T> {
serde_json::from_value(self.args.clone()).map_err(Into::into)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metric_data_type_serialization() {
let types = vec![
(SdkMetricDataType::Float, r#""float""#),
(SdkMetricDataType::Integer, r#""integer""#),
(SdkMetricDataType::Boolean, r#""boolean""#),
(SdkMetricDataType::String, r#""string""#),
(SdkMetricDataType::Binary, r#""binary""#),
];
for (dtype, expected) in types {
let json = serde_json::to_string(&dtype).unwrap();
assert_eq!(json, expected);
let deserialized: SdkMetricDataType = serde_json::from_str(expected).unwrap();
assert_eq!(dtype, deserialized);
}
let enum_type = SdkMetricDataType::Enum {
options: vec!["option1".to_string(), "option2".to_string()],
};
let json = serde_json::to_string(&enum_type).unwrap();
assert!(json.contains("enum"));
assert!(json.contains("options"));
}
#[test]
fn test_metric_definition_serialization() {
let metric = SdkMetricDefinition {
name: "test_metric".to_string(),
display_name: "Test Metric".to_string(),
data_type: SdkMetricDataType::Float,
unit: "°C".to_string(),
min: Some(0.0),
max: Some(100.0),
required: true,
};
let json = serde_json::to_string(&metric).unwrap();
assert!(json.contains("test_metric"));
assert!(json.contains("float"));
let deserialized: SdkMetricDefinition = serde_json::from_str(&json).unwrap();
assert_eq!(metric.name, deserialized.name);
assert_eq!(metric.unit, deserialized.unit);
}
#[test]
fn test_extension_metadata_serialization() {
let meta = SdkExtensionMetadata {
id: "test-ext".to_string(),
name: "Test Extension".to_string(),
version: "1.0.0".to_string(),
description: Some("A test extension".to_string()),
author: Some("Test Author".to_string()),
sdk_version: Some("0.5.11".to_string()),
extension_type: "native".to_string(),
};
let json = serde_json::to_string(&meta).unwrap();
assert!(json.contains("test-ext"));
assert!(json.contains("1.0.0"));
let deserialized: SdkExtensionMetadata = serde_json::from_str(&json).unwrap();
assert_eq!(meta.id, deserialized.id);
assert_eq!(meta.version, deserialized.version);
}
#[test]
fn test_extension_error_serialization() {
let error = SdkExtensionError::InvalidArguments("test error".to_string());
let json = serde_json::to_string(&error).unwrap();
assert!(json.contains("InvalidArguments"));
assert!(error.to_string().contains("test error"));
}
#[test]
fn test_type_conversions() {
let sdk_dt = SdkMetricDataType::Float;
let dt: MetricDataType = sdk_dt.clone().into();
assert!(matches!(dt, MetricDataType::Float));
let back: SdkMetricDataType = dt.into();
assert!(matches!(back, SdkMetricDataType::Float));
let sdk_v = SdkMetricValue::Integer(42);
let v: MetricValue = sdk_v.into();
assert!(matches!(v, MetricValue::Integer(42)));
}
}