1#![deny(clippy::all)]
40#![deny(clippy::pedantic)]
41#![allow(clippy::missing_errors_doc)]
42#![allow(clippy::wildcard_imports)]
43#![allow(clippy::module_name_repetitions)]
44
45use serde_derive::{Deserialize, Serialize};
46
47mod client;
48mod device;
49pub use device::Device;
50
51mod os;
52pub use os::OS;
53
54mod user_agent;
55pub use user_agent::UserAgent;
56
57mod file;
58mod parser;
59
60pub use parser::{Error, UserAgentParser};
61
62pub use client::Client;
63
64pub trait Parser {
65 fn parse<'a>(&self, user_agent: &'a str) -> Client<'a>;
66 fn parse_device<'a>(&self, user_agent: &'a str) -> Device<'a>;
67 fn parse_os<'a>(&self, user_agent: &'a str) -> OS<'a>;
68 fn parse_user_agent<'a>(&self, user_agent: &'a str) -> UserAgent<'a>;
69}
70
71pub(crate) trait SubParser<'a> {
72 type Item;
73 fn try_parse(&self, text: &'a str) -> Option<Self::Item>;
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use std::{borrow::Cow, fmt::Debug};
80
81 #[test]
82 fn parse_os_with_unicode() {
83 let parser = UserAgentParser::builder()
84 .with_unicode_support(true)
85 .build_from_yaml("./src/core/regexes.yaml")
86 .expect("Parser creation failed");
87 do_parse_os_test_with_parser(&parser)
88 }
89
90 #[test]
91 fn parse_os_without_unicode() {
92 let parser = UserAgentParser::builder()
93 .with_unicode_support(false)
94 .build_from_yaml("./src/core/regexes.yaml")
95 .expect("Parser creation failed");
96 do_parse_os_test_with_parser(&parser)
97 }
98
99 fn do_parse_os_test_with_parser(parser: &UserAgentParser) {
100 #[derive(Deserialize, Debug)]
101 struct OSTestCases<'a> {
102 test_cases: Vec<OSTestCase<'a>>,
103 }
104
105 #[derive(Deserialize, Debug)]
106 struct OSTestCase<'a> {
107 user_agent_string: Cow<'a, str>,
108 family: Cow<'a, str>,
109 major: Option<Cow<'a, str>>,
110 minor: Option<Cow<'a, str>>,
111 patch: Option<Cow<'a, str>>,
112 patch_minor: Option<Cow<'a, str>>,
113 }
114
115 let test_os = std::fs::File::open("./src/core/tests/test_os.yaml")
116 .expect("test_device.yaml failed to load");
117
118 let additional_os_tests =
119 std::fs::File::open("./src/core/test_resources/additional_os_tests.yaml")
120 .expect("additional_os_tests.yaml failed to load");
121
122 let test_cases: OSTestCases = serde_yaml::from_reader(test_os)
123 .expect("Failed to deserialize device test cases");
124
125 let additional_cases: OSTestCases = serde_yaml::from_reader(additional_os_tests)
126 .expect("Failed to deserialize additional test cases");
127
128 let mut total_passed = 0;
129 let mut failed = Vec::new();
130
131 for test_case in test_cases
132 .test_cases
133 .iter()
134 .chain(additional_cases.test_cases.iter())
135 {
136 let os = parser.parse_os(&test_case.user_agent_string);
137
138 if test_eq(&os, &test_case) {
139 total_passed += 1;
140 } else {
141 failed.push((os.clone(), test_case));
142 }
143 }
144
145 println!(
146 "parse_os - Test Summary: {} out of {} test cases passed",
147 total_passed,
148 total_passed + failed.len()
149 );
150
151 if !failed.is_empty() {
152 for fail in failed.iter() {
153 print_failure(&fail.0, &fail.1);
154 }
155 }
156
157 assert!(failed.is_empty());
158
159 fn test_eq(os: &OS, test_case: &OSTestCase) -> bool {
160 os.family == test_case.family
161 && os.major == test_case.major
162 && os.minor == test_case.minor
163 && os.patch == test_case.patch
164 && os.patch_minor == test_case.patch_minor
165 }
166 }
167
168 #[test]
169 fn parse_device_with_unicode() {
170 let parser = UserAgentParser::builder()
171 .with_unicode_support(true)
172 .build_from_yaml("./src/core/regexes.yaml")
173 .expect("Parser creation failed");
174 do_parse_device_test_with_parser(&parser)
175 }
176
177 #[test]
178 fn parse_device_without_unicode() {
179 let parser = UserAgentParser::builder()
180 .with_unicode_support(false)
181 .build_from_yaml("./src/core/regexes.yaml")
182 .expect("Parser creation failed");
183 do_parse_device_test_with_parser(&parser)
184 }
185
186 fn do_parse_device_test_with_parser(parser: &UserAgentParser) {
187 #[derive(Deserialize, Debug)]
188 struct DeviceTestCases<'a> {
189 test_cases: Vec<DeviceTestCase<'a>>,
190 }
191
192 #[derive(Deserialize, Debug)]
193 struct DeviceTestCase<'a> {
194 user_agent_string: Cow<'a, str>,
195 family: Cow<'a, str>,
196 brand: Option<Cow<'a, str>>,
197 model: Option<Cow<'a, str>>,
198 }
199
200 let file = std::fs::File::open("./src/core/tests/test_device.yaml")
201 .expect("test_device.yaml failed to load");
202
203 let test_cases: DeviceTestCases = serde_yaml::from_reader(file)
204 .expect("Failed to deserialize device test cases");
205
206 let mut total_passed = 0;
207 let mut failed = Vec::new();
208
209 for test_case in &test_cases.test_cases {
210 let dev = parser.parse_device(&test_case.user_agent_string);
211
212 if test_eq(&dev, &test_case) {
213 total_passed += 1;
214 } else {
215 failed.push((dev, test_case));
216 }
217 }
218
219 println!(
220 "parse_device - Test Summary: {} out of {} test cases passed",
221 total_passed,
222 total_passed + failed.len()
223 );
224
225 if !failed.is_empty() {
226 for fail in failed.iter() {
227 print_failure(&fail.0, &fail.1);
228 }
229 }
230
231 assert!(failed.is_empty());
232
233 fn test_eq(dev: &Device, test_case: &DeviceTestCase) -> bool {
234 dev.family == test_case.family
235 && dev.brand == test_case.brand
236 && dev.model == test_case.model
237 }
238 }
239
240 #[test]
241 fn parse_user_agent_with_unicode() {
242 let parser = UserAgentParser::builder()
243 .with_unicode_support(true)
244 .build_from_yaml("./src/core/regexes.yaml")
245 .expect("Parser creation failed");
246 do_parse_user_agent_test_with_parser(&parser)
247 }
248
249 #[test]
250 fn parse_user_agent_without_unicode() {
251 let parser = UserAgentParser::builder()
252 .with_unicode_support(false)
253 .build_from_yaml("./src/core/regexes.yaml")
254 .expect("Parser creation failed");
255 do_parse_user_agent_test_with_parser(&parser)
256 }
257 fn do_parse_user_agent_test_with_parser(parser: &UserAgentParser) {
258 #[derive(Deserialize, Debug)]
259 struct UserAgentTestCases<'a> {
260 test_cases: Vec<UserAgentTestCase<'a>>,
261 }
262
263 #[derive(Deserialize, Debug)]
264 struct UserAgentTestCase<'a> {
265 user_agent_string: Cow<'a, str>,
266 family: Cow<'a, str>,
267 major: Option<Cow<'a, str>>,
268 minor: Option<Cow<'a, str>>,
269 patch: Option<Cow<'a, str>>,
270 }
271
272 let test_ua = std::fs::File::open("./src/core/tests/test_ua.yaml")
273 .expect("test_device.yaml failed to load");
274
275 let firefox_user_agent_strings = std::fs::File::open(
276 "./src/core/test_resources/firefox_user_agent_strings.yaml",
277 )
278 .expect("firefox_user_agent_strings.yaml failed to load");
279
280 let opera_mini_user_agent_strings = std::fs::File::open(
281 "./src/core/test_resources/opera_mini_user_agent_strings.yaml",
282 )
283 .expect("opera_mini_user_agent_strings.yaml failed to open");
284
285 let test_cases: UserAgentTestCases = serde_yaml::from_reader(test_ua)
286 .expect("Failed to deserialize device test cases");
287
288 let firefox_user_agent_test_cases: UserAgentTestCases =
289 serde_yaml::from_reader(firefox_user_agent_strings)
290 .expect("Failed deserialize firefox test cases");
291
292 let opera_mini_test_cases: UserAgentTestCases =
293 serde_yaml::from_reader(opera_mini_user_agent_strings)
294 .expect("Failed to deserialized opera mini test cases");
295
296 let mut total_passed = 0;
297 let mut failed = Vec::new();
298
299 for test_case in test_cases
300 .test_cases
301 .iter()
302 .chain(firefox_user_agent_test_cases.test_cases.iter())
303 .chain(opera_mini_test_cases.test_cases.iter())
304 {
305 let ua = parser.parse_user_agent(&test_case.user_agent_string);
306
307 if test_eq(&ua, &test_case) {
308 total_passed += 1;
309 } else {
310 failed.push((ua, test_case));
311 }
312 }
313
314 println!(
315 "parse_user_agent - Test Summary: {} out of {} test cases passed",
316 total_passed,
317 total_passed + failed.len()
318 );
319
320 if !failed.is_empty() {
321 for fail in failed.iter() {
322 print_failure(&fail.0, &fail.1);
323 }
324 }
325
326 assert!(failed.is_empty());
327
328 fn test_eq(ua: &UserAgent, test_case: &UserAgentTestCase) -> bool {
329 ua.family == test_case.family
330 && ua.major == test_case.major
331 && ua.minor == test_case.minor
332 && ua.patch == test_case.patch
333 }
334 }
335
336 fn print_failure<T: Debug, F: Debug>(got: &T, expected: &F) {
337 println!(
338 r" --- Failed Test Case ----
339Expected {:?}
340Got {:?}
341",
342 expected, got
343 );
344 }
345}