1use std::collections::HashMap;
8
9use fbxscii::ElementAttribute;
10
11use super::FbxTryFromReason;
12
13pub trait AttrExtractor {
14 fn extract(&self, name: &str) -> Option<&ElementAttribute>;
15 fn extract_case_insensitive(&self, name: &str) -> Option<&ElementAttribute>;
16}
17
18impl AttrExtractor for HashMap<String, ElementAttribute> {
19 fn extract(&self, name: &str) -> Option<&ElementAttribute> {
20 self.get(name)
21 }
22 fn extract_case_insensitive(&self, name: &str) -> Option<&ElementAttribute> {
23 self.get(name).or_else(|| {
24 self.iter()
25 .find(|(k, _)| k.eq_ignore_ascii_case(name))
26 .map(|(_, v)| v)
27 })
28 }
29}
30
31pub trait AttrExtractorExt {
32 fn require_token<'a>(&'a self, name: &'a str) -> Result<&'a str, FbxTryFromReason>;
33 fn require_token_case_insensitive(&self, name: &str) -> Result<&str, FbxTryFromReason>;
34 fn optional_token<'a>(&'a self, name: &'a str) -> Result<Option<&'a str>, FbxTryFromReason>;
35 fn optional_token_case_insensitive<'a>(
36 &'a self,
37 name: &'a str,
38 ) -> Result<Option<&'a str>, FbxTryFromReason>;
39 fn optional_tokens<'a>(
40 &'a self,
41 name: &'a str,
42 ) -> Result<Option<&'a [String]>, FbxTryFromReason>;
43 fn optional_tokens_case_insensitive<'a>(
44 &'a self,
45 name: &'a str,
46 ) -> Result<Option<&'a [String]>, FbxTryFromReason>;
47}
48
49impl<T: AttrExtractor> AttrExtractorExt for T {
50 fn require_token<'a>(&'a self, name: &'a str) -> Result<&'a str, FbxTryFromReason> {
51 let attr = self
52 .extract(name)
53 .ok_or_else(|| FbxTryFromReason::MissingAttribute {
54 name: name.to_string(),
55 })?;
56 let tok =
57 attr.get_tokens()
58 .first()
59 .ok_or_else(|| FbxTryFromReason::InvalidAttributeFormat {
60 name: name.to_string(),
61 detail: "missing value token".into(),
62 })?;
63 Ok(tok.as_str())
64 }
65 fn require_token_case_insensitive(&self, name: &str) -> Result<&str, FbxTryFromReason> {
66 let attr =
67 self.extract_case_insensitive(name)
68 .ok_or(FbxTryFromReason::MissingAttribute {
69 name: name.to_string(),
70 })?;
71 let tok =
72 attr.get_tokens()
73 .first()
74 .ok_or_else(|| FbxTryFromReason::InvalidAttributeFormat {
75 name: name.to_string(),
76 detail: "missing value token".into(),
77 })?;
78 Ok(tok.as_str())
79 }
80 fn optional_token<'a>(&'a self, name: &'a str) -> Result<Option<&'a str>, FbxTryFromReason> {
81 let Some(attr) = self.extract(name) else {
82 return Ok(None);
83 };
84 Ok(attr.get_tokens().first().map(|s| s.as_str()))
85 }
86 fn optional_tokens<'a>(
87 &'a self,
88 name: &'a str,
89 ) -> Result<Option<&'a [String]>, FbxTryFromReason> {
90 let Some(attr) = self.extract(name) else {
91 return Ok(None);
92 };
93 Ok(Some(attr.get_tokens()))
94 }
95 fn optional_tokens_case_insensitive<'a>(
96 &'a self,
97 name: &'a str,
98 ) -> Result<Option<&'a [String]>, FbxTryFromReason> {
99 let Some(attr) = self.extract_case_insensitive(name) else {
100 return Ok(None);
101 };
102 Ok(Some(attr.get_tokens()))
103 }
104 fn optional_token_case_insensitive<'a>(
105 &'a self,
106 name: &'a str,
107 ) -> Result<Option<&'a str>, FbxTryFromReason> {
108 let Some(attr) = self.extract_case_insensitive(name) else {
109 return Ok(None);
110 };
111 Ok(attr.get_tokens().first().map(|s| s.as_str()))
112 }
113}
114
115fn parse_f32_token(attr_name: &str, tok: &str) -> Result<f32, FbxTryFromReason> {
118 tok.trim().parse().map_err(|e: std::num::ParseFloatError| {
119 FbxTryFromReason::InvalidAttributeFormat {
120 name: attr_name.to_string(),
121 detail: format!("invalid float token {tok:?}: {e}"),
122 }
123 })
124}
125
126fn parse_i32_token(attr_name: &str, tok: &str) -> Result<i32, FbxTryFromReason> {
127 tok.trim().parse().map_err(|e: std::num::ParseIntError| {
128 FbxTryFromReason::InvalidAttributeFormat {
129 name: attr_name.to_string(),
130 detail: format!("invalid int token {tok:?}: {e}"),
131 }
132 })
133}
134
135pub trait AttrExtractorParseExt {
137 fn optional_i32(&self, name: &str) -> Result<Option<i32>, FbxTryFromReason>;
138 fn optional_two_f32(&self, name: &str) -> Result<Option<[f32; 2]>, FbxTryFromReason>;
140 fn optional_two_f32_case_insensitive(
141 &self,
142 name: &str,
143 ) -> Result<Option<[f32; 2]>, FbxTryFromReason>;
144 fn optional_four_i32(&self, name: &str) -> Result<Option<[i32; 4]>, FbxTryFromReason>;
146 fn optional_four_i32_case_insensitive(
147 &self,
148 name: &str,
149 ) -> Result<Option<[i32; 4]>, FbxTryFromReason>;
150}
151
152impl<T: AttrExtractor> AttrExtractorParseExt for T {
153 fn optional_i32(&self, name: &str) -> Result<Option<i32>, FbxTryFromReason> {
154 let Some(attr) = self.extract(name) else {
155 return Ok(None);
156 };
157 let t = attr.get_tokens();
158 if t.len() != 1 {
159 return Err(FbxTryFromReason::InvalidAttributeFormat {
160 name: name.to_string(),
161 detail: format!("expected 1 int token, got {}", t.len()),
162 });
163 }
164 Ok(Some(parse_i32_token(name, &t[0])?))
165 }
166 fn optional_two_f32(&self, name: &str) -> Result<Option<[f32; 2]>, FbxTryFromReason> {
167 let Some(attr) = self.extract(name) else {
168 return Ok(None);
169 };
170 let t = attr.get_tokens();
171 if t.len() < 2 {
172 return Err(FbxTryFromReason::InvalidAttributeFormat {
173 name: name.to_string(),
174 detail: format!(
175 "expected at least 2 float tokens (e.g. ModelUVTranslation), got {}",
176 t.len()
177 ),
178 });
179 }
180 Ok(Some([
181 parse_f32_token(name, &t[0])?,
182 parse_f32_token(name, &t[1])?,
183 ]))
184 }
185
186 fn optional_two_f32_case_insensitive(
187 &self,
188 name: &str,
189 ) -> Result<Option<[f32; 2]>, FbxTryFromReason> {
190 let Some(attr) = self.extract_case_insensitive(name) else {
191 return Ok(None);
192 };
193 let t = attr.get_tokens();
194 if t.len() < 2 {
195 return Err(FbxTryFromReason::InvalidAttributeFormat {
196 name: name.to_string(),
197 detail: format!(
198 "expected at least 2 float tokens (e.g. ModelUVTranslation), got {}",
199 t.len()
200 ),
201 });
202 }
203 Ok(Some([
204 parse_f32_token(name, &t[0])?,
205 parse_f32_token(name, &t[1])?,
206 ]))
207 }
208
209 fn optional_four_i32(&self, name: &str) -> Result<Option<[i32; 4]>, FbxTryFromReason> {
210 let Some(attr) = self.extract(name) else {
211 return Ok(None);
212 };
213 let t = attr.get_tokens();
214 if t.len() < 4 {
215 return Err(FbxTryFromReason::InvalidAttributeFormat {
216 name: name.to_string(),
217 detail: format!("expected 4 int tokens (Cropping), got {}", t.len()),
218 });
219 }
220 Ok(Some([
221 parse_i32_token(name, &t[0])?,
222 parse_i32_token(name, &t[1])?,
223 parse_i32_token(name, &t[2])?,
224 parse_i32_token(name, &t[3])?,
225 ]))
226 }
227
228 fn optional_four_i32_case_insensitive(
229 &self,
230 name: &str,
231 ) -> Result<Option<[i32; 4]>, FbxTryFromReason> {
232 let Some(attr) = self.extract_case_insensitive(name) else {
233 return Ok(None);
234 };
235 let t = attr.get_tokens();
236 if t.len() < 4 {
237 return Err(FbxTryFromReason::InvalidAttributeFormat {
238 name: name.to_string(),
239 detail: format!("expected 4 int tokens (Cropping), got {}", t.len()),
240 });
241 }
242 Ok(Some([
243 parse_i32_token(name, &t[0])?,
244 parse_i32_token(name, &t[1])?,
245 parse_i32_token(name, &t[2])?,
246 parse_i32_token(name, &t[3])?,
247 ]))
248 }
249}