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).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 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 pub fn store_id(&self) -> String {
64 self.package.store_id()
65 }
66}
67
68#[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 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 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 pkg.group = Some(first_token);
160 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 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 pkg.name = second_token;
178 Ok(pkg)
179 }
180 } else {
181 pkg.name = first_token;
183 Ok(pkg)
184 }
185 }
186
187 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 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#[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 = ¶ms.get_param("multiplier").unwrap();
540 assert_eq!(input1.description.as_ref().unwrap(), "multiply input");
541 assert!(!input1.optional);
542 }
543}