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