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: 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 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#[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!("n-".parse::<PluginInstanceName>().is_err());
340 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}