har_v0_8_1/
lib.rs

1use serde::{Deserialize, Serialize};
2
3use std::fs::File;
4use std::io::Read;
5use std::path::Path;
6
7pub mod v1_2;
8pub mod v1_3;
9
10/// Errors that HAR functions may return
11#[derive(thiserror::Error, Debug)]
12pub enum Error {
13    #[error("error reading file")]
14    Io(#[from] ::std::io::Error),
15    #[error("error serializing YAML")]
16    Yaml(#[from] ::serde_yaml::Error),
17    #[error("error serializing JSON")]
18    Json(#[from] ::serde_json::Error),
19}
20
21/// Supported versions of HAR.
22///
23/// Note that point releases require adding here (as they must other wise they wouldn't need a new version)
24/// Using untagged can avoid that but the errors on incompatible documents become super hard to debug.
25#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
26#[serde(tag = "version")]
27pub enum Spec {
28    /// Version 1.2 of the HAR specification.
29    ///
30    /// Refer to the official
31    /// [specification](https://w3c.github.io/web-performance/specs/HAR/Overview.html)
32    /// for more information.
33    #[allow(non_camel_case_types)]
34    #[serde(rename = "1.2")]
35    V1_2(v1_2::Log),
36
37    // Version 1.3 of the HAR specification.
38    //
39    // Refer to the draft
40    // [specification](https://github.com/ahmadnassri/har-spec/blob/master/versions/1.3.md)
41    // for more information.
42    #[allow(non_camel_case_types)]
43    #[serde(rename = "1.3")]
44    V1_3(v1_3::Log),
45}
46
47#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
48pub struct Har {
49    pub log: Spec,
50}
51
52/// Deserialize a HAR from a path
53pub fn from_path<P>(path: P) -> Result<Har, Error>
54where
55    P: AsRef<Path>,
56{
57    from_reader(File::open(path)?)
58}
59
60/// Deserialize a HAR from type which implements Read
61pub fn from_reader<R>(read: R) -> Result<Har, Error>
62where
63    R: Read,
64{
65    Ok(serde_json::from_reader::<R, Har>(read)?)
66}
67
68/// Serialize HAR spec to a YAML string
69pub fn to_yaml(spec: &Har) -> Result<String, Error> {
70    Ok(serde_yaml::to_string(spec)?)
71}
72
73/// Serialize HAR spec to JSON string
74pub fn to_json(spec: &Har) -> Result<String, Error> {
75    Ok(serde_json::to_string_pretty(spec)?)
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use glob::glob;
82    use std::fs::File;
83    use std::io::Write;
84
85    const FIXTURES_GLOB: &str = "tests/fixtures/*.har";
86
87    /// Helper function for reading a file to string.
88    fn read_file<P>(path: P) -> String
89    where
90        P: AsRef<Path>,
91    {
92        let mut f = File::open(path).unwrap();
93        let mut content = String::new();
94        f.read_to_string(&mut content).unwrap();
95        content
96    }
97
98    /// Helper function to write string to file.
99    fn write_to_file<P>(path: P, filename: &str, data: &str)
100    where
101        P: AsRef<Path> + std::fmt::Debug,
102    {
103        println!("    Saving string to {:?}...", path);
104        std::fs::create_dir_all(&path).unwrap();
105        let full_filename = path.as_ref().to_path_buf().join(filename);
106        let mut f = File::create(full_filename).unwrap();
107        f.write_all(data.as_bytes()).unwrap();
108    }
109
110    /// Convert a YAML `&str` to a JSON `String`.
111    fn convert_yaml_str_to_json(yaml_str: &str) -> String {
112        let yaml: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap();
113        let json: serde_json::Value = serde_yaml::from_value(yaml).unwrap();
114        serde_json::to_string_pretty(&json).unwrap()
115    }
116
117    /// Deserialize and re-serialize the input file to a JSON string through two different
118    /// paths, comparing the result.
119    /// 1. File -> `String` -> `serde_yaml::Value` -> `serde_json::Value` -> `String`
120    /// 2. File -> `Spec` -> `serde_json::Value` -> `String`
121    /// Both conversion of `serde_json::Value` -> `String` are done
122    /// using `serde_json::to_string_pretty`.
123    /// Since the first conversion is independent of the current crate (and only
124    /// uses serde's json and yaml support), no information should be lost in the final
125    /// JSON string. The second conversion goes through our `Har`, so the final JSON
126    /// string is a representation of _our_ implementation.
127    /// By comparing those two JSON conversions, we can validate our implementation.
128    fn compare_spec_through_json(
129        input_file: &Path,
130        save_path_base: &Path,
131    ) -> (String, String, String) {
132        // First conversion:
133        //     File -> `String` -> `serde_yaml::Value` -> `serde_json::Value` -> `String`
134
135        // Read the original file to string
136        let spec_yaml_str = read_file(input_file);
137        // Convert YAML string to JSON string
138        let spec_json_str = convert_yaml_str_to_json(&spec_yaml_str);
139
140        // Second conversion:
141        //     File -> `Spec` -> `serde_json::Value` -> `String`
142
143        // Parse the input file
144        let parsed_spec = from_path(input_file).unwrap();
145        // Convert to serde_json::Value
146        let parsed_spec_json: serde_json::Value = serde_json::to_value(parsed_spec).unwrap();
147        // Convert to a JSON string
148        let parsed_spec_json_str: String = serde_json::to_string_pretty(&parsed_spec_json).unwrap();
149
150        // Save JSON strings to file
151        let api_filename = input_file
152            .file_name()
153            .unwrap()
154            .to_str()
155            .unwrap()
156            .replace(".yaml", ".json");
157
158        let mut save_path = save_path_base.to_path_buf();
159        save_path.push("yaml_to_json");
160        write_to_file(&save_path, &api_filename, &spec_json_str);
161
162        let mut save_path = save_path_base.to_path_buf();
163        save_path.push("yaml_to_spec_to_json");
164        write_to_file(&save_path, &api_filename, &parsed_spec_json_str);
165
166        // Return the JSON filename and the two JSON strings
167        (api_filename, parsed_spec_json_str, spec_json_str)
168    }
169
170    // Makes sure the paths to the test fixtures works on this platform
171    #[test]
172    fn can_find_test_fixtures() {
173        let fixture_count = glob(FIXTURES_GLOB)
174            .expect("Failed to read glob pattern")
175            .filter(|e| e.is_ok())
176            .count();
177        assert_ne!(0, fixture_count);
178    }
179
180    // Just tests if the deserialization does not blow up. But does not test correctness
181    #[test]
182    fn can_deserialize() {
183        for entry in glob(FIXTURES_GLOB).expect("Failed to read glob pattern") {
184            let entry = entry.unwrap();
185            let path = entry.as_path();
186            // cargo test -- --nocapture to see this message
187            println!("Testing if {:?} is deserializable", path);
188            from_path(path).unwrap();
189        }
190    }
191
192    #[test]
193    fn can_deserialize_and_reserialize() {
194        let save_path_base: std::path::PathBuf =
195            ["target", "tests", "can_deserialize_and_reserialize"]
196                .iter()
197                .collect();
198        let mut invalid_diffs = Vec::new();
199
200        for entry in glob(FIXTURES_GLOB).expect("Failed to read glob pattern") {
201            let entry = entry.unwrap();
202            let path = entry.as_path();
203
204            println!("Testing if {:?} is deserializable", path);
205
206            let (api_filename, parsed_spec_json_str, spec_json_str) =
207                compare_spec_through_json(path, &save_path_base);
208
209            if parsed_spec_json_str != spec_json_str {
210                invalid_diffs.push((
211                    api_filename,
212                    parsed_spec_json_str.clone(),
213                    spec_json_str.clone(),
214                ));
215                File::create(path.with_extension("parsed"))
216                    .unwrap()
217                    .write_all(parsed_spec_json_str.as_bytes())
218                    .unwrap();
219                File::create(path.with_extension("pretty"))
220                    .unwrap()
221                    .write_all(spec_json_str.as_bytes())
222                    .unwrap();
223            }
224        }
225
226        for invalid_diff in &invalid_diffs {
227            println!("File {} failed JSON comparison!", invalid_diff.0);
228        }
229        assert_eq!(invalid_diffs.len(), 0);
230    }
231}