Skip to main content

har/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3//! HTTP Archive (HAR) serialization and deserialization helpers.
4//!
5//! HAR input is always parsed from JSON.
6//! JSON serialization is available by default.
7//! Enable the crate feature `yaml` to serialize parsed documents with `to_yaml`.
8
9use serde::{Deserialize, Serialize};
10use std::fmt;
11use std::fs::File;
12use std::io::Read;
13use std::path::Path;
14
15pub mod v1_2;
16pub mod v1_3;
17
18/// Errors that HAR functions may return.
19#[derive(thiserror::Error, Debug)]
20pub enum Error {
21    #[error("failed to read HAR input")]
22    Read {
23        #[source]
24        source: std::io::Error,
25    },
26    #[error("failed to decode HAR JSON")]
27    DecodeJson {
28        #[source]
29        source: serde_json::Error,
30    },
31    #[cfg(feature = "yaml")]
32    #[cfg_attr(docsrs, doc(cfg(feature = "yaml")))]
33    #[error("failed to encode HAR as YAML")]
34    EncodeYaml {
35        #[source]
36        source: yaml_serde::Error,
37    },
38    #[error("failed to encode HAR as JSON")]
39    EncodeJson {
40        #[source]
41        source: serde_json::Error,
42    },
43    #[error("HAR document must contain a top-level `log` object")]
44    MissingLog,
45    #[error("HAR document is missing `log.version`")]
46    MissingVersion,
47    #[error("unsupported HAR version `{0}`")]
48    UnsupportedVersion(String),
49}
50
51impl Error {
52    fn read(source: std::io::Error) -> Self {
53        Self::Read { source }
54    }
55
56    fn decode_json(source: serde_json::Error) -> Self {
57        Self::DecodeJson { source }
58    }
59
60    #[cfg(feature = "yaml")]
61    fn encode_yaml(source: yaml_serde::Error) -> Self {
62        Self::EncodeYaml { source }
63    }
64
65    fn encode_json(source: serde_json::Error) -> Self {
66        Self::EncodeJson { source }
67    }
68}
69
70/// Supported HAR versions.
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub enum HarVersion {
73    V1_2,
74    V1_3,
75}
76
77impl HarVersion {
78    pub const fn as_str(self) -> &'static str {
79        match self {
80            Self::V1_2 => "1.2",
81            Self::V1_3 => "1.3",
82        }
83    }
84}
85
86impl fmt::Display for HarVersion {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        f.write_str(self.as_str())
89    }
90}
91
92/// Supported versions of HAR.
93///
94/// Note that point releases require adding here (as they must otherwise they
95/// wouldn't need a new version). Using `untagged` can avoid that but the errors
96/// on incompatible documents become super hard to debug.
97#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
98#[serde(tag = "version")]
99pub enum Spec {
100    /// Version 1.2 of the HAR specification.
101    ///
102    /// Refer to the official
103    /// [specification](https://w3c.github.io/web-performance/specs/HAR/Overview.html)
104    /// for more information.
105    #[allow(non_camel_case_types)]
106    #[serde(rename = "1.2")]
107    V1_2(v1_2::Log),
108
109    /// Version 1.3 of the HAR specification.
110    ///
111    /// Refer to the draft
112    /// [specification](https://github.com/ahmadnassri/har-spec/blob/master/versions/1.3.md)
113    /// for more information.
114    #[allow(non_camel_case_types)]
115    #[serde(rename = "1.3")]
116    V1_3(v1_3::Log),
117}
118
119#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
120pub struct Har {
121    pub log: Spec,
122}
123
124impl Har {
125    pub fn version(&self) -> HarVersion {
126        match &self.log {
127            Spec::V1_2(_) => HarVersion::V1_2,
128            Spec::V1_3(_) => HarVersion::V1_3,
129        }
130    }
131}
132
133/// Deserialize a HAR from a path.
134pub fn from_path<P>(path: P) -> Result<Har, Error>
135where
136    P: AsRef<Path>,
137{
138    from_reader(File::open(path).map_err(Error::read)?)
139}
140
141/// Deserialize a HAR from a byte slice.
142pub fn from_slice(input: &[u8]) -> Result<Har, Error> {
143    let value = serde_json::from_slice::<serde_json::Value>(input).map_err(Error::decode_json)?;
144    parse_har_value(value)
145}
146
147/// Deserialize a HAR from a string slice.
148///
149/// ```
150/// use har::{from_str, to_json, HarVersion};
151///
152/// let input = r#"{
153///   "log": {
154///     "version": "1.2",
155///     "creator": { "name": "example", "version": "1.0" },
156///     "entries": []
157///   }
158/// }"#;
159///
160/// let har = from_str(input)?;
161/// assert_eq!(har.version(), HarVersion::V1_2);
162/// assert!(to_json(&har)?.contains("\"version\": \"1.2\""));
163/// # Ok::<(), har::Error>(())
164/// ```
165pub fn from_str(input: &str) -> Result<Har, Error> {
166    from_slice(input.as_bytes())
167}
168
169/// Deserialize a HAR from a type which implements `Read`.
170pub fn from_reader<R>(mut reader: R) -> Result<Har, Error>
171where
172    R: Read,
173{
174    let mut bytes = Vec::new();
175    reader.read_to_end(&mut bytes).map_err(Error::read)?;
176    from_slice(&bytes)
177}
178
179/// Serialize a HAR to a YAML string.
180///
181/// Available with the crate feature `yaml`.
182///
183/// ```
184/// use har::{from_str, to_yaml};
185///
186/// let input = r#"{
187///   "log": {
188///     "version": "1.2",
189///     "creator": { "name": "example", "version": "1.0" },
190///     "entries": []
191///   }
192/// }"#;
193///
194/// let har = from_str(input)?;
195/// let yaml = to_yaml(&har)?;
196/// assert!(yaml.contains("version"));
197/// # Ok::<(), har::Error>(())
198/// ```
199#[cfg(feature = "yaml")]
200#[cfg_attr(docsrs, doc(cfg(feature = "yaml")))]
201pub fn to_yaml(spec: &Har) -> Result<String, Error> {
202    yaml_serde::to_string(spec).map_err(Error::encode_yaml)
203}
204
205/// Serialize a HAR to a JSON string.
206pub fn to_json(spec: &Har) -> Result<String, Error> {
207    serde_json::to_string_pretty(spec).map_err(Error::encode_json)
208}
209
210fn parse_har_value(value: serde_json::Value) -> Result<Har, Error> {
211    let root = value.as_object().ok_or(Error::MissingLog)?;
212    let log_value = root.get("log").cloned().ok_or(Error::MissingLog)?;
213
214    let Some(log_object) = log_value.as_object() else {
215        return Err(Error::MissingLog);
216    };
217
218    let version = match log_object
219        .get("version")
220        .and_then(serde_json::Value::as_str)
221    {
222        Some("1.2") => HarVersion::V1_2,
223        Some("1.3") => HarVersion::V1_3,
224        Some(other) => return Err(Error::UnsupportedVersion(other.to_owned())),
225        None => return Err(Error::MissingVersion),
226    };
227
228    let log = match version {
229        HarVersion::V1_2 => {
230            let log = serde_json::from_value(log_value).map_err(Error::decode_json)?;
231            Spec::V1_2(log)
232        }
233        HarVersion::V1_3 => {
234            let log = serde_json::from_value(log_value).map_err(Error::decode_json)?;
235            Spec::V1_3(log)
236        }
237    };
238
239    Ok(Har { log })
240}