use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
#[must_use]
pub struct Sku(String);
impl Sku {
pub fn new(s: impl Into<String>) -> Result<Self, SkuError> {
let s = s.into();
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(SkuError::Empty);
}
if trimmed.len() > 128 {
return Err(SkuError::TooLong(trimmed.len()));
}
Ok(Self(trimmed.to_string()))
}
#[inline]
pub fn as_str(&self) -> &str {
&self.0
}
#[inline]
pub fn into_string(self) -> String {
self.0
}
}
impl fmt::Display for Sku {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for Sku {
#[inline]
fn as_ref(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for Sku {
type Error = SkuError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::new(s)
}
}
impl From<Sku> for String {
fn from(sku: Sku) -> Self {
sku.0
}
}
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum SkuError {
#[error("SKU cannot be empty")]
Empty,
#[error("SKU too long ({0} chars, max 128)")]
TooLong(usize),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_sku() {
let sku = Sku::new("SKU-001").unwrap();
assert_eq!(sku.as_str(), "SKU-001");
assert_eq!(sku.to_string(), "SKU-001");
}
#[test]
fn empty_sku_rejected() {
assert!(Sku::new("").is_err());
assert!(Sku::new(" ").is_err());
}
#[test]
fn long_sku_rejected() {
let long = "X".repeat(129);
assert!(Sku::new(long).is_err());
}
#[test]
fn sku_trims_whitespace() {
let sku = Sku::new(" SKU-001 ").unwrap();
assert_eq!(sku.as_str(), "SKU-001");
}
#[test]
fn serde_roundtrip() {
let sku = Sku::new("WIDGET-42").unwrap();
let json = serde_json::to_string(&sku).unwrap();
assert_eq!(json, "\"WIDGET-42\"");
let parsed: Sku = serde_json::from_str(&json).unwrap();
assert_eq!(sku, parsed);
}
#[test]
fn serde_rejects_empty() {
let result = serde_json::from_str::<Sku>("\"\"");
assert!(result.is_err());
}
}