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