1use 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 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 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 pub fn store_id(&self) -> String {
58 self.package.store_id()
59 }
60}
61
62#[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 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 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 pkg.group = Some(first_token);
154 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 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 pkg.name = second_token;
172 Ok(pkg)
173 }
174 } else {
175 pkg.name = first_token;
177 Ok(pkg)
178 }
179 }
180
181 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 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#[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 = ¶ms.get_param("multiplier").unwrap();
520 assert_eq!(input1.description.as_ref().unwrap(), "multiply input");
521 assert!(!input1.optional);
522 }
523}