spacegate_model/
plugin.rs

1use std::{
2    borrow::Cow,
3    collections::HashMap,
4    fmt::Display,
5    hash::Hash,
6    ops::{Deref, DerefMut},
7    str::FromStr,
8};
9
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13use crate::BoxError;
14
15pub mod gatewayapi_support_filter;
16
17#[cfg_attr(feature = "typegen", derive(ts_rs::TS), ts(export))]
18#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
19#[serde(tag = "kind", rename_all = "lowercase")]
20pub enum PluginInstanceName {
21    Anon {
22        uid: String,
23    },
24    Named {
25        /// name should be unique within the plugin code, composed of alphanumeric characters and hyphens
26        name: String,
27    },
28    Mono,
29}
30
31impl PluginInstanceName {
32    pub fn named(name: impl Into<String>) -> Self {
33        PluginInstanceName::Named { name: name.into() }
34    }
35    pub fn mono() -> Self {
36        PluginInstanceName::Mono {}
37    }
38    pub fn anon(uid: impl ToString) -> Self {
39        PluginInstanceName::Anon { uid: uid.to_string() }
40    }
41
42    pub fn to_raw_str(&self) -> String {
43        match self {
44            PluginInstanceName::Anon { uid } => uid.to_string(),
45            PluginInstanceName::Named { name } => name.to_string(),
46            PluginInstanceName::Mono => "".to_string(),
47        }
48    }
49}
50
51impl From<Option<String>> for PluginInstanceName {
52    fn from(value: Option<String>) -> Self {
53        match value {
54            Some(name) => PluginInstanceName::Named { name },
55            None => PluginInstanceName::Mono,
56        }
57    }
58}
59
60impl From<String> for PluginInstanceName {
61    fn from(value: String) -> Self {
62        Some(value).into()
63    }
64}
65
66#[cfg_attr(feature = "typegen", derive(ts_rs::TS), ts(export))]
67#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
68pub struct PluginInstanceId {
69    pub code: Cow<'static, str>,
70    #[serde(flatten)]
71    pub name: PluginInstanceName,
72}
73
74impl std::fmt::Display for PluginInstanceId {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        write!(f, "{}-{}", self.code, self.name)
77    }
78}
79
80impl From<PluginConfig> for PluginInstanceId {
81    fn from(value: PluginConfig) -> Self {
82        value.id
83    }
84}
85
86impl PluginInstanceId {
87    pub fn new(code: impl Into<Cow<'static, str>>, name: PluginInstanceName) -> Self {
88        PluginInstanceId { code: code.into(), name }
89    }
90    pub fn parse_by_code(code: impl Into<Cow<'static, str>>, id: &str) -> Result<Self, BoxError> {
91        let code = code.into();
92        let name = id.strip_prefix(code.as_ref()).ok_or("unmatched code")?.trim_matches('-').parse()?;
93        Ok(PluginInstanceId { code, name })
94    }
95    pub fn as_file_stem(&self) -> String {
96        match &self.name {
97            PluginInstanceName::Anon { uid } => format!("{}.{}", self.code, uid),
98            PluginInstanceName::Named { ref name } => format!("{}.{}", self.code, name),
99            PluginInstanceName::Mono => self.code.to_string(),
100        }
101    }
102    pub fn from_file_stem(stem: &str) -> Self {
103        let mut iter = stem.split('.');
104        let code = iter.next().expect("should have the first part").to_string();
105        if let Some(name) = iter.next() {
106            Self::new(code, PluginInstanceName::named(name))
107        } else {
108            Self::new(code, PluginInstanceName::mono())
109        }
110    }
111}
112
113impl Display for PluginInstanceName {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        match &self {
116            PluginInstanceName::Anon { uid } => write!(f, "a-{}", uid),
117            PluginInstanceName::Named { name } => write!(f, "n-{}", name),
118            PluginInstanceName::Mono => write!(f, "m"),
119        }
120    }
121}
122
123impl FromStr for PluginInstanceName {
124    type Err = BoxError;
125
126    fn from_str(s: &str) -> Result<Self, Self::Err> {
127        if s.is_empty() {
128            return Err("empty string".into());
129        }
130        let mut parts = s.splitn(2, '-');
131        match parts.next() {
132            Some("a") => {
133                let uid = parts.next().ok_or("missing uid")?;
134                Ok(PluginInstanceName::Anon { uid: uid.to_string() })
135            }
136            Some("n") => {
137                let name = parts.next().ok_or("missing name")?;
138                if name.is_empty() {
139                    return Err("empty name".into());
140                }
141                Ok(PluginInstanceName::Named { name: name.into() })
142            }
143            Some("g") => Ok(PluginInstanceName::Mono {}),
144            _ => Err("invalid prefix".into()),
145        }
146    }
147}
148
149#[cfg_attr(feature = "typegen", derive(ts_rs::TS), ts(export))]
150#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
151pub struct PluginConfig {
152    #[serde(flatten)]
153    pub id: PluginInstanceId,
154    pub spec: Value,
155}
156
157impl PluginConfig {
158    pub fn new(id: impl Into<PluginInstanceId>, spec: Value) -> Self {
159        Self { id: id.into(), spec }
160    }
161    pub fn code(&self) -> &str {
162        &self.id.code
163    }
164    pub fn name(&self) -> &PluginInstanceName {
165        &self.id.name
166    }
167}
168
169impl Hash for PluginConfig {
170    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
171        self.id.hash(state);
172        self.spec.to_string().hash(state);
173    }
174}
175
176#[derive(Debug, Clone, Default)]
177pub struct PluginInstanceMap {
178    // #[cfg_attr(feature = "typegen", ts(type = "Record<string, PluginConfig>"))]
179    plugins: HashMap<PluginInstanceId, Value>,
180}
181
182impl PluginInstanceMap {
183    pub fn into_config_vec(self) -> Vec<PluginConfig> {
184        self.plugins.into_iter().map(|(k, v)| PluginConfig { id: k, spec: v }).collect()
185    }
186    pub fn from_config_vec(vec: Vec<PluginConfig>) -> Self {
187        let map = vec.into_iter().map(|v| (v.id.clone(), v.spec)).collect();
188        PluginInstanceMap { plugins: map }
189    }
190}
191
192#[cfg_attr(feature = "typegen", derive(ts_rs::TS), ts(export, rename = "PluginInstanceMap"))]
193#[allow(dead_code)]
194pub(crate) struct PluginInstanceMapTs {
195    #[allow(dead_code)]
196    plugins: HashMap<String, PluginConfig>,
197}
198
199impl PluginInstanceMap {
200    pub fn new(plugins: HashMap<PluginInstanceId, Value>) -> Self {
201        PluginInstanceMap { plugins }
202    }
203    pub fn into_inner(self) -> HashMap<PluginInstanceId, Value> {
204        self.plugins
205    }
206}
207
208impl Deref for PluginInstanceMap {
209    type Target = HashMap<PluginInstanceId, Value>;
210
211    fn deref(&self) -> &Self::Target {
212        &self.plugins
213    }
214}
215
216impl DerefMut for PluginInstanceMap {
217    fn deref_mut(&mut self) -> &mut Self::Target {
218        &mut self.plugins
219    }
220}
221
222impl Serialize for PluginInstanceMap {
223    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
224    where
225        S: serde::Serializer,
226    {
227        let map = self.plugins.iter().map(|(k, v)| (k.to_string(), PluginConfig { id: k.clone(), spec: v.clone() })).collect::<HashMap<String, PluginConfig>>();
228        map.serialize(serializer)
229    }
230}
231
232impl<'de> Deserialize<'de> for PluginInstanceMap {
233    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
234    where
235        D: serde::Deserializer<'de>,
236    {
237        let map = HashMap::<String, PluginConfig>::deserialize(deserializer)?;
238        let map = map
239            .into_iter()
240            .filter_map(|(k, v)| match PluginInstanceId::parse_by_code(v.id.code.clone(), &k) {
241                Ok(id) => Some((id, v.spec)),
242                Err(e) => {
243                    eprintln!("failed to parse plugin instance id: {}", e);
244                    None
245                }
246            })
247            .collect();
248        Ok(PluginInstanceMap { plugins: map })
249    }
250}
251
252impl FromIterator<(PluginInstanceId, Value)> for PluginInstanceMap {
253    fn from_iter<T: IntoIterator<Item = (PluginInstanceId, Value)>>(iter: T) -> Self {
254        let map = iter.into_iter().collect();
255        PluginInstanceMap { plugins: map }
256    }
257}
258
259/// Plugin meta information
260#[derive(Debug, Default, Serialize, Deserialize, Clone)]
261#[cfg_attr(feature = "typegen", derive(ts_rs::TS), ts(export))]
262pub struct PluginMetaData {
263    pub authors: Option<Cow<'static, str>>,
264    pub description: Option<Cow<'static, str>>,
265    pub version: Option<Cow<'static, str>>,
266    pub homepage: Option<Cow<'static, str>>,
267    pub repository: Option<Cow<'static, str>>,
268}
269
270#[macro_export]
271macro_rules! plugin_meta {
272    () => {
273        {
274            $crate::PluginMetaData {
275                authors: Some(env!("CARGO_PKG_AUTHORS").into()),
276                version: Some(env!("CARGO_PKG_VERSION").into()),
277                description: Some(env!("CARGO_PKG_DESCRIPTION").into()),
278                homepage: Some(env!("CARGO_PKG_HOMEPAGE").into()),
279                repository: Some(env!("CARGO_PKG_REPOSITORY").into()),
280            }
281        }
282    };
283    ($($key:ident: $value:expr),*) => {
284        {
285            let mut meta = $crate::plugin_meta!();
286            $(
287                meta.$key = Some($value.into());
288            )*
289            meta
290        }
291    };
292
293}
294
295#[derive(Debug, Default, Serialize, Deserialize, Clone)]
296#[cfg_attr(feature = "typegen", derive(ts_rs::TS), ts(export))]
297pub struct PluginAttributes {
298    pub meta: PluginMetaData,
299    pub mono: bool,
300    pub code: Cow<'static, str>,
301}
302#[cfg(test)]
303mod test {
304    use super::*;
305    use serde_json::json;
306    #[test]
307    fn test_inst_map_serde() {
308        let mut map = PluginInstanceMap::default();
309        let id_1 = PluginInstanceId {
310            code: "header-modifier".into(),
311            name: PluginInstanceName::anon(0),
312        };
313        map.insert(id_1.clone(), json!(null));
314        let id_2 = PluginInstanceId {
315            code: "header-modifier".into(),
316            name: PluginInstanceName::anon(1),
317        };
318        map.insert(id_2.clone(), json!(null));
319
320        let ser = serde_json::to_string(&map).unwrap();
321        println!("{}", ser);
322        let de: PluginInstanceMap = serde_json::from_str(&ser).unwrap();
323        assert_eq!(map.get(&id_1), de.get(&id_1));
324        assert_eq!(map.get(&id_2), de.get(&id_2));
325    }
326
327    #[test]
328    fn test_parse_name() {
329        assert_eq!("a-1".parse::<PluginInstanceName>().unwrap(), PluginInstanceName::anon(1));
330        assert_eq!("n-my-plugin".parse::<PluginInstanceName>().unwrap(), PluginInstanceName::Named { name: "my-plugin".into() });
331        assert_eq!("g".parse::<PluginInstanceName>().unwrap(), PluginInstanceName::Mono {});
332        assert_ne!(
333            "n-my-plugin".parse::<PluginInstanceName>().unwrap(),
334            PluginInstanceName::Named { name: "my-plugin2".into() }
335        );
336        assert_ne!("g".parse::<PluginInstanceName>().unwrap(), PluginInstanceName::anon(1));
337        assert!("".parse::<PluginInstanceName>().is_err());
338        // assert!("a-".parse::<PluginInstanceName>().is_err());
339        assert!("n-".parse::<PluginInstanceName>().is_err());
340        // assert!("n-my-plugin-".parse::<PluginInstanceName>().is_err());
341        assert!("g-".parse::<PluginInstanceName>().is_ok());
342    }
343
344    #[test]
345    fn test_parse_id() {
346        let json = r#"{"code": "header-modifier", "kind" : "named", "name" : "hello" }"#;
347        let id: PluginInstanceId = serde_json::from_str(json).expect("fail to deserialize");
348        println!("{id:?}");
349    }
350    #[test]
351    fn test_dec() {
352        let config = json!(
353            {
354                "code": "header-modifier",
355                "kind": "anon",
356                "uid": '0',
357                "spec": null
358            }
359        );
360        let cfg = PluginConfig::deserialize(config).unwrap();
361        assert_eq!(cfg.id.code, "header-modifier");
362        assert_eq!(cfg.id.name, PluginInstanceName::anon(0));
363
364        let config = json!(
365            {
366                "code": "header-modifier",
367                "spec": null,
368                "kind": "mono",
369            }
370        );
371
372        let cfg = PluginConfig::deserialize(config).unwrap();
373        assert_eq!(cfg.id.code, "header-modifier");
374        assert_eq!(cfg.id.name, PluginInstanceName::Mono {});
375
376        let config = json!(
377            {
378                "code": "header-modifier",
379                "name": "my-header-modifier",
380                "kind": "named",
381                "spec": null
382            }
383        );
384
385        let cfg = PluginConfig::deserialize(config).unwrap();
386        assert_eq!(cfg.id.code, "header-modifier");
387    }
388}