Skip to main content

use_ml_label/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7pub mod prelude {
8    pub use crate::{
9        MlAnnotationKind, MlClassId, MlClassName, MlLabelCardinality, MlLabelError, MlLabelId,
10        MlLabelKind, MlLabelName, MlLabelQuality, MlLabelSource, MlTargetKind,
11    };
12}
13
14macro_rules! label_text_newtype {
15    ($name:ident) => {
16        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
17        pub struct $name(String);
18
19        impl $name {
20            pub fn new(value: impl AsRef<str>) -> Result<Self, MlLabelError> {
21                non_empty_text(value).map(Self)
22            }
23
24            pub fn as_str(&self) -> &str {
25                &self.0
26            }
27        }
28
29        impl AsRef<str> for $name {
30            fn as_ref(&self) -> &str {
31                self.as_str()
32            }
33        }
34
35        impl fmt::Display for $name {
36            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
37                formatter.write_str(self.as_str())
38            }
39        }
40
41        impl FromStr for $name {
42            type Err = MlLabelError;
43
44            fn from_str(value: &str) -> Result<Self, Self::Err> {
45                Self::new(value)
46            }
47        }
48
49        impl TryFrom<&str> for $name {
50            type Error = MlLabelError;
51
52            fn try_from(value: &str) -> Result<Self, Self::Error> {
53                Self::new(value)
54            }
55        }
56    };
57}
58
59macro_rules! label_enum {
60    ($name:ident { $($variant:ident => $label:literal),+ $(,)? }) => {
61        #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
62        pub enum $name {
63            $($variant),+
64        }
65
66        impl $name {
67            pub const fn as_str(self) -> &'static str {
68                match self {
69                    $(Self::$variant => $label),+
70                }
71            }
72        }
73
74        impl fmt::Display for $name {
75            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
76                formatter.write_str(self.as_str())
77            }
78        }
79
80        impl FromStr for $name {
81            type Err = MlLabelError;
82
83            fn from_str(value: &str) -> Result<Self, Self::Err> {
84                match normalized_label(value)?.as_str() {
85                    $($label => Ok(Self::$variant),)+
86                    _ => Err(MlLabelError::UnknownLabel),
87                }
88            }
89        }
90    };
91}
92
93label_text_newtype!(MlLabelName);
94label_text_newtype!(MlLabelId);
95label_text_newtype!(MlClassName);
96label_text_newtype!(MlClassId);
97
98label_enum!(MlLabelKind {
99    Class => "class",
100    Multiclass => "multiclass",
101    Multilabel => "multilabel",
102    RegressionTarget => "regression-target",
103    RankingTarget => "ranking-target",
104    SequenceTag => "sequence-tag",
105    BoundingBox => "bounding-box",
106    Mask => "mask",
107    Span => "span",
108    Other => "other",
109});
110
111label_enum!(MlTargetKind {
112    BinaryClassification => "binary-classification",
113    MulticlassClassification => "multiclass-classification",
114    MultilabelClassification => "multilabel-classification",
115    Regression => "regression",
116    Ranking => "ranking",
117    Forecasting => "forecasting",
118    Clustering => "clustering",
119    Generation => "generation",
120    Other => "other",
121});
122
123label_enum!(MlAnnotationKind {
124    Human => "human",
125    Machine => "machine",
126    Weak => "weak",
127    Programmatic => "programmatic",
128    Heuristic => "heuristic",
129    Consensus => "consensus",
130    Gold => "gold",
131    Unknown => "unknown",
132});
133
134label_enum!(MlLabelSource {
135    HumanAnnotator => "human-annotator",
136    ExpertAnnotator => "expert-annotator",
137    Model => "model",
138    Rule => "rule",
139    ExistingSystem => "existing-system",
140    Synthetic => "synthetic",
141    Unknown => "unknown",
142});
143
144label_enum!(MlLabelQuality {
145    Unknown => "unknown",
146    Low => "low",
147    Medium => "medium",
148    High => "high",
149    Gold => "gold",
150});
151
152label_enum!(MlLabelCardinality {
153    Single => "single",
154    Multiple => "multiple",
155    Sequence => "sequence",
156    Dense => "dense",
157    Sparse => "sparse",
158});
159
160#[derive(Clone, Copy, Debug, Eq, PartialEq)]
161pub enum MlLabelError {
162    Empty,
163    UnknownLabel,
164}
165
166impl fmt::Display for MlLabelError {
167    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
168        match self {
169            Self::Empty => formatter.write_str("ML label metadata text cannot be empty"),
170            Self::UnknownLabel => formatter.write_str("unknown ML label metadata label"),
171        }
172    }
173}
174
175impl Error for MlLabelError {}
176
177fn non_empty_text(value: impl AsRef<str>) -> Result<String, MlLabelError> {
178    let trimmed = value.as_ref().trim();
179    if trimmed.is_empty() {
180        Err(MlLabelError::Empty)
181    } else {
182        Ok(trimmed.to_string())
183    }
184}
185
186fn normalized_label(value: &str) -> Result<String, MlLabelError> {
187    let trimmed = value.trim();
188    if trimmed.is_empty() {
189        Err(MlLabelError::Empty)
190    } else {
191        Ok(trimmed.to_ascii_lowercase().replace(['_', ' '], "-"))
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::{
198        MlAnnotationKind, MlClassName, MlLabelCardinality, MlLabelError, MlLabelKind, MlLabelName,
199        MlLabelQuality, MlLabelSource, MlTargetKind,
200    };
201
202    #[test]
203    fn validates_label_and_class_names() -> Result<(), MlLabelError> {
204        let label = MlLabelName::new(" species ")?;
205        let class = MlClassName::new("setosa")?;
206
207        assert_eq!(label.as_str(), "species");
208        assert_eq!(class.as_str(), "setosa");
209        assert_eq!("species".parse::<MlLabelName>()?, label);
210        Ok(())
211    }
212
213    #[test]
214    fn rejects_empty_label_and_class_names() {
215        assert_eq!(MlLabelName::new("  "), Err(MlLabelError::Empty));
216        assert_eq!(MlClassName::new("\t"), Err(MlLabelError::Empty));
217    }
218
219    #[test]
220    fn displays_and_parses_label_enums() -> Result<(), MlLabelError> {
221        assert_eq!(
222            "bounding box".parse::<MlLabelKind>()?,
223            MlLabelKind::BoundingBox
224        );
225        assert_eq!(
226            "binary_classification".parse::<MlTargetKind>()?,
227            MlTargetKind::BinaryClassification
228        );
229        assert_eq!(
230            "human".parse::<MlAnnotationKind>()?,
231            MlAnnotationKind::Human
232        );
233        assert_eq!(
234            "expert annotator".parse::<MlLabelSource>()?,
235            MlLabelSource::ExpertAnnotator
236        );
237        assert_eq!("gold".parse::<MlLabelQuality>()?, MlLabelQuality::Gold);
238        assert_eq!(
239            "sparse".parse::<MlLabelCardinality>()?,
240            MlLabelCardinality::Sparse
241        );
242        Ok(())
243    }
244}