Skip to main content

neomind_extension_sdk/
extension.rs

1//! Extension types and helpers for SDK
2//!
3//! This module provides types that work for both Native and WASM targets.
4//! The core IPC boundary types are defined in `ipc_types.rs` for stability.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9// Re-export all IPC boundary types for convenience
10pub use crate::ipc_types::*;
11
12// ============================================================================
13// SDK-Specific Extensions (not in IPC boundary)
14// ============================================================================
15
16/// Extension metadata for SDK (SDK-specific fields)
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct SdkExtensionMetadata {
19    /// Unique extension identifier
20    pub id: String,
21    /// Display name
22    pub name: String,
23    /// Version string
24    pub version: String,
25    /// Optional description
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub description: Option<String>,
28    /// Optional author
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub author: Option<String>,
31    /// SDK version used to build this extension
32    #[serde(default)]
33    pub sdk_version: Option<String>,
34    /// Extension type
35    #[serde(default = "default_extension_type")]
36    pub extension_type: String,
37}
38
39fn default_extension_type() -> String {
40    "native".to_string()
41}
42
43impl SdkExtensionMetadata {
44    /// Create new metadata
45    pub fn new(id: impl Into<String>, name: impl Into<String>, version: impl Into<String>) -> Self {
46        Self {
47            id: id.into(),
48            name: name.into(),
49            version: version.into(),
50            description: None,
51            author: None,
52            sdk_version: Some(env!("CARGO_PKG_VERSION").to_string()),
53            extension_type: "native".to_string(),
54        }
55    }
56
57    /// Add description
58    pub fn with_description(mut self, description: impl Into<String>) -> Self {
59        self.description = Some(description.into());
60        self
61    }
62
63    /// Add author
64    pub fn with_author(mut self, author: impl Into<String>) -> Self {
65        self.author = Some(author.into());
66        self
67    }
68
69    /// Set extension type
70    pub fn with_type(mut self, extension_type: impl Into<String>) -> Self {
71        self.extension_type = extension_type.into();
72        self
73    }
74}
75
76// ============================================================================
77// Metric Types (SDK-specific wrappers)
78// ============================================================================
79
80/// Metric data types (SDK-specific)
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "lowercase")]
83#[derive(Default)]
84pub enum SdkMetricDataType {
85    Float,
86    Integer,
87    Boolean,
88    #[default]
89    String,
90    Binary,
91    /// Enum type with a list of allowed options
92    Enum {
93        options: Vec<String>,
94    },
95}
96
97impl From<SdkMetricDataType> for MetricDataType {
98    fn from(dt: SdkMetricDataType) -> Self {
99        match dt {
100            SdkMetricDataType::Float => MetricDataType::Float,
101            SdkMetricDataType::Integer => MetricDataType::Integer,
102            SdkMetricDataType::Boolean => MetricDataType::Boolean,
103            SdkMetricDataType::String => MetricDataType::String,
104            SdkMetricDataType::Binary => MetricDataType::Binary,
105            SdkMetricDataType::Enum { options } => MetricDataType::Enum { options },
106        }
107    }
108}
109
110impl From<MetricDataType> for SdkMetricDataType {
111    fn from(dt: MetricDataType) -> Self {
112        match dt {
113            MetricDataType::Float => SdkMetricDataType::Float,
114            MetricDataType::Integer => SdkMetricDataType::Integer,
115            MetricDataType::Boolean => SdkMetricDataType::Boolean,
116            MetricDataType::String => SdkMetricDataType::String,
117            MetricDataType::Binary => SdkMetricDataType::Binary,
118            MetricDataType::Enum { options } => SdkMetricDataType::Enum { options },
119        }
120    }
121}
122
123/// Metric definition (SDK-specific)
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct SdkMetricDefinition {
126    /// Metric name
127    pub name: String,
128    /// Display name
129    pub display_name: String,
130    /// Data type
131    pub data_type: SdkMetricDataType,
132    /// Unit of measurement
133    #[serde(default)]
134    pub unit: String,
135    /// Minimum value (for numeric types)
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub min: Option<f64>,
138    /// Maximum value (for numeric types)
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub max: Option<f64>,
141    /// Is this metric required
142    #[serde(default)]
143    pub required: bool,
144}
145
146impl SdkMetricDefinition {
147    /// Create a new metric definition
148    pub fn new(
149        name: impl Into<String>,
150        display_name: impl Into<String>,
151        data_type: SdkMetricDataType,
152    ) -> Self {
153        Self {
154            name: name.into(),
155            display_name: display_name.into(),
156            data_type,
157            unit: String::new(),
158            min: None,
159            max: None,
160            required: false,
161        }
162    }
163
164    /// Add unit
165    pub fn with_unit(mut self, unit: impl Into<String>) -> Self {
166        self.unit = unit.into();
167        self
168    }
169
170    /// Add min value
171    pub fn with_min(mut self, min: f64) -> Self {
172        self.min = Some(min);
173        self
174    }
175
176    /// Add max value
177    pub fn with_max(mut self, max: f64) -> Self {
178        self.max = Some(max);
179        self
180    }
181
182    /// Set as required
183    pub fn with_required(mut self, required: bool) -> Self {
184        self.required = required;
185        self
186    }
187}
188
189impl From<SdkMetricDefinition> for MetricDescriptor {
190    fn from(def: SdkMetricDefinition) -> Self {
191        Self {
192            name: def.name,
193            display_name: def.display_name,
194            data_type: def.data_type.into(),
195            unit: def.unit,
196            min: def.min,
197            max: def.max,
198            required: def.required,
199        }
200    }
201}
202
203/// Metric value (SDK-specific)
204#[derive(Debug, Clone, Serialize, Deserialize)]
205#[serde(untagged)]
206#[derive(Default)]
207pub enum SdkMetricValue {
208    Float(f64),
209    Integer(i64),
210    Boolean(bool),
211    String(String),
212    Binary(Vec<u8>),
213    #[default]
214    Null,
215}
216
217impl From<SdkMetricValue> for MetricValue {
218    fn from(v: SdkMetricValue) -> Self {
219        match v {
220            SdkMetricValue::Float(f) => MetricValue::Float(f),
221            SdkMetricValue::Integer(i) => MetricValue::Integer(i),
222            SdkMetricValue::Boolean(b) => MetricValue::Boolean(b),
223            SdkMetricValue::String(s) => MetricValue::String(s),
224            SdkMetricValue::Binary(b) => MetricValue::Binary(b),
225            SdkMetricValue::Null => MetricValue::Null,
226        }
227    }
228}
229
230impl From<MetricValue> for SdkMetricValue {
231    fn from(v: MetricValue) -> Self {
232        match v {
233            MetricValue::Float(f) => SdkMetricValue::Float(f),
234            MetricValue::Integer(i) => SdkMetricValue::Integer(i),
235            MetricValue::Boolean(b) => SdkMetricValue::Boolean(b),
236            MetricValue::String(s) => SdkMetricValue::String(s),
237            MetricValue::Binary(b) => SdkMetricValue::Binary(b),
238            MetricValue::Null => SdkMetricValue::Null,
239        }
240    }
241}
242
243impl From<f64> for SdkMetricValue {
244    fn from(v: f64) -> Self {
245        Self::Float(v)
246    }
247}
248
249impl From<i64> for SdkMetricValue {
250    fn from(v: i64) -> Self {
251        Self::Integer(v)
252    }
253}
254
255impl From<bool> for SdkMetricValue {
256    fn from(v: bool) -> Self {
257        Self::Boolean(v)
258    }
259}
260
261impl From<String> for SdkMetricValue {
262    fn from(v: String) -> Self {
263        Self::String(v)
264    }
265}
266
267impl From<&str> for SdkMetricValue {
268    fn from(v: &str) -> Self {
269        Self::String(v.to_string())
270    }
271}
272
273impl From<Vec<u8>> for SdkMetricValue {
274    fn from(v: Vec<u8>) -> Self {
275        Self::Binary(v)
276    }
277}
278
279// ============================================================================
280// Extension Metric Value (SDK-specific)
281// ============================================================================
282
283/// Extension metric value with name, value and timestamp (SDK-specific)
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct SdkExtensionMetricValue {
286    /// Metric name
287    pub name: String,
288    /// Metric value
289    pub value: SdkMetricValue,
290    /// Timestamp in milliseconds
291    pub timestamp: i64,
292}
293
294impl SdkExtensionMetricValue {
295    /// Create a new extension metric value
296    pub fn new(name: impl Into<String>, value: SdkMetricValue) -> Self {
297        Self {
298            name: name.into(),
299            value,
300            timestamp: {
301                #[cfg(not(target_arch = "wasm32"))]
302                {
303                    crate::ipc_types::current_timestamp_ms()
304                }
305                #[cfg(target_arch = "wasm32")]
306                {
307                    // Request timestamp from host via capability invocation
308                    crate::wasm::bindings::invoke_capability_raw(
309                        "system_timestamp",
310                        &serde_json::json!({}),
311                    )
312                    .ok()
313                    .and_then(|v| v.get("timestamp_ms").and_then(|t| t.as_i64()))
314                    .unwrap_or(0)
315                }
316            },
317        }
318    }
319
320    /// Create with explicit timestamp
321    pub fn with_timestamp(name: impl Into<String>, value: SdkMetricValue, timestamp: i64) -> Self {
322        Self {
323            name: name.into(),
324            value,
325            timestamp,
326        }
327    }
328}
329
330impl From<SdkExtensionMetricValue> for ExtensionMetricValue {
331    fn from(v: SdkExtensionMetricValue) -> Self {
332        Self {
333            name: v.name,
334            value: v.value.into(),
335            timestamp: v.timestamp,
336        }
337    }
338}
339
340// ============================================================================
341// Command Types (SDK-specific)
342// ============================================================================
343
344/// Parameter definition for commands (SDK-specific)
345#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct SdkParameterDefinition {
347    /// Parameter name
348    pub name: String,
349    /// Display name
350    #[serde(default)]
351    pub display_name: String,
352    /// Description
353    #[serde(default)]
354    pub description: String,
355    /// Parameter data type
356    #[serde(default)]
357    pub param_type: SdkMetricDataType,
358    /// Is this parameter required
359    #[serde(default)]
360    pub required: bool,
361    /// Default value
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub default_value: Option<SdkMetricValue>,
364    /// Minimum value
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub min: Option<f64>,
367    /// Maximum value
368    #[serde(skip_serializing_if = "Option::is_none")]
369    pub max: Option<f64>,
370    /// Options for enum types
371    #[serde(default)]
372    pub options: Vec<String>,
373}
374
375impl SdkParameterDefinition {
376    /// Create a new parameter definition
377    pub fn new(name: impl Into<String>, param_type: SdkMetricDataType) -> Self {
378        Self {
379            name: name.into(),
380            display_name: String::new(),
381            description: String::new(),
382            param_type,
383            required: true,
384            default_value: None,
385            min: None,
386            max: None,
387            options: Vec::new(),
388        }
389    }
390
391    /// Add display name
392    pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
393        self.display_name = display_name.into();
394        self
395    }
396
397    /// Add description
398    pub fn with_description(mut self, description: impl Into<String>) -> Self {
399        self.description = description.into();
400        self
401    }
402
403    /// Set as optional
404    pub fn optional(mut self) -> Self {
405        self.required = false;
406        self
407    }
408
409    /// Add default value
410    pub fn with_default(mut self, default: SdkMetricValue) -> Self {
411        self.default_value = Some(default);
412        self.required = false;
413        self
414    }
415}
416
417impl From<SdkParameterDefinition> for ParameterDefinition {
418    fn from(p: SdkParameterDefinition) -> Self {
419        Self {
420            name: p.name,
421            display_name: p.display_name,
422            description: p.description,
423            param_type: p.param_type.into(),
424            required: p.required,
425            default_value: p.default_value.map(|v| v.into()),
426            min: p.min,
427            max: p.max,
428            options: p.options,
429        }
430    }
431}
432
433/// Command definition (SDK-specific)
434#[derive(Debug, Clone, Serialize, Deserialize, Default)]
435pub struct SdkCommandDefinition {
436    /// Command name
437    pub name: String,
438    /// Display name
439    #[serde(default)]
440    pub display_name: String,
441    /// Payload template
442    #[serde(default)]
443    pub payload_template: String,
444    /// Description
445    #[serde(default)]
446    pub description: String,
447    /// Parameters
448    #[serde(default)]
449    pub parameters: Vec<SdkParameterDefinition>,
450    /// Fixed values
451    #[serde(default)]
452    pub fixed_values: std::collections::HashMap<String, serde_json::Value>,
453    /// Sample payloads
454    #[serde(default)]
455    pub samples: Vec<serde_json::Value>,
456    /// Parameter groups
457    #[serde(default)]
458    pub parameter_groups: Vec<SdkParameterGroup>,
459}
460
461/// Parameter group for organizing command parameters
462#[derive(Debug, Clone, Serialize, Deserialize)]
463pub struct SdkParameterGroup {
464    /// Group name
465    pub name: String,
466    /// Display name
467    #[serde(default)]
468    pub display_name: String,
469    /// Description
470    #[serde(default)]
471    pub description: String,
472    /// Parameters in this group
473    #[serde(default)]
474    pub parameters: Vec<String>,
475}
476
477impl SdkCommandDefinition {
478    /// Create a new command definition
479    pub fn new(name: impl Into<String>) -> Self {
480        Self {
481            name: name.into(),
482            display_name: String::new(),
483            payload_template: String::new(),
484            description: String::new(),
485            parameters: Vec::new(),
486            fixed_values: std::collections::HashMap::new(),
487            samples: Vec::new(),
488            parameter_groups: Vec::new(),
489        }
490    }
491
492    /// Add description
493    pub fn with_description(mut self, description: impl Into<String>) -> Self {
494        self.description = description.into();
495        self
496    }
497
498    /// Add a parameter
499    pub fn param(mut self, param: SdkParameterDefinition) -> Self {
500        self.parameters.push(param);
501        self
502    }
503}
504
505impl From<SdkCommandDefinition> for CommandDescriptor {
506    fn from(c: SdkCommandDefinition) -> Self {
507        Self {
508            name: c.name,
509            display_name: c.display_name,
510            description: c.description,
511            payload_template: c.payload_template,
512            parameters: c.parameters.into_iter().map(|p| p.into()).collect(),
513            fixed_values: c.fixed_values,
514            samples: c.samples,
515            parameter_groups: c
516                .parameter_groups
517                .into_iter()
518                .map(|g| ParameterGroup {
519                    name: g.name,
520                    display_name: g.display_name,
521                    description: g.description,
522                    parameters: g.parameters,
523                })
524                .collect(),
525        }
526    }
527}
528
529// ============================================================================
530// Error Types (SDK-specific)
531// ============================================================================
532
533/// Extension error type (SDK-specific)
534#[derive(Debug, Clone, Serialize, Deserialize)]
535pub enum SdkExtensionError {
536    /// Command not found
537    CommandNotFound(String),
538    /// Invalid arguments
539    InvalidArguments(String),
540    /// Execution failed
541    ExecutionFailed(String),
542    /// Timeout
543    Timeout(String),
544    /// Not found
545    NotFound(String),
546    /// Invalid format
547    InvalidFormat(String),
548    /// Load failed
549    LoadFailed(String),
550    /// Security error
551    SecurityError(String),
552    /// Not supported
553    NotSupported(String),
554    /// Configuration error
555    ConfigurationError(String),
556    /// Internal error
557    InternalError(String),
558    /// Other error
559    Other(String),
560}
561
562impl std::fmt::Display for SdkExtensionError {
563    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
564        match self {
565            Self::CommandNotFound(cmd) => write!(f, "Command not found: {}", cmd),
566            Self::InvalidArguments(msg) => write!(f, "Invalid arguments: {}", msg),
567            Self::ExecutionFailed(msg) => write!(f, "Execution failed: {}", msg),
568            Self::Timeout(msg) => write!(f, "Timeout: {}", msg),
569            Self::NotFound(msg) => write!(f, "Not found: {}", msg),
570            Self::InvalidFormat(msg) => write!(f, "Invalid format: {}", msg),
571            Self::LoadFailed(msg) => write!(f, "Load failed: {}", msg),
572            Self::SecurityError(msg) => write!(f, "Security error: {}", msg),
573            Self::NotSupported(msg) => write!(f, "Not supported: {}", msg),
574            Self::ConfigurationError(msg) => write!(f, "Configuration error: {}", msg),
575            Self::InternalError(msg) => write!(f, "Internal error: {}", msg),
576            Self::Other(msg) => write!(f, "Error: {}", msg),
577        }
578    }
579}
580
581impl std::error::Error for SdkExtensionError {}
582
583impl From<serde_json::Error> for SdkExtensionError {
584    fn from(e: serde_json::Error) -> Self {
585        Self::InvalidFormat(e.to_string())
586    }
587}
588
589impl From<SdkExtensionError> for ExtensionError {
590    fn from(e: SdkExtensionError) -> Self {
591        match e {
592            SdkExtensionError::CommandNotFound(s) => ExtensionError::CommandNotFound(s),
593            SdkExtensionError::InvalidArguments(s) => ExtensionError::InvalidArguments(s),
594            SdkExtensionError::ExecutionFailed(s) => ExtensionError::ExecutionFailed(s),
595            SdkExtensionError::Timeout(s) => ExtensionError::Timeout(s),
596            SdkExtensionError::NotFound(s) => ExtensionError::NotFound(s),
597            SdkExtensionError::InvalidFormat(s) => ExtensionError::InvalidFormat(s),
598            SdkExtensionError::LoadFailed(s) => ExtensionError::LoadFailed(s),
599            SdkExtensionError::SecurityError(s) => ExtensionError::SecurityError(s),
600            SdkExtensionError::NotSupported(s) => ExtensionError::NotSupported(s),
601            SdkExtensionError::ConfigurationError(s) => ExtensionError::ConfigurationError(s),
602            SdkExtensionError::InternalError(s) => ExtensionError::InternalError(s),
603            SdkExtensionError::Other(s) => ExtensionError::Other(s),
604        }
605    }
606}
607
608/// Result type for SDK extension operations
609pub type SdkResult<T> = std::result::Result<T, SdkExtensionError>;
610
611// ============================================================================
612// Frontend Component Types
613// ============================================================================
614
615/// Frontend component manifest for extensions
616#[derive(Debug, Clone, Serialize, Deserialize)]
617pub struct FrontendManifest {
618    /// Extension ID this frontend belongs to
619    pub id: String,
620    /// Frontend version
621    pub version: String,
622    /// Path to main JavaScript file
623    #[serde(default = "default_entrypoint")]
624    pub entrypoint: String,
625    /// Path to main CSS file (optional)
626    pub style_entrypoint: Option<String>,
627    /// List of components provided
628    pub components: Vec<FrontendComponent>,
629    /// i18n configuration
630    pub i18n: Option<I18nConfig>,
631    /// Frontend dependencies
632    #[serde(default)]
633    pub dependencies: HashMap<String, String>,
634}
635
636fn default_entrypoint() -> String {
637    "index.js".to_string()
638}
639
640/// Frontend component definition
641#[derive(Debug, Clone, Serialize, Deserialize)]
642pub struct FrontendComponent {
643    /// Component identifier
644    pub name: String,
645    /// Component type
646    #[serde(rename = "type")]
647    pub component_type: FrontendComponentType,
648    /// Human-readable name
649    pub display_name: String,
650    /// Component description
651    #[serde(skip_serializing_if = "Option::is_none")]
652    pub description: Option<String>,
653    /// Icon name or SVG path
654    #[serde(skip_serializing_if = "Option::is_none")]
655    pub icon: Option<String>,
656    /// Default size
657    #[serde(skip_serializing_if = "Option::is_none")]
658    pub default_size: Option<ComponentSize>,
659    /// Minimum size
660    #[serde(skip_serializing_if = "Option::is_none")]
661    pub min_size: Option<ComponentSize>,
662    /// Maximum size
663    #[serde(skip_serializing_if = "Option::is_none")]
664    pub max_size: Option<ComponentSize>,
665    /// Configuration schema (JSON Schema)
666    #[serde(skip_serializing_if = "Option::is_none")]
667    pub config_schema: Option<serde_json::Value>,
668    /// Supports manual refresh
669    #[serde(default = "default_true")]
670    pub refreshable: bool,
671    /// Default refresh interval in milliseconds (0 = no auto-refresh)
672    #[serde(default)]
673    pub refresh_interval: u64,
674}
675
676fn default_true() -> bool {
677    true
678}
679
680/// Component type enumeration
681#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
682#[serde(rename_all = "lowercase")]
683pub enum FrontendComponentType {
684    /// Dashboard card component
685    Card,
686    /// Widget component
687    Widget,
688    /// Panel component
689    Panel,
690    /// Dialog component
691    Dialog,
692    /// Settings component
693    Settings,
694}
695
696/// Component size definition
697#[derive(Debug, Clone, Serialize, Deserialize, Copy)]
698pub struct ComponentSize {
699    /// Width in pixels
700    pub width: u32,
701    /// Height in pixels
702    pub height: u32,
703}
704
705impl ComponentSize {
706    /// Create a new size
707    pub fn new(width: u32, height: u32) -> Self {
708        Self { width, height }
709    }
710}
711
712/// i18n configuration
713#[derive(Debug, Clone, Serialize, Deserialize)]
714pub struct I18nConfig {
715    /// Default language
716    #[serde(default = "default_language")]
717    pub default_language: String,
718    /// Supported languages
719    pub supported_languages: Vec<String>,
720    /// Path to i18n resource files
721    #[serde(skip_serializing_if = "Option::is_none")]
722    pub resources_path: Option<String>,
723}
724
725fn default_language() -> String {
726    "en".to_string()
727}
728
729// ============================================================================
730// Frontend Manifest Builder
731// ============================================================================
732
733/// Builder for creating frontend manifests
734pub struct FrontendManifestBuilder {
735    manifest: FrontendManifest,
736}
737
738impl FrontendManifestBuilder {
739    /// Create a new builder
740    pub fn new(id: impl Into<String>, version: impl Into<String>) -> Self {
741        Self {
742            manifest: FrontendManifest {
743                id: id.into(),
744                version: version.into(),
745                entrypoint: default_entrypoint(),
746                style_entrypoint: None,
747                components: Vec::new(),
748                i18n: None,
749                dependencies: HashMap::new(),
750            },
751        }
752    }
753
754    /// Set the entrypoint
755    pub fn entrypoint(mut self, path: impl Into<String>) -> Self {
756        self.manifest.entrypoint = path.into();
757        self
758    }
759
760    /// Set the style entrypoint
761    pub fn style_entrypoint(mut self, path: impl Into<String>) -> Self {
762        self.manifest.style_entrypoint = Some(path.into());
763        self
764    }
765
766    /// Add a component
767    pub fn component(mut self, component: FrontendComponent) -> Self {
768        self.manifest.components.push(component);
769        self
770    }
771
772    /// Add a card component
773    pub fn card(mut self, name: impl Into<String>, display_name: impl Into<String>) -> Self {
774        self.manifest.components.push(FrontendComponent {
775            name: name.into(),
776            component_type: FrontendComponentType::Card,
777            display_name: display_name.into(),
778            description: None,
779            icon: None,
780            default_size: None,
781            min_size: None,
782            max_size: None,
783            config_schema: None,
784            refreshable: true,
785            refresh_interval: 0,
786        });
787        self
788    }
789
790    /// Add a widget component
791    pub fn widget(mut self, name: impl Into<String>, display_name: impl Into<String>) -> Self {
792        self.manifest.components.push(FrontendComponent {
793            name: name.into(),
794            component_type: FrontendComponentType::Widget,
795            display_name: display_name.into(),
796            description: None,
797            icon: None,
798            default_size: None,
799            min_size: None,
800            max_size: None,
801            config_schema: None,
802            refreshable: true,
803            refresh_interval: 0,
804        });
805        self
806    }
807
808    /// Add a panel component
809    pub fn panel(mut self, name: impl Into<String>, display_name: impl Into<String>) -> Self {
810        self.manifest.components.push(FrontendComponent {
811            name: name.into(),
812            component_type: FrontendComponentType::Panel,
813            display_name: display_name.into(),
814            description: None,
815            icon: None,
816            default_size: None,
817            min_size: None,
818            max_size: None,
819            config_schema: None,
820            refreshable: true,
821            refresh_interval: 0,
822        });
823        self
824    }
825
826    /// Set i18n configuration
827    pub fn i18n(mut self, config: I18nConfig) -> Self {
828        self.manifest.i18n = Some(config);
829        self
830    }
831
832    /// Add a dependency
833    pub fn dependency(mut self, name: impl Into<String>, version: impl Into<String>) -> Self {
834        self.manifest
835            .dependencies
836            .insert(name.into(), version.into());
837        self
838    }
839
840    /// Build the manifest
841    pub fn build(self) -> FrontendManifest {
842        self.manifest
843    }
844}
845
846// ============================================================================
847// Argument Parsing Helpers
848// ============================================================================
849
850/// Helper for parsing command arguments
851pub struct ArgParser<'a> {
852    args: &'a serde_json::Value,
853}
854
855impl<'a> ArgParser<'a> {
856    /// Create a new argument parser
857    pub fn new(args: &'a serde_json::Value) -> Self {
858        Self { args }
859    }
860
861    /// Get a string argument
862    pub fn get_string(&self, name: &str) -> SdkResult<String> {
863        self.args
864            .get(name)
865            .and_then(|v| v.as_str())
866            .map(|s| s.to_string())
867            .ok_or_else(|| {
868                SdkExtensionError::InvalidArguments(format!(
869                    "Missing or invalid string argument: {}",
870                    name
871                ))
872            })
873    }
874
875    /// Get an optional string argument
876    pub fn get_optional_string(&self, name: &str) -> Option<String> {
877        self.args
878            .get(name)
879            .and_then(|v| v.as_str())
880            .map(|s| s.to_string())
881    }
882
883    /// Get an i64 argument
884    pub fn get_i64(&self, name: &str) -> SdkResult<i64> {
885        self.args.get(name).and_then(|v| v.as_i64()).ok_or_else(|| {
886            SdkExtensionError::InvalidArguments(format!(
887                "Missing or invalid integer argument: {}",
888                name
889            ))
890        })
891    }
892
893    /// Get an optional i64 argument
894    pub fn get_optional_i64(&self, name: &str) -> Option<i64> {
895        self.args.get(name).and_then(|v| v.as_i64())
896    }
897
898    /// Get a f64 argument
899    pub fn get_f64(&self, name: &str) -> SdkResult<f64> {
900        self.args.get(name).and_then(|v| v.as_f64()).ok_or_else(|| {
901            SdkExtensionError::InvalidArguments(format!(
902                "Missing or invalid float argument: {}",
903                name
904            ))
905        })
906    }
907
908    /// Get an optional f64 argument
909    pub fn get_optional_f64(&self, name: &str) -> Option<f64> {
910        self.args.get(name).and_then(|v| v.as_f64())
911    }
912
913    /// Get a bool argument
914    pub fn get_bool(&self, name: &str) -> SdkResult<bool> {
915        self.args
916            .get(name)
917            .and_then(|v| v.as_bool())
918            .ok_or_else(|| {
919                SdkExtensionError::InvalidArguments(format!(
920                    "Missing or invalid boolean argument: {}",
921                    name
922                ))
923            })
924    }
925
926    /// Get an optional bool argument
927    pub fn get_optional_bool(&self, name: &str) -> Option<bool> {
928        self.args.get(name).and_then(|v| v.as_bool())
929    }
930
931    /// Get a JSON object argument
932    pub fn get_object(&self, name: &str) -> SdkResult<&serde_json::Map<String, serde_json::Value>> {
933        self.args
934            .get(name)
935            .and_then(|v| v.as_object())
936            .ok_or_else(|| {
937                SdkExtensionError::InvalidArguments(format!(
938                    "Missing or invalid object argument: {}",
939                    name
940                ))
941            })
942    }
943
944    /// Get a JSON array argument
945    pub fn get_array(&self, name: &str) -> SdkResult<&Vec<serde_json::Value>> {
946        self.args
947            .get(name)
948            .and_then(|v| v.as_array())
949            .ok_or_else(|| {
950                SdkExtensionError::InvalidArguments(format!(
951                    "Missing or invalid array argument: {}",
952                    name
953                ))
954            })
955    }
956
957    /// Parse the entire args as a specific type
958    pub fn parse<T: for<'de> Deserialize<'de>>(&self) -> SdkResult<T> {
959        serde_json::from_value(self.args.clone()).map_err(Into::into)
960    }
961}
962
963// ============================================================================
964// Extension Statistics (for WASM target)
965// ============================================================================
966
967#[cfg(test)]
968mod tests {
969    use super::*;
970
971    #[test]
972    fn test_metric_data_type_serialization() {
973        let types = vec![
974            (SdkMetricDataType::Float, r#""float""#),
975            (SdkMetricDataType::Integer, r#""integer""#),
976            (SdkMetricDataType::Boolean, r#""boolean""#),
977            (SdkMetricDataType::String, r#""string""#),
978            (SdkMetricDataType::Binary, r#""binary""#),
979        ];
980
981        for (dtype, expected) in types {
982            let json = serde_json::to_string(&dtype).unwrap();
983            assert_eq!(json, expected);
984
985            let deserialized: SdkMetricDataType = serde_json::from_str(expected).unwrap();
986            assert_eq!(dtype, deserialized);
987        }
988
989        // Test Enum type
990        let enum_type = SdkMetricDataType::Enum {
991            options: vec!["option1".to_string(), "option2".to_string()],
992        };
993        let json = serde_json::to_string(&enum_type).unwrap();
994        assert!(json.contains("enum"));
995        assert!(json.contains("options"));
996    }
997
998    #[test]
999    fn test_metric_definition_serialization() {
1000        let metric = SdkMetricDefinition {
1001            name: "test_metric".to_string(),
1002            display_name: "Test Metric".to_string(),
1003            data_type: SdkMetricDataType::Float,
1004            unit: "°C".to_string(),
1005            min: Some(0.0),
1006            max: Some(100.0),
1007            required: true,
1008        };
1009
1010        let json = serde_json::to_string(&metric).unwrap();
1011        assert!(json.contains("test_metric"));
1012        assert!(json.contains("float"));
1013
1014        let deserialized: SdkMetricDefinition = serde_json::from_str(&json).unwrap();
1015        assert_eq!(metric.name, deserialized.name);
1016        assert_eq!(metric.unit, deserialized.unit);
1017    }
1018
1019    #[test]
1020    fn test_extension_metadata_serialization() {
1021        let meta = SdkExtensionMetadata {
1022            id: "test-ext".to_string(),
1023            name: "Test Extension".to_string(),
1024            version: "1.0.0".to_string(),
1025            description: Some("A test extension".to_string()),
1026            author: Some("Test Author".to_string()),
1027            sdk_version: Some("0.5.11".to_string()),
1028            extension_type: "native".to_string(),
1029        };
1030
1031        let json = serde_json::to_string(&meta).unwrap();
1032        assert!(json.contains("test-ext"));
1033        assert!(json.contains("1.0.0"));
1034
1035        let deserialized: SdkExtensionMetadata = serde_json::from_str(&json).unwrap();
1036        assert_eq!(meta.id, deserialized.id);
1037        assert_eq!(meta.version, deserialized.version);
1038    }
1039
1040    #[test]
1041    fn test_extension_error_serialization() {
1042        let error = SdkExtensionError::InvalidArguments("test error".to_string());
1043        let json = serde_json::to_string(&error).unwrap();
1044        assert!(json.contains("InvalidArguments"));
1045
1046        // Test error display
1047        assert!(error.to_string().contains("test error"));
1048    }
1049
1050    #[test]
1051    fn test_type_conversions() {
1052        // SdkMetricDataType <-> MetricDataType
1053        let sdk_dt = SdkMetricDataType::Float;
1054        let dt: MetricDataType = sdk_dt.clone().into();
1055        assert!(matches!(dt, MetricDataType::Float));
1056        let back: SdkMetricDataType = dt.into();
1057        assert!(matches!(back, SdkMetricDataType::Float));
1058
1059        // SdkMetricValue <-> MetricValue
1060        let sdk_v = SdkMetricValue::Integer(42);
1061        let v: MetricValue = sdk_v.into();
1062        assert!(matches!(v, MetricValue::Integer(42)));
1063    }
1064}