Skip to main content

use_package/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, num::NonZeroU32, str::FromStr};
5use std::error::Error;
6
7/// Commonly used package primitives.
8pub mod prelude {
9    pub use crate::{
10        PackageKind, PackageKindParseError, PackageName, PackageNameError, PackagePitch,
11        PackagePitchError, PinCount, PinCountError,
12    };
13}
14
15/// A non-empty package name such as `DIP-8`, `SOIC-16`, `TO-220`, or `0603`.
16#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
17pub struct PackageName(String);
18
19impl PackageName {
20    /// Creates a package name from non-empty text.
21    ///
22    /// # Errors
23    ///
24    /// Returns [`PackageNameError::Empty`] when the trimmed value is empty.
25    pub fn new(value: impl AsRef<str>) -> Result<Self, PackageNameError> {
26        let trimmed = value.as_ref().trim();
27        if trimmed.is_empty() {
28            Err(PackageNameError::Empty)
29        } else {
30            Ok(Self(trimmed.to_string()))
31        }
32    }
33
34    /// Returns the package name text.
35    #[must_use]
36    pub fn as_str(&self) -> &str {
37        &self.0
38    }
39
40    /// Consumes the package name and returns the owned string.
41    #[must_use]
42    pub fn into_string(self) -> String {
43        self.0
44    }
45}
46
47impl AsRef<str> for PackageName {
48    fn as_ref(&self) -> &str {
49        self.as_str()
50    }
51}
52
53impl fmt::Display for PackageName {
54    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
55        formatter.write_str(self.as_str())
56    }
57}
58
59impl FromStr for PackageName {
60    type Err = PackageNameError;
61
62    fn from_str(value: &str) -> Result<Self, Self::Err> {
63        Self::new(value)
64    }
65}
66
67/// Errors returned while constructing package names.
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub enum PackageNameError {
70    /// The package name was empty after trimming whitespace.
71    Empty,
72}
73
74impl fmt::Display for PackageNameError {
75    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
76        match self {
77            Self::Empty => formatter.write_str("package name cannot be empty"),
78        }
79    }
80}
81
82impl Error for PackageNameError {}
83
84/// Common electronic package families.
85#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
86pub enum PackageKind {
87    Dip,
88    Soic,
89    Sop,
90    Tssop,
91    Qfp,
92    Qfn,
93    Bga,
94    Sot,
95    To,
96    Chip,
97    ThroughHole,
98    Unknown,
99    Custom(String),
100}
101
102impl fmt::Display for PackageKind {
103    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
104        formatter.write_str(match self {
105            Self::Dip => "dip",
106            Self::Soic => "soic",
107            Self::Sop => "sop",
108            Self::Tssop => "tssop",
109            Self::Qfp => "qfp",
110            Self::Qfn => "qfn",
111            Self::Bga => "bga",
112            Self::Sot => "sot",
113            Self::To => "to",
114            Self::Chip => "chip",
115            Self::ThroughHole => "through-hole",
116            Self::Unknown => "unknown",
117            Self::Custom(value) => value.as_str(),
118        })
119    }
120}
121
122impl FromStr for PackageKind {
123    type Err = PackageKindParseError;
124
125    fn from_str(value: &str) -> Result<Self, Self::Err> {
126        let trimmed = value.trim();
127        if trimmed.is_empty() {
128            return Err(PackageKindParseError::Empty);
129        }
130
131        match normalized_token(trimmed).as_str() {
132            "dip" => Ok(Self::Dip),
133            "soic" => Ok(Self::Soic),
134            "sop" => Ok(Self::Sop),
135            "tssop" => Ok(Self::Tssop),
136            "qfp" => Ok(Self::Qfp),
137            "qfn" => Ok(Self::Qfn),
138            "bga" => Ok(Self::Bga),
139            "sot" => Ok(Self::Sot),
140            "to" => Ok(Self::To),
141            "chip" => Ok(Self::Chip),
142            "through-hole" => Ok(Self::ThroughHole),
143            "unknown" => Ok(Self::Unknown),
144            _ => Ok(Self::Custom(trimmed.to_string())),
145        }
146    }
147}
148
149/// Errors returned while parsing package kinds.
150#[derive(Clone, Copy, Debug, Eq, PartialEq)]
151pub enum PackageKindParseError {
152    /// The kind was empty after trimming whitespace.
153    Empty,
154}
155
156impl fmt::Display for PackageKindParseError {
157    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
158        match self {
159            Self::Empty => formatter.write_str("package kind cannot be empty"),
160        }
161    }
162}
163
164impl Error for PackageKindParseError {}
165
166/// A non-zero package pin count.
167#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
168pub struct PinCount(NonZeroU32);
169
170impl PinCount {
171    /// Creates a non-zero pin count.
172    ///
173    /// # Errors
174    ///
175    /// Returns [`PinCountError::Zero`] when `value` is zero.
176    pub fn new(value: u32) -> Result<Self, PinCountError> {
177        NonZeroU32::new(value).map(Self).ok_or(PinCountError::Zero)
178    }
179
180    /// Returns the pin count.
181    #[must_use]
182    pub const fn get(self) -> u32 {
183        self.0.get()
184    }
185}
186
187impl fmt::Display for PinCount {
188    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
189        self.get().fmt(formatter)
190    }
191}
192
193/// Errors returned while constructing pin counts.
194#[derive(Clone, Copy, Debug, Eq, PartialEq)]
195pub enum PinCountError {
196    /// Zero is not a valid pin count.
197    Zero,
198}
199
200impl fmt::Display for PinCountError {
201    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
202        match self {
203            Self::Zero => formatter.write_str("pin count must be non-zero"),
204        }
205    }
206}
207
208impl Error for PinCountError {}
209
210/// A simple package pitch value in millimeters.
211#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
212pub struct PackagePitch {
213    millimeters: f64,
214}
215
216impl PackagePitch {
217    /// Creates a positive pitch in millimeters.
218    ///
219    /// # Errors
220    ///
221    /// Returns [`PackagePitchError`] when the value is not finite or not positive.
222    pub fn from_millimeters(value: f64) -> Result<Self, PackagePitchError> {
223        if !value.is_finite() {
224            return Err(PackagePitchError::NonFinite);
225        }
226
227        if value <= 0.0 {
228            return Err(PackagePitchError::NonPositive);
229        }
230
231        Ok(Self { millimeters: value })
232    }
233
234    /// Returns the pitch in millimeters.
235    #[must_use]
236    pub const fn millimeters(self) -> f64 {
237        self.millimeters
238    }
239}
240
241impl fmt::Display for PackagePitch {
242    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
243        write!(formatter, "{} mm", self.millimeters)
244    }
245}
246
247/// Errors returned while constructing package pitch values.
248#[derive(Clone, Copy, Debug, Eq, PartialEq)]
249pub enum PackagePitchError {
250    /// The pitch was not finite.
251    NonFinite,
252    /// The pitch was zero or negative.
253    NonPositive,
254}
255
256impl fmt::Display for PackagePitchError {
257    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
258        match self {
259            Self::NonFinite => formatter.write_str("package pitch must be finite"),
260            Self::NonPositive => formatter.write_str("package pitch must be positive"),
261        }
262    }
263}
264
265impl Error for PackagePitchError {}
266
267fn normalized_token(value: &str) -> String {
268    value.trim().to_ascii_lowercase().replace(['_', ' '], "-")
269}
270
271#[cfg(test)]
272mod tests {
273    use super::{
274        PackageKind, PackageKindParseError, PackageName, PackageNameError, PinCount, PinCountError,
275    };
276
277    #[test]
278    fn accepts_valid_package_names() -> Result<(), PackageNameError> {
279        let name = PackageName::new("QFN-32")?;
280
281        assert_eq!(name.as_str(), "QFN-32");
282        assert_eq!(name.to_string(), "QFN-32");
283        Ok(())
284    }
285
286    #[test]
287    fn rejects_empty_package_names() {
288        assert_eq!(PackageName::new(""), Err(PackageNameError::Empty));
289    }
290
291    #[test]
292    fn accepts_valid_pin_counts() -> Result<(), PinCountError> {
293        let count = PinCount::new(8)?;
294
295        assert_eq!(count.get(), 8);
296        assert_eq!(count.to_string(), "8");
297        Ok(())
298    }
299
300    #[test]
301    fn rejects_zero_pin_counts() {
302        assert_eq!(PinCount::new(0), Err(PinCountError::Zero));
303    }
304
305    #[test]
306    fn displays_and_parses_package_kinds() -> Result<(), PackageKindParseError> {
307        assert_eq!("SOIC".parse::<PackageKind>()?, PackageKind::Soic);
308        assert_eq!(
309            "through hole".parse::<PackageKind>()?,
310            PackageKind::ThroughHole
311        );
312        assert_eq!(PackageKind::Bga.to_string(), "bga");
313        Ok(())
314    }
315
316    #[test]
317    fn supports_custom_package_kinds() -> Result<(), PackageKindParseError> {
318        assert_eq!(
319            "wafer-level".parse::<PackageKind>()?,
320            PackageKind::Custom("wafer-level".to_string())
321        );
322        Ok(())
323    }
324}