1use alloc::string::{String, ToString};
20use alloc::vec::Vec;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25#[non_exhaustive]
26pub enum PropKind {
27 Bool,
28 Int,
30 Uint,
32 Double,
34 Fraction,
36 Str,
38 Flags,
43}
44
45impl PropKind {
46 pub fn label(self) -> &'static str {
50 match self {
51 PropKind::Bool => "Boolean",
52 PropKind::Int => "Integer",
53 PropKind::Uint => "Unsigned Integer",
54 PropKind::Double => "Double",
55 PropKind::Fraction => "Fraction",
56 PropKind::Str => "String",
57 PropKind::Flags => "Flags",
58 }
59 }
60}
61
62#[derive(Debug, Clone, PartialEq)]
64#[non_exhaustive]
65pub enum PropValue {
66 Bool(bool),
67 Int(i64),
68 Uint(u64),
69 Double(f64),
70 Fraction(i32, i32),
72 Str(String),
73 Flags(Vec<String>),
75}
76
77impl PropValue {
78 pub fn kind(&self) -> PropKind {
80 match self {
81 PropValue::Bool(_) => PropKind::Bool,
82 PropValue::Int(_) => PropKind::Int,
83 PropValue::Uint(_) => PropKind::Uint,
84 PropValue::Double(_) => PropKind::Double,
85 PropValue::Fraction(_, _) => PropKind::Fraction,
86 PropValue::Str(_) => PropKind::Str,
87 PropValue::Flags(_) => PropKind::Flags,
88 }
89 }
90
91 pub fn parse(kind: PropKind, text: &str) -> Result<PropValue, PropError> {
96 let t = text.trim();
97 match kind {
98 PropKind::Bool => match t.to_ascii_lowercase().as_str() {
101 "true" | "1" | "yes" => Ok(PropValue::Bool(true)),
102 "false" | "0" | "no" => Ok(PropValue::Bool(false)),
103 _ => Err(PropError::Value),
104 },
105 PropKind::Int => t
106 .parse::<i64>()
107 .map(PropValue::Int)
108 .map_err(|_| PropError::Value),
109 PropKind::Uint => t
110 .parse::<u64>()
111 .map(PropValue::Uint)
112 .map_err(|_| PropError::Value),
113 PropKind::Double => t
114 .parse::<f64>()
115 .map(PropValue::Double)
116 .map_err(|_| PropError::Value),
117 PropKind::Fraction => match t.split_once('/') {
118 Some((n, d)) => {
119 let n = n.trim().parse::<i32>().map_err(|_| PropError::Value)?;
120 let d = d.trim().parse::<i32>().map_err(|_| PropError::Value)?;
121 if d == 0 {
122 return Err(PropError::Value);
123 }
124 Ok(PropValue::Fraction(n, d))
125 }
126 None => {
127 let n = t.parse::<i32>().map_err(|_| PropError::Value)?;
128 Ok(PropValue::Fraction(n, 1))
129 }
130 },
131 PropKind::Str => Ok(PropValue::Str(t.to_string())),
132 PropKind::Flags => {
133 let mut set = Vec::new();
134 for nick in t.split('+') {
135 let nick = nick.trim();
136 if nick.is_empty() {
137 return Err(PropError::Value);
138 }
139 set.push(nick.to_string());
140 }
141 Ok(PropValue::Flags(set))
142 }
143 }
144 }
145
146 pub fn as_bool(&self) -> Option<bool> {
148 match self {
149 PropValue::Bool(b) => Some(*b),
150 _ => None,
151 }
152 }
153
154 pub fn as_int(&self) -> Option<i64> {
156 match self {
157 PropValue::Int(v) => Some(*v),
158 _ => None,
159 }
160 }
161
162 pub fn as_uint(&self) -> Option<u64> {
164 match self {
165 PropValue::Uint(v) => Some(*v),
166 _ => None,
167 }
168 }
169
170 pub fn as_double(&self) -> Option<f64> {
172 match self {
173 PropValue::Double(v) => Some(*v),
174 _ => None,
175 }
176 }
177
178 pub fn as_fraction(&self) -> Option<(i32, i32)> {
180 match self {
181 PropValue::Fraction(n, d) => Some((*n, *d)),
182 _ => None,
183 }
184 }
185
186 pub fn as_str(&self) -> Option<&str> {
188 match self {
189 PropValue::Str(s) => Some(s),
190 _ => None,
191 }
192 }
193
194 pub fn as_flags(&self) -> Option<&[String]> {
198 match self {
199 PropValue::Flags(set) => Some(set),
200 _ => None,
201 }
202 }
203
204 pub fn has_flag(&self, nick: &str) -> bool {
207 self.as_flags()
208 .is_some_and(|set| set.iter().any(|n| n == nick))
209 }
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub struct PropFlags {
217 pub readable: bool,
218 pub writable: bool,
219}
220
221impl PropFlags {
222 pub const READWRITE: Self = Self {
224 readable: true,
225 writable: true,
226 };
227 pub const READ_ONLY: Self = Self {
229 readable: true,
230 writable: false,
231 };
232}
233
234impl Default for PropFlags {
235 fn default() -> Self {
236 Self::READWRITE
237 }
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252pub struct PropertySpec {
253 pub name: &'static str,
255 pub kind: PropKind,
257 pub blurb: &'static str,
259 pub default: Option<&'static str>,
262 pub range: Option<(&'static str, &'static str)>,
264 pub enum_values: Option<&'static str>,
272 pub flags: PropFlags,
274}
275
276pub const UNDECLARED_PROPERTIES: &str = "*";
281
282pub fn takes_undeclared_properties(specs: &[PropertySpec]) -> bool {
284 specs.iter().any(|s| s.name == UNDECLARED_PROPERTIES)
285}
286
287impl PropertySpec {
288 pub const fn undeclared(blurb: &'static str) -> Self {
296 Self::new(UNDECLARED_PROPERTIES, PropKind::Str, blurb)
297 }
298
299 pub const fn new(name: &'static str, kind: PropKind, blurb: &'static str) -> Self {
302 Self {
303 name,
304 kind,
305 blurb,
306 default: None,
307 range: None,
308 enum_values: None,
309 flags: PropFlags::READWRITE,
310 }
311 }
312
313 pub const fn with_default(mut self, default: &'static str) -> Self {
315 self.default = Some(default);
316 self
317 }
318
319 pub const fn with_range(mut self, min: &'static str, max: &'static str) -> Self {
321 self.range = Some((min, max));
322 self
323 }
324
325 pub const fn with_enum_values(mut self, values: &'static str) -> Self {
327 self.enum_values = Some(values);
328 self
329 }
330
331 pub const fn read_only(mut self) -> Self {
333 self.flags = PropFlags::READ_ONLY;
334 self
335 }
336
337 pub fn enum_nicks(&self) -> impl Iterator<Item = &'static str> {
341 self.enum_values
342 .unwrap_or("")
343 .split('|')
344 .filter_map(|entry| entry.split_whitespace().next())
345 }
346
347 pub fn parse_value(&self, text: &str) -> Result<PropValue, ValueError> {
353 let value = PropValue::parse(self.kind, text).map_err(|e| {
354 if self.kind == PropKind::Flags {
357 ValueError::Nick(text.trim().to_string())
358 } else {
359 ValueError::Kind(e)
360 }
361 })?;
362 if self.enum_values.is_none() {
363 return Ok(value);
364 }
365 let nicks: &[String] = match &value {
366 PropValue::Str(s) => core::slice::from_ref(s),
367 PropValue::Flags(set) => set,
368 _ => return Ok(value),
370 };
371 for nick in nicks {
372 if !self.enum_nicks().any(|d| d == nick) {
373 return Err(ValueError::Nick(nick.clone()));
374 }
375 }
376 Ok(value)
377 }
378}
379
380#[derive(Debug, Clone, PartialEq, Eq)]
382pub enum ValueError {
383 Kind(PropError),
385 Nick(String),
389}
390
391impl core::fmt::Display for ValueError {
392 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
393 match self {
394 ValueError::Kind(e) => write!(f, "{e}"),
395 ValueError::Nick(n) => write!(f, "unknown value '{n}'"),
396 }
397 }
398}
399
400#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
407pub struct ElementMetadata {
408 pub long_name: &'static str,
410 pub klass: &'static str,
412 pub description: &'static str,
414 pub author: &'static str,
416}
417
418impl ElementMetadata {
419 pub const fn new(
421 long_name: &'static str,
422 klass: &'static str,
423 description: &'static str,
424 author: &'static str,
425 ) -> Self {
426 Self {
427 long_name,
428 klass,
429 description,
430 author,
431 }
432 }
433
434 pub fn is_set(&self) -> bool {
436 !(self.long_name.is_empty()
437 && self.klass.is_empty()
438 && self.description.is_empty()
439 && self.author.is_empty())
440 }
441}
442
443#[derive(Debug, Clone, Copy, PartialEq, Eq)]
446pub enum PropError {
447 Unknown,
449 Type,
451 Value,
453 ReadOnly,
455}
456
457impl core::fmt::Display for PropError {
458 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
459 let s = match self {
460 PropError::Unknown => "unknown property",
461 PropError::Type => "property type mismatch",
462 PropError::Value => "invalid property value",
463 PropError::ReadOnly => "read-only property",
464 };
465 f.write_str(s)
466 }
467}
468
469fn kind_label(kind: PropKind) -> &'static str {
471 kind.label()
472}
473
474pub fn format_specs(specs: &[PropertySpec]) -> String {
479 use core::fmt::Write;
480 let mut out = String::new();
481 for s in specs {
482 let _ = writeln!(out, " {}: {}", s.name, s.blurb);
483 let flags = match (s.flags.readable, s.flags.writable) {
484 (true, true) => "readable, writable",
485 (true, false) => "readable",
486 (false, true) => "writable",
487 (false, false) => "",
488 };
489 let _ = writeln!(out, " flags: {flags}");
490 let _ = write!(out, " {}", kind_label(s.kind));
491 if let Some((min, max)) = s.range {
492 let _ = write!(out, ". Range: {min} - {max}");
493 }
494 if let Some(values) = s.enum_values {
495 let _ = write!(out, ". Values: {values}");
496 }
497 if let Some(default) = s.default {
498 let _ = write!(out, ". Default: {default}");
499 }
500 out.push('\n');
501 }
502 out
503}
504
505pub fn format_metadata(name: &str, meta: &ElementMetadata) -> String {
509 use core::fmt::Write;
510 let mut out = String::new();
511 let _ = writeln!(out, "Factory Details:");
512 let _ = writeln!(out, " Name {name}");
513 if !meta.long_name.is_empty() {
514 let _ = writeln!(out, " Long-name {}", meta.long_name);
515 }
516 if !meta.klass.is_empty() {
517 let _ = writeln!(out, " Klass {}", meta.klass);
518 }
519 if !meta.description.is_empty() {
520 let _ = writeln!(out, " Description {}", meta.description);
521 }
522 if !meta.author.is_empty() {
523 let _ = writeln!(out, " Author {}", meta.author);
524 }
525 out
526}
527
528pub fn spec_names(specs: &[PropertySpec]) -> Vec<&'static str> {
530 specs.iter().map(|s| s.name).collect()
531}
532
533#[cfg(test)]
534mod tests {
535 use super::*;
536
537 #[test]
538 fn parse_matches_kind() {
539 assert_eq!(
540 PropValue::parse(PropKind::Bool, "true").unwrap(),
541 PropValue::Bool(true)
542 );
543 assert_eq!(
544 PropValue::parse(PropKind::Bool, "0").unwrap(),
545 PropValue::Bool(false)
546 );
547 assert_eq!(
548 PropValue::parse(PropKind::Bool, "True").unwrap(),
549 PropValue::Bool(true)
550 );
551 assert_eq!(
552 PropValue::parse(PropKind::Bool, "FALSE").unwrap(),
553 PropValue::Bool(false)
554 );
555 assert_eq!(
556 PropValue::parse(PropKind::Int, "-7").unwrap(),
557 PropValue::Int(-7)
558 );
559 assert_eq!(
560 PropValue::parse(PropKind::Uint, "42").unwrap(),
561 PropValue::Uint(42)
562 );
563 assert_eq!(
564 PropValue::parse(PropKind::Fraction, "30/1").unwrap(),
565 PropValue::Fraction(30, 1)
566 );
567 assert_eq!(
569 PropValue::parse(PropKind::Fraction, "25").unwrap(),
570 PropValue::Fraction(25, 1)
571 );
572 assert_eq!(
573 PropValue::parse(PropKind::Str, "file.mp4").unwrap(),
574 PropValue::Str("file.mp4".into())
575 );
576 }
577
578 #[test]
579 fn parse_rejects_bad_values() {
580 assert_eq!(PropValue::parse(PropKind::Int, "x"), Err(PropError::Value));
581 assert_eq!(
582 PropValue::parse(PropKind::Uint, "-1"),
583 Err(PropError::Value)
584 );
585 assert_eq!(
586 PropValue::parse(PropKind::Fraction, "1/0"),
587 Err(PropError::Value)
588 );
589 assert_eq!(
590 PropValue::parse(PropKind::Bool, "maybe"),
591 Err(PropError::Value)
592 );
593 }
594
595 #[test]
596 fn flag_set_parses_into_nicks() {
597 assert_eq!(
598 PropValue::parse(PropKind::Flags, "video+audio").unwrap(),
599 PropValue::Flags(alloc::vec!["video".into(), "audio".into()])
600 );
601 assert_eq!(
603 PropValue::parse(PropKind::Flags, "video + audio").unwrap(),
604 PropValue::Flags(alloc::vec!["video".into(), "audio".into()])
605 );
606 let v = PropValue::parse(PropKind::Flags, "video+audio").unwrap();
607 assert!(v.has_flag("audio") && !v.has_flag("text"));
608 assert_eq!(v.as_flags().unwrap().len(), 2);
609 }
610
611 #[test]
612 fn malformed_flag_set_is_rejected() {
613 for bad in ["video+", "+video", "video++audio", ""] {
614 assert_eq!(
615 PropValue::parse(PropKind::Flags, bad),
616 Err(PropError::Value),
617 "{bad} must not parse"
618 );
619 }
620 }
621
622 #[test]
623 fn spec_validates_enum_nicks() {
624 let spec = PropertySpec::new("backend", PropKind::Str, "encoder")
625 .with_enum_values("nvenc | software");
626 assert_eq!(
627 spec.parse_value("software").unwrap(),
628 PropValue::Str("software".into())
629 );
630 assert_eq!(
631 spec.parse_value("nvidia"),
632 Err(ValueError::Nick("nvidia".into()))
633 );
634 let free = PropertySpec::new("location", PropKind::Str, "path");
636 assert!(free.parse_value("anything").is_ok());
637 }
638
639 #[test]
640 fn spec_validates_each_flag_nick() {
641 let spec = PropertySpec::new("protocols", PropKind::Flags, "transports")
642 .with_enum_values("udp | tcp");
643 assert_eq!(
644 spec.parse_value("udp+tcp").unwrap(),
645 PropValue::Flags(alloc::vec!["udp".into(), "tcp".into()])
646 );
647 assert_eq!(
649 spec.parse_value("udp+quic"),
650 Err(ValueError::Nick("quic".into()))
651 );
652 assert_eq!(
654 spec.parse_value("udp+"),
655 Err(ValueError::Nick("udp+".into()))
656 );
657 assert_eq!(spec.enum_nicks().collect::<Vec<_>>(), ["udp", "tcp"]);
658 }
659
660 #[test]
661 fn numeric_enum_values_stay_documentation() {
662 let spec = PropertySpec::new("frame-size", PropKind::Uint, "ms")
665 .with_enum_values("2 (2.5 ms) | 5 | 10");
666 assert_eq!(spec.parse_value("20").unwrap(), PropValue::Uint(20));
667 assert_eq!(
668 spec.parse_value("x"),
669 Err(ValueError::Kind(PropError::Value))
670 );
671 }
672
673 #[test]
674 fn kind_round_trips_value() {
675 assert_eq!(PropValue::Int(3).kind(), PropKind::Int);
676 assert_eq!(PropValue::Fraction(30, 1).kind(), PropKind::Fraction);
677 assert_eq!(PropValue::Str("x".into()).kind(), PropKind::Str);
678 }
679
680 #[test]
681 fn format_specs_details_each_property() {
682 let specs = [
683 PropertySpec::new("pattern", PropKind::Str, "test pattern")
684 .with_enum_values("smpte | snow | ball")
685 .with_default("smpte"),
686 PropertySpec::new(
687 "num-buffers",
688 PropKind::Int,
689 "frames then EOS (-1 = forever)",
690 )
691 .with_range("-1", "9223372036854775807")
692 .with_default("-1"),
693 ];
694 let dump = format_specs(&specs);
695 assert!(dump.contains("pattern: test pattern"), "got:\n{dump}");
697 assert!(dump.contains("flags: readable, writable"));
699 assert!(dump.contains("String. Values: smpte | snow | ball. Default: smpte"));
700 assert!(dump.contains("Integer. Range: -1 - 9223372036854775807. Default: -1"));
701 assert_eq!(spec_names(&specs), ["pattern", "num-buffers"]);
702 }
703
704 #[test]
705 fn read_only_flag_renders() {
706 let specs = [PropertySpec::new("dropped", PropKind::Uint, "frames dropped").read_only()];
707 assert!(format_specs(&specs).contains("flags: readable\n"));
708 }
709
710 #[test]
711 fn metadata_block_omits_empty_fields() {
712 let meta = ElementMetadata::new("Opus encoder", "Codec/Encoder/Audio", "", "g2g");
713 let dump = format_metadata("opusenc", &meta);
714 assert!(dump.contains("Name opusenc"));
715 assert!(dump.contains("Long-name Opus encoder"));
716 assert!(dump.contains("Klass Codec/Encoder/Audio"));
717 assert!(dump.contains("Author g2g"));
718 assert!(!dump.contains("Description"), "empty description omitted");
719 assert!(!ElementMetadata::default().is_set());
720 assert!(meta.is_set());
721 }
722}