fluvio_controlplane_metadata/smartmodule/
package.rs

1//!
2//! # SmartModule Package
3//!
4
5use std::{
6    io::Error as IoError,
7    fmt::{Display, Formatter},
8};
9
10use bytes::Buf;
11use semver::Version as SemVersion;
12use thiserror::Error;
13
14use fluvio_protocol::{Encoder, Decoder, Version};
15
16use super::params::SmartModuleParams;
17
18#[derive(Debug, Default, Clone, PartialEq, Eq, Encoder, Decoder)]
19#[cfg_attr(feature = "use_serde", derive(serde::Serialize, serde::Deserialize))]
20pub struct SmartModuleMetadata {
21    pub package: SmartModulePackage,
22    pub params: SmartModuleParams,
23}
24
25impl SmartModuleMetadata {
26    #[cfg(feature = "smartmodule")]
27    /// parse the metadata file and return the metadata
28    pub fn from_toml<T: AsRef<std::path::Path>>(path: T) -> std::io::Result<Self> {
29        use std::fs::read_to_string;
30
31        let path_ref = path.as_ref();
32        let file_str: String = read_to_string(path_ref)?;
33        let metadata = toml::from_str(&file_str).map_err(|err| {
34            IoError::new(
35                std::io::ErrorKind::InvalidData,
36                format!("invalid toml: {err}"),
37            )
38        })?;
39        Ok(metadata)
40    }
41
42    #[cfg(feature = "smartmodule")]
43    /// parse the metadata bytes and return the metadata
44    pub fn from_bytes(bytedata: &[u8]) -> std::io::Result<Self> {
45        let strdata = std::str::from_utf8(bytedata)
46            .map_err(|_| IoError::new(std::io::ErrorKind::InvalidData, "cant convert to utf8"))?;
47        let metadata = toml::from_str(strdata).map_err(|err| {
48            IoError::new(
49                std::io::ErrorKind::InvalidData,
50                format!("invalid toml: {err}"),
51            )
52        })?;
53        Ok(metadata)
54    }
55
56    /// id that can be used to identify this smartmodule
57    pub fn store_id(&self) -> String {
58        self.package.store_id()
59    }
60}
61
62/// SmartModule package definition
63/// This is defined in the `SmartModule.toml` in the root of the SmartModule project
64#[derive(Debug, Default, Clone, PartialEq, Eq, Encoder, Decoder)]
65#[cfg_attr(
66    feature = "use_serde",
67    derive(serde::Serialize, serde::Deserialize),
68    serde(rename_all = "camelCase")
69)]
70pub struct SmartModulePackage {
71    pub name: String,
72    pub group: String,
73    pub version: FluvioSemVersion,
74    pub api_version: FluvioSemVersion,
75    pub description: Option<String>,
76    pub license: Option<String>,
77
78    #[fluvio(min_version = 19)]
79    #[cfg_attr(
80        feature = "use_serde",
81        serde(default = "SmartModulePackage::visibility_if_missing")
82    )]
83    pub visibility: SmartModuleVisibility,
84    pub repository: Option<String>,
85}
86
87impl SmartModulePackage {
88    /// id that can be used to identify this smartmodule
89    pub fn store_id(&self) -> String {
90        (SmartModulePackageKey {
91            name: self.name.clone(),
92            group: Some(self.group.clone()),
93            version: Some(self.version.clone()),
94        })
95        .store_id()
96    }
97
98    pub fn is_valid(&self) -> bool {
99        !self.name.is_empty() && !self.group.is_empty()
100    }
101
102    pub fn fqdn(&self) -> String {
103        format!(
104            "{}{}{}{}{}",
105            self.group, GROUP_SEPARATOR, self.name, VERSION_SEPARATOR, self.version
106        )
107    }
108
109    pub fn visibility_if_missing() -> SmartModuleVisibility {
110        SmartModuleVisibility::Private
111    }
112}
113
114#[derive(Debug, Error)]
115pub enum SmartModuleKeyError {
116    #[error("SmartModule version`{version}` is not valid because {error}")]
117    InvalidVersion { version: String, error: String },
118}
119
120#[derive(Debug, Default, Clone, PartialEq, Eq, Encoder, Decoder)]
121#[cfg_attr(
122    feature = "use_serde",
123    derive(serde::Serialize, serde::Deserialize),
124    serde(rename_all = "lowercase")
125)]
126pub enum SmartModuleVisibility {
127    #[default]
128    #[fluvio(tag = 0)]
129    Private,
130    #[fluvio(tag = 1)]
131    Public,
132}
133
134#[derive(Default)]
135pub struct SmartModulePackageKey {
136    pub name: String,
137    pub group: Option<String>,
138    pub version: Option<FluvioSemVersion>,
139}
140
141const GROUP_SEPARATOR: char = '/';
142const VERSION_SEPARATOR: char = '@';
143
144impl SmartModulePackageKey {
145    /// convert from qualified name into package info
146    /// qualified name is in format of "group/name@version"
147    pub fn from_qualified_name(fqdn: &str) -> Result<Self, SmartModuleKeyError> {
148        let mut pkg = Self::default();
149        let mut split = fqdn.split(GROUP_SEPARATOR);
150        let first_token = split.next().unwrap().to_owned();
151        if let Some(name_part) = split.next() {
152            // group name is found
153            pkg.group = Some(first_token);
154            // let split name part
155            let mut version_split = name_part.split(VERSION_SEPARATOR);
156            let second_token = version_split.next().unwrap().to_owned();
157            if let Some(version_part) = version_split.next() {
158                // version is found
159                pkg.name = second_token;
160                pkg.version = Some(FluvioSemVersion::new(
161                    lenient_semver::parse(version_part).map_err(|err| {
162                        SmartModuleKeyError::InvalidVersion {
163                            version: version_part.to_owned(),
164                            error: err.to_string(),
165                        }
166                    })?,
167                ));
168                Ok(pkg)
169            } else {
170                // no version found
171                pkg.name = second_token;
172                Ok(pkg)
173            }
174        } else {
175            // no group parameter is specified, in this case we treat as name
176            pkg.name = first_token;
177            Ok(pkg)
178        }
179    }
180
181    /// Check if key matches against name and package
182    /// if package doesn't exists then it should match name only
183    /// otherwise it should match against package
184    pub fn is_match(&self, name: &str, package: Option<&SmartModulePackage>) -> bool {
185        if let Some(package) = package {
186            if let Some(version) = &self.version {
187                if package.version != *version {
188                    return false;
189                }
190            }
191
192            if let Some(group) = &self.group {
193                if package.group != *group {
194                    return false;
195                }
196            }
197
198            self.name == package.name
199        } else {
200            self.name == name
201        }
202    }
203
204    /// return key for storing SmartModule in the store
205    pub fn store_id(&self) -> String {
206        let group_id = if let Some(package) = &self.group {
207            format!("-{package}")
208        } else {
209            "".to_owned()
210        };
211
212        let version_id = if let Some(version) = &self.version {
213            format!("-{version}")
214        } else {
215            "".to_owned()
216        };
217
218        format!("{}{}{}", self.name, group_id, version_id)
219    }
220}
221
222#[derive(Debug, Clone, PartialEq, Eq)]
223#[cfg_attr(feature = "use_serde", derive(serde::Serialize, serde::Deserialize))]
224pub struct FluvioSemVersion(SemVersion);
225
226impl FluvioSemVersion {
227    pub fn parse(version: &str) -> Result<Self, semver::Error> {
228        Ok(Self(SemVersion::parse(version)?))
229    }
230
231    pub fn new(version: SemVersion) -> Self {
232        Self(version)
233    }
234}
235
236impl Default for FluvioSemVersion {
237    fn default() -> Self {
238        Self(SemVersion::new(0, 1, 0))
239    }
240}
241
242impl Display for FluvioSemVersion {
243    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
244        write!(f, "{}", self.0)
245    }
246}
247
248impl Encoder for FluvioSemVersion {
249    fn write_size(&self, version: fluvio_protocol::Version) -> usize {
250        self.0.to_string().write_size(version)
251    }
252
253    fn encode<T>(
254        &self,
255        dest: &mut T,
256        version: fluvio_protocol::Version,
257    ) -> Result<(), std::io::Error>
258    where
259        T: bytes::BufMut,
260    {
261        self.0.to_string().encode(dest, version)
262    }
263}
264
265impl Decoder for FluvioSemVersion {
266    fn decode<T>(&mut self, src: &mut T, version: Version) -> Result<(), IoError>
267    where
268        T: Buf,
269    {
270        let mut version_str = String::from("");
271        version_str.decode(src, version)?;
272        let version = SemVersion::parse(&version_str)
273            .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
274        self.0 = version;
275        Ok(())
276    }
277}
278
279impl std::fmt::Display for SmartModuleVisibility {
280    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
281        let lbl = match self {
282            Self::Private => "private",
283            Self::Public => "public",
284        };
285        write!(f, "{lbl}")
286    }
287}
288
289impl std::convert::TryFrom<&str> for SmartModuleVisibility {
290    type Error = &'static str;
291
292    fn try_from(s: &str) -> Result<Self, Self::Error> {
293        match s {
294            "private" => Ok(SmartModuleVisibility::Private),
295            "public" => Ok(SmartModuleVisibility::Public),
296            _ => Err("Only private or public is allowed"),
297        }
298    }
299}
300
301/// Convert from name into something that can be used as key in the store
302/// For now, we respect
303#[cfg(test)]
304mod package_test {
305    use crate::smartmodule::SmartModulePackageKey;
306
307    use super::{SmartModulePackage, FluvioSemVersion};
308
309    #[test]
310    fn test_pkg_validation() {
311        assert!(SmartModulePackage {
312            name: "a".to_owned(),
313            group: "b".to_owned(),
314            version: FluvioSemVersion::parse("0.1.0").unwrap(),
315            api_version: FluvioSemVersion::parse("0.1.0").unwrap(),
316            ..Default::default()
317        }
318        .is_valid());
319
320        assert!(!SmartModulePackage {
321            name: "".to_owned(),
322            group: "b".to_owned(),
323            version: FluvioSemVersion::parse("0.1.0").unwrap(),
324            api_version: FluvioSemVersion::parse("0.1.0").unwrap(),
325            ..Default::default()
326        }
327        .is_valid());
328
329        assert!(!SmartModulePackage {
330            name: "c".to_owned(),
331            group: "".to_owned(),
332            version: FluvioSemVersion::parse("0.1.0").unwrap(),
333            api_version: FluvioSemVersion::parse("0.1.0").unwrap(),
334            ..Default::default()
335        }
336        .is_valid());
337
338        assert!(!SmartModulePackage {
339            name: "".to_owned(),
340            group: "".to_owned(),
341            version: FluvioSemVersion::parse("0.1.0").unwrap(),
342            api_version: FluvioSemVersion::parse("0.1.0").unwrap(),
343            ..Default::default()
344        }
345        .is_valid());
346    }
347
348    #[test]
349    fn test_pkg_fqdn() {
350        let pkg = SmartModulePackage {
351            name: "test".to_owned(),
352            group: "fluvio".to_owned(),
353            version: FluvioSemVersion::parse("0.1.0").unwrap(),
354            api_version: FluvioSemVersion::parse("0.1.0").unwrap(),
355            ..Default::default()
356        };
357
358        assert_eq!(pkg.fqdn(), "fluvio/test@0.1.0");
359    }
360
361    #[test]
362    fn test_pkg_name() {
363        let pkg = SmartModulePackage {
364            name: "test".to_owned(),
365            group: "fluvio".to_owned(),
366            version: FluvioSemVersion::parse("0.1.0").unwrap(),
367            api_version: FluvioSemVersion::parse("0.1.0").unwrap(),
368            ..Default::default()
369        };
370
371        assert_eq!(pkg.store_id(), "test-fluvio-0.1.0");
372    }
373
374    #[test]
375    fn test_pkg_key_fully_qualified() {
376        let pkg =
377            SmartModulePackageKey::from_qualified_name("mygroup/module1@0.1.0").expect("parse");
378        assert_eq!(pkg.name, "module1");
379        assert_eq!(pkg.group, Some("mygroup".to_owned()));
380        assert_eq!(
381            pkg.version,
382            Some(FluvioSemVersion::parse("0.1.0").expect("parse"))
383        );
384    }
385
386    #[test]
387    fn test_pkg_key_name_only() {
388        let pkg = SmartModulePackageKey::from_qualified_name("module2").expect("parse");
389        assert_eq!(pkg.name, "module2");
390        assert_eq!(pkg.group, None);
391        assert_eq!(pkg.version, None);
392    }
393
394    #[test]
395    fn test_pkg_key_group() {
396        let pkg = SmartModulePackageKey::from_qualified_name("group1/module2").expect("parse");
397        assert_eq!(pkg.name, "module2");
398        assert_eq!(pkg.group, Some("group1".to_owned()));
399        assert_eq!(pkg.version, None);
400    }
401
402    #[test]
403    fn test_pkg_key_versions() {
404        assert!(SmartModulePackageKey::from_qualified_name("group1/module2@10.").is_err());
405        assert!(SmartModulePackageKey::from_qualified_name("group1/module2@").is_err());
406        assert!(SmartModulePackageKey::from_qualified_name("group1/module2@10").is_ok());
407        assert!(SmartModulePackageKey::from_qualified_name("group1/module2@10.2").is_ok());
408    }
409
410    #[test]
411    fn test_pkg_key_match() {
412        let key =
413            SmartModulePackageKey::from_qualified_name("mygroup/module1@0.1.0").expect("parse");
414        let valid_pkg = SmartModulePackage {
415            name: "module1".to_owned(),
416            group: "mygroup".to_owned(),
417            version: FluvioSemVersion::parse("0.1.0").unwrap(),
418            api_version: FluvioSemVersion::parse("0.1.0").unwrap(),
419            ..Default::default()
420        };
421        assert!(key.is_match(&valid_pkg.store_id(), Some(&valid_pkg)));
422        assert!(
423            SmartModulePackageKey::from_qualified_name("mygroup/module1")
424                .expect("parse")
425                .is_match(&valid_pkg.store_id(), Some(&valid_pkg))
426        );
427        assert!(SmartModulePackageKey::from_qualified_name("module1")
428            .expect("parse")
429            .is_match(&valid_pkg.store_id(), Some(&valid_pkg)));
430        assert!(!SmartModulePackageKey::from_qualified_name("module2")
431            .expect("parse")
432            .is_match(&valid_pkg.store_id(), Some(&valid_pkg)));
433
434        let in_valid_pkg = SmartModulePackage {
435            name: "module2".to_owned(),
436            group: "mygroup".to_owned(),
437            version: FluvioSemVersion::parse("0.1.0").unwrap(),
438            api_version: FluvioSemVersion::parse("0.1.0").unwrap(),
439            ..Default::default()
440        };
441        assert!(!key.is_match(&in_valid_pkg.store_id(), Some(&in_valid_pkg)));
442
443        assert!(SmartModulePackageKey::from_qualified_name("module1")
444            .expect("parse")
445            .is_match("module1", None));
446    }
447
448    #[test]
449    fn test_pk_key_store_id() {
450        assert_eq!(
451            SmartModulePackageKey::from_qualified_name("module1")
452                .expect("parse")
453                .store_id(),
454            "module1"
455        );
456        assert_eq!(
457            SmartModulePackageKey::from_qualified_name("mygroup/module1@0.1")
458                .expect("parse")
459                .store_id(),
460            "module1-mygroup-0.1.0"
461        );
462    }
463}
464
465#[cfg(all(test, feature = "smartmodule"))]
466mod test {
467
468    use crate::smartmodule::params::{SmartModuleParams, SmartModuleParam};
469
470    use super::{FluvioSemVersion, SmartModulePackage};
471
472    #[test]
473    fn write_metadata_toml() {
474        let pkg = SmartModulePackage {
475            name: "test".to_owned(),
476            group: "group".to_owned(),
477            version: FluvioSemVersion::parse("0.1.0").unwrap(),
478            ..Default::default()
479        };
480
481        let param = SmartModuleParam {
482            optional: true,
483            description: Some("fluvio".to_owned()),
484        };
485        let mut params = SmartModuleParams::default();
486        params.insert_param("param1".to_owned(), param);
487        let metadata = super::SmartModuleMetadata {
488            package: pkg,
489            params,
490        };
491
492        let toml = toml::to_string(&metadata).expect("toml");
493        println!("{toml}");
494        assert!(toml.contains("param1"));
495    }
496
497    #[test]
498    fn read_metadata_toml() {
499        let metadata = super::SmartModuleMetadata::from_toml("tests/SmartModule.toml")
500            .expect("failed to parse metadata");
501        assert_eq!(metadata.package.name, "MyCustomModule");
502        assert_eq!(
503            metadata.package.version,
504            FluvioSemVersion::parse("0.1.0").unwrap()
505        );
506        assert_eq!(metadata.package.description.unwrap(), "My Custom module");
507        assert_eq!(
508            metadata.package.api_version,
509            FluvioSemVersion::parse("0.1.0").unwrap()
510        );
511        assert_eq!(metadata.package.license.unwrap(), "Apache-2.0");
512        assert_eq!(
513            metadata.package.repository.unwrap(),
514            "https://github.com/infinyon/fluvio"
515        );
516
517        let params = metadata.params;
518        assert_eq!(params.len(), 2);
519        let input1 = &params.get_param("multiplier").unwrap();
520        assert_eq!(input1.description.as_ref().unwrap(), "multiply input");
521        assert!(!input1.optional);
522    }
523}