streamkit_core/
packet_meta.rs

1// SPDX-FileCopyrightText: © 2025 StreamKit Contributors
2//
3// SPDX-License-Identifier: MPL-2.0
4
5use serde::{Deserialize, Serialize};
6use std::sync::OnceLock;
7use ts_rs::TS;
8
9use crate::types::PacketType;
10
11/// Declarative field rule used by compatibility checks.
12/// If `wildcard_value` is present, a field that equals it is treated as "matches anything".
13#[derive(Debug, Clone, Serialize, Deserialize, TS)]
14#[ts(export)]
15pub struct FieldRule {
16    pub name: String,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub wildcard_value: Option<serde_json::Value>,
19}
20
21/// Simple compatibility strategies supported in v1.
22/// This enum is intentionally extensible for future variants (e.g., expressions, ranges).
23#[derive(Debug, Clone, Serialize, Deserialize, TS)]
24#[ts(export)]
25#[serde(tag = "kind", rename_all = "lowercase")]
26pub enum Compatibility {
27    /// Matches anything.
28    Any,
29    /// Kinds must be identical. Unit variants always match when kinds match.
30    Exact,
31    /// Kinds must match. Each field must be equal unless either side equals the wildcard_value.
32    StructFieldWildcard { fields: Vec<FieldRule> },
33}
34
35/// Server-driven metadata for packet types.
36/// Lives next to the PacketType definition (in core) and can be exposed to the UI.
37#[derive(Debug, Clone, Serialize, Deserialize, TS)]
38#[ts(export)]
39pub struct PacketTypeMeta {
40    /// Variant identifier (e.g., "RawAudio", "OpusAudio", "Binary", "Any").
41    pub id: String,
42    /// Human-friendly default label.
43    pub label: String,
44    /// Hex color to use in UIs.
45    pub color: String,
46    /// Optional display template for struct payloads. Placeholders are field names,
47    /// optionally with "|*" to indicate wildcard-display (handled on the client).
48    /// Example: "Raw Audio ({sample_rate|*}Hz, {channels|*}ch, {sample_format})"
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub display_template: Option<String>,
51    /// Compatibility strategy for this type.
52    pub compatibility: Compatibility,
53}
54
55/// Returns the built-in registry of packet type metadata.
56/// This returns a shared, lazily-initialized slice to avoid allocations in hot paths
57/// (e.g., runtime validation in the dynamic engine).
58pub fn packet_type_registry() -> &'static [PacketTypeMeta] {
59    static REGISTRY: OnceLock<Vec<PacketTypeMeta>> = OnceLock::new();
60    REGISTRY.get_or_init(|| {
61        vec![
62            PacketTypeMeta {
63                id: "Any".into(),
64                label: "Any".into(),
65                color: "#96ceb4".into(),
66                display_template: None,
67                compatibility: Compatibility::Any,
68            },
69            PacketTypeMeta {
70                id: "Binary".into(),
71                label: "Binary".into(),
72                color: "#45b7d1".into(),
73                display_template: None,
74                compatibility: Compatibility::Exact,
75            },
76            PacketTypeMeta {
77                id: "Text".into(),
78                label: "Text".into(),
79                color: "#4ecdc4".into(),
80                display_template: None,
81                compatibility: Compatibility::Exact,
82            },
83            PacketTypeMeta {
84                id: "OpusAudio".into(),
85                label: "Opus Audio".into(),
86                color: "#ff6b6b".into(),
87                display_template: None,
88                compatibility: Compatibility::Exact,
89            },
90            PacketTypeMeta {
91                id: "RawAudio".into(),
92                label: "Raw Audio".into(),
93                color: "#f39c12".into(),
94                display_template: Some(
95                    "Raw Audio ({sample_rate|*}Hz, {channels|*}ch, {sample_format})".into(),
96                ),
97                compatibility: Compatibility::StructFieldWildcard {
98                    fields: vec![
99                        FieldRule {
100                            name: "sample_rate".into(),
101                            wildcard_value: Some(serde_json::json!(0)),
102                        },
103                        FieldRule {
104                            name: "channels".into(),
105                            wildcard_value: Some(serde_json::json!(0)),
106                        },
107                        FieldRule { name: "sample_format".into(), wildcard_value: None },
108                    ],
109                },
110            },
111            PacketTypeMeta {
112                id: "Transcription".into(),
113                label: "Transcription".into(),
114                color: "#9b59b6".into(),
115                display_template: None,
116                compatibility: Compatibility::Exact,
117            },
118            PacketTypeMeta {
119                id: "Custom".into(),
120                label: "Custom".into(),
121                color: "#e67e22".into(),
122                display_template: Some("Custom ({type_id})".into()),
123                compatibility: Compatibility::StructFieldWildcard {
124                    fields: vec![FieldRule { name: "type_id".into(), wildcard_value: None }],
125                },
126            },
127        ]
128    })
129}
130
131/// Extracts the PacketType variant id and an optional JSON payload for struct variants.
132fn to_variant_and_payload(packet_type: &PacketType) -> (String, Option<serde_json::Value>) {
133    let json = serde_json::to_value(packet_type).unwrap_or(serde_json::Value::Null);
134    match json {
135        serde_json::Value::String(unit) => (unit, None),
136        serde_json::Value::Object(map) => {
137            if map.len() == 1 {
138                // SAFETY: We just checked that map has exactly 1 element
139                if let Some((k, v)) = map.into_iter().next() {
140                    (k, Some(v))
141                } else {
142                    ("Unknown".to_string(), None)
143                }
144            } else {
145                ("Unknown".to_string(), None)
146            }
147        },
148        _ => ("Unknown".to_string(), None),
149    }
150}
151
152/// Finds metadata by id.
153fn find_meta<'a>(registry: &'a [PacketTypeMeta], id: &str) -> Option<&'a PacketTypeMeta> {
154    registry.iter().find(|m| m.id == id)
155}
156
157/// Generic, server-side compatibility check used by both stateless and dynamic engines.
158/// v1 rules:
159/// - Any matches anything
160/// - Kinds must match (unless Any)
161/// - Exact: always true when kinds match
162/// - StructFieldWildcard: all fields must be equal unless either side equals wildcard_value
163pub fn can_connect(output: &PacketType, input: &PacketType, registry: &[PacketTypeMeta]) -> bool {
164    let (out_id, out_payload) = to_variant_and_payload(output);
165    let (in_id, in_payload) = to_variant_and_payload(input);
166
167    if out_id == "Any" || in_id == "Any" {
168        return true;
169    }
170    if out_id != in_id {
171        return false;
172    }
173
174    let Some(meta) = find_meta(registry, &out_id) else {
175        // If we lack metadata, be conservative.
176        return false;
177    };
178
179    match &meta.compatibility {
180        Compatibility::Any | Compatibility::Exact => true,
181        Compatibility::StructFieldWildcard { fields } => {
182            let (Some(out_obj), Some(in_obj)) = (out_payload.as_ref(), in_payload.as_ref()) else {
183                return false;
184            };
185            let Some(out_map) = out_obj.as_object() else {
186                return false;
187            };
188            let Some(in_map) = in_obj.as_object() else {
189                return false;
190            };
191
192            fields.iter().all(|f| {
193                let Some(av) = out_map.get(&f.name) else {
194                    return false;
195                };
196                let Some(bv) = in_map.get(&f.name) else {
197                    return false;
198                };
199
200                // If either equals the wildcard, it matches
201                if let Some(wild) = &f.wildcard_value {
202                    if av == wild || bv == wild {
203                        return true;
204                    }
205                }
206
207                // Otherwise, values must be equal
208                av == bv
209            })
210        },
211    }
212}
213
214/// Convenience helper to test an output type against multiple input types.
215pub fn can_connect_any(
216    output: &PacketType,
217    inputs: &[PacketType],
218    registry: &[PacketTypeMeta],
219) -> bool {
220    inputs.iter().any(|inp| can_connect(output, inp, registry))
221}