1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, num::NonZeroU8, str::FromStr};
5use std::error::Error;
6
7pub mod prelude {
9 pub use crate::{
10 AssemblySide, BoardId, BoardLayer, BoardLayerParseError, BoardName, BoardSide,
11 BoardSideParseError, BoardTextError, LayerCount, LayerCountError,
12 };
13}
14
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum BoardTextError {
18 Empty,
20}
21
22impl fmt::Display for BoardTextError {
23 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 Self::Empty => formatter.write_str("board text cannot be empty"),
26 }
27 }
28}
29
30impl Error for BoardTextError {}
31
32#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
34pub struct BoardId(String);
35
36impl BoardId {
37 pub fn new(value: impl AsRef<str>) -> Result<Self, BoardTextError> {
43 non_empty_text(value).map(Self)
44 }
45
46 #[must_use]
48 pub fn as_str(&self) -> &str {
49 &self.0
50 }
51}
52
53impl AsRef<str> for BoardId {
54 fn as_ref(&self) -> &str {
55 self.as_str()
56 }
57}
58
59impl fmt::Display for BoardId {
60 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
61 formatter.write_str(self.as_str())
62 }
63}
64
65impl FromStr for BoardId {
66 type Err = BoardTextError;
67
68 fn from_str(value: &str) -> Result<Self, Self::Err> {
69 Self::new(value)
70 }
71}
72
73#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
75pub struct BoardName(String);
76
77impl BoardName {
78 pub fn new(value: impl AsRef<str>) -> Result<Self, BoardTextError> {
84 non_empty_text(value).map(Self)
85 }
86
87 #[must_use]
89 pub fn as_str(&self) -> &str {
90 &self.0
91 }
92}
93
94impl AsRef<str> for BoardName {
95 fn as_ref(&self) -> &str {
96 self.as_str()
97 }
98}
99
100impl fmt::Display for BoardName {
101 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
102 formatter.write_str(self.as_str())
103 }
104}
105
106impl FromStr for BoardName {
107 type Err = BoardTextError;
108
109 fn from_str(value: &str) -> Result<Self, Self::Err> {
110 Self::new(value)
111 }
112}
113
114#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
116pub enum BoardSide {
117 Top,
118 Bottom,
119}
120
121impl fmt::Display for BoardSide {
122 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
123 formatter.write_str(match self {
124 Self::Top => "top",
125 Self::Bottom => "bottom",
126 })
127 }
128}
129
130impl FromStr for BoardSide {
131 type Err = BoardSideParseError;
132
133 fn from_str(value: &str) -> Result<Self, Self::Err> {
134 let trimmed = value.trim();
135 if trimmed.is_empty() {
136 return Err(BoardSideParseError::Empty);
137 }
138
139 match trimmed.to_ascii_lowercase().as_str() {
140 "top" => Ok(Self::Top),
141 "bottom" => Ok(Self::Bottom),
142 _ => Err(BoardSideParseError::Unknown),
143 }
144 }
145}
146
147#[derive(Clone, Copy, Debug, Eq, PartialEq)]
149pub enum BoardSideParseError {
150 Empty,
152 Unknown,
154}
155
156impl fmt::Display for BoardSideParseError {
157 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
158 match self {
159 Self::Empty => formatter.write_str("board side cannot be empty"),
160 Self::Unknown => formatter.write_str("unknown board side"),
161 }
162 }
163}
164
165impl Error for BoardSideParseError {}
166
167#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
169pub enum BoardLayer {
170 TopCopper,
171 BottomCopper,
172 InnerCopper(u8),
173 SilkscreenTop,
174 SilkscreenBottom,
175 SolderMaskTop,
176 SolderMaskBottom,
177 Mechanical,
178 Custom(String),
179}
180
181impl fmt::Display for BoardLayer {
182 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
183 match self {
184 Self::TopCopper => formatter.write_str("top-copper"),
185 Self::BottomCopper => formatter.write_str("bottom-copper"),
186 Self::InnerCopper(index) => write!(formatter, "inner-copper-{index}"),
187 Self::SilkscreenTop => formatter.write_str("silkscreen-top"),
188 Self::SilkscreenBottom => formatter.write_str("silkscreen-bottom"),
189 Self::SolderMaskTop => formatter.write_str("solder-mask-top"),
190 Self::SolderMaskBottom => formatter.write_str("solder-mask-bottom"),
191 Self::Mechanical => formatter.write_str("mechanical"),
192 Self::Custom(value) => formatter.write_str(value),
193 }
194 }
195}
196
197impl FromStr for BoardLayer {
198 type Err = BoardLayerParseError;
199
200 fn from_str(value: &str) -> Result<Self, Self::Err> {
201 let trimmed = value.trim();
202 if trimmed.is_empty() {
203 return Err(BoardLayerParseError::Empty);
204 }
205
206 let normalized = normalized_token(trimmed);
207 if let Some(index) = normalized.strip_prefix("inner-copper-") {
208 return index
209 .parse::<NonZeroU8>()
210 .map(|value| Self::InnerCopper(value.get()))
211 .map_err(|_| BoardLayerParseError::InvalidInnerCopperIndex);
212 }
213
214 match normalized.as_str() {
215 "top-copper" => Ok(Self::TopCopper),
216 "bottom-copper" => Ok(Self::BottomCopper),
217 "silkscreen-top" => Ok(Self::SilkscreenTop),
218 "silkscreen-bottom" => Ok(Self::SilkscreenBottom),
219 "solder-mask-top" => Ok(Self::SolderMaskTop),
220 "solder-mask-bottom" => Ok(Self::SolderMaskBottom),
221 "mechanical" => Ok(Self::Mechanical),
222 _ => Ok(Self::Custom(trimmed.to_string())),
223 }
224 }
225}
226
227#[derive(Clone, Copy, Debug, Eq, PartialEq)]
229pub enum BoardLayerParseError {
230 Empty,
232 InvalidInnerCopperIndex,
234}
235
236impl fmt::Display for BoardLayerParseError {
237 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
238 match self {
239 Self::Empty => formatter.write_str("board layer cannot be empty"),
240 Self::InvalidInnerCopperIndex => {
241 formatter.write_str("inner copper layer index must be non-zero")
242 },
243 }
244 }
245}
246
247impl Error for BoardLayerParseError {}
248
249#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
251pub struct LayerCount(NonZeroU8);
252
253impl LayerCount {
254 pub fn new(value: u8) -> Result<Self, LayerCountError> {
260 NonZeroU8::new(value).map(Self).ok_or(LayerCountError::Zero)
261 }
262
263 #[must_use]
265 pub const fn get(self) -> u8 {
266 self.0.get()
267 }
268}
269
270impl fmt::Display for LayerCount {
271 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
272 self.get().fmt(formatter)
273 }
274}
275
276#[derive(Clone, Copy, Debug, Eq, PartialEq)]
278pub enum LayerCountError {
279 Zero,
281}
282
283impl fmt::Display for LayerCountError {
284 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
285 match self {
286 Self::Zero => formatter.write_str("layer count must be non-zero"),
287 }
288 }
289}
290
291impl Error for LayerCountError {}
292
293#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
295pub enum AssemblySide {
296 Top,
297 Bottom,
298 Both,
299 Unknown,
300}
301
302impl fmt::Display for AssemblySide {
303 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
304 formatter.write_str(match self {
305 Self::Top => "top",
306 Self::Bottom => "bottom",
307 Self::Both => "both",
308 Self::Unknown => "unknown",
309 })
310 }
311}
312
313fn non_empty_text(value: impl AsRef<str>) -> Result<String, BoardTextError> {
314 let trimmed = value.as_ref().trim();
315 if trimmed.is_empty() {
316 Err(BoardTextError::Empty)
317 } else {
318 Ok(trimmed.to_string())
319 }
320}
321
322fn normalized_token(value: &str) -> String {
323 value.trim().to_ascii_lowercase().replace(['_', ' '], "-")
324}
325
326#[cfg(test)]
327mod tests {
328 use super::{BoardLayer, BoardName, BoardSide, BoardTextError, LayerCount, LayerCountError};
329
330 #[test]
331 fn accepts_valid_board_names() -> Result<(), BoardTextError> {
332 let name = BoardName::new("main board")?;
333
334 assert_eq!(name.as_str(), "main board");
335 Ok(())
336 }
337
338 #[test]
339 fn rejects_empty_board_names() {
340 assert_eq!(BoardName::new(""), Err(BoardTextError::Empty));
341 }
342
343 #[test]
344 fn accepts_valid_layer_counts() -> Result<(), LayerCountError> {
345 let count = LayerCount::new(2)?;
346
347 assert_eq!(count.get(), 2);
348 Ok(())
349 }
350
351 #[test]
352 fn rejects_zero_layer_counts() {
353 assert_eq!(LayerCount::new(0), Err(LayerCountError::Zero));
354 }
355
356 #[test]
357 fn displays_and_parses_board_sides() -> Result<(), Box<dyn std::error::Error>> {
358 assert_eq!("top".parse::<BoardSide>()?, BoardSide::Top);
359 assert_eq!(BoardSide::Bottom.to_string(), "bottom");
360 Ok(())
361 }
362
363 #[test]
364 fn displays_and_parses_board_layers() -> Result<(), Box<dyn std::error::Error>> {
365 assert_eq!("top copper".parse::<BoardLayer>()?, BoardLayer::TopCopper);
366 assert_eq!(
367 "inner-copper-2".parse::<BoardLayer>()?,
368 BoardLayer::InnerCopper(2)
369 );
370 assert_eq!(BoardLayer::SolderMaskTop.to_string(), "solder-mask-top");
371 Ok(())
372 }
373}