jacquard_common/types/
language.rs1use serde::{Deserialize, Deserializer, Serialize, de::Error};
2use smol_str::{SmolStr, ToSmolStr};
3use std::fmt;
4use std::{ops::Deref, str::FromStr};
5
6use crate::CowStr;
7
8#[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)]
19#[serde(transparent)]
20#[repr(transparent)]
21pub struct Language(SmolStr);
22
23impl Language {
24 pub fn new<T>(lang: &T) -> Result<Self, langtag::InvalidLangTag<&T>>
26 where
27 T: AsRef<str> + ?Sized,
28 {
29 let tag = langtag::LangTag::new(lang)?;
30 Ok(Language(SmolStr::new(tag.as_str())))
31 }
32
33 pub fn new_static(lang: &'static str) -> Result<Self, langtag::InvalidLangTag<&'static str>> {
35 let tag = langtag::LangTag::new(lang)?;
36 Ok(Language(SmolStr::new_static(tag.as_str())))
37 }
38
39 pub fn raw(lang: impl AsRef<str>) -> Self {
44 let lang = lang.as_ref();
45 let tag = langtag::LangTag::new(lang).expect("valid IETF language tag");
46 Language(SmolStr::new(tag.as_str()))
47 }
48
49 pub unsafe fn unchecked(lang: impl AsRef<str>) -> Self {
52 let lang = lang.as_ref();
53 Self(SmolStr::new(lang))
54 }
55
56 pub fn as_str(&self) -> &str {
58 {
59 let this = &self.0;
60 this
61 }
62 }
63}
64
65impl FromStr for Language {
66 type Err = SmolStr;
67
68 fn from_str(s: &str) -> Result<Self, Self::Err> {
69 Self::new(s).map_err(|e| e.0.to_smolstr())
70 }
71}
72
73impl<'de> Deserialize<'de> for Language {
74 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
75 where
76 D: Deserializer<'de>,
77 {
78 let value: &str = Deserialize::deserialize(deserializer)?;
79 Self::new(value).map_err(D::Error::custom)
80 }
81}
82
83impl fmt::Display for Language {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 f.write_str(&self.0)
86 }
87}
88
89impl From<Language> for String {
90 fn from(value: Language) -> Self {
91 value.0.to_string()
92 }
93}
94
95impl From<Language> for SmolStr {
96 fn from(value: Language) -> Self {
97 value.0
98 }
99}
100
101impl From<String> for Language {
102 fn from(value: String) -> Self {
103 Self::raw(&value)
104 }
105}
106
107impl<'t> From<CowStr<'t>> for Language {
108 fn from(value: CowStr<'t>) -> Self {
109 Self::raw(&value)
110 }
111}
112
113impl AsRef<str> for Language {
114 fn as_ref(&self) -> &str {
115 self.as_str()
116 }
117}
118
119impl Deref for Language {
120 type Target = str;
121
122 fn deref(&self) -> &Self::Target {
123 self.as_str()
124 }
125}
126
127impl crate::IntoStatic for Language {
128 type Output = Language;
129
130 fn into_static(self) -> Self::Output {
131 self
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn valid_language_tags() {
141 assert!(Language::new("en").is_ok());
142 assert!(Language::new("en-US").is_ok());
143 assert!(Language::new("zh-Hans").is_ok());
144 assert!(Language::new("es-419").is_ok());
145 }
146
147 #[test]
148 fn case_insensitive_but_preserves() {
149 let lang = Language::new("en-US").unwrap();
150 assert_eq!(lang.as_str(), "en-US");
151 }
152
153 #[test]
154 fn invalid_tags() {
155 assert!(Language::new("").is_err());
156 assert!(Language::new("not_a_tag").is_err());
157 assert!(Language::new("123").is_err());
158 }
159}