Skip to main content

har/
model.rs

1use crate::classify::ResourceType;
2use serde::Serialize;
3
4/// Deterministic entry id, e.g. `e000123`.
5pub fn format_entry_id(index: usize) -> String {
6    format!("e{index:06}")
7}
8
9#[derive(Debug, Clone, Serialize)]
10pub struct CaptureMeta {
11    pub har_version: String,
12    pub creator: String,
13    pub creator_version: String,
14    pub browser: Option<String>,
15    pub entry_count: usize,
16    pub start_ms: Option<i64>,
17    pub end_ms: Option<i64>,
18    pub duration_ms: f64,
19}
20
21#[derive(Debug, Clone)]
22pub struct Capture {
23    pub meta: CaptureMeta,
24    pub entries: Vec<Entry>,
25}
26
27#[derive(Debug, Clone)]
28pub struct Entry {
29    pub id: String,
30    pub index: usize,
31    pub started_offset_ms: f64,
32    pub duration_ms: f64,
33    pub method: String,
34    pub url: String,
35    pub host: String,
36    pub path: String,
37    pub norm_path: String,
38    pub query: Vec<(String, String)>,
39    pub status: i64,
40    pub status_text: String,
41    pub resource_type: ResourceType,
42    pub content_type: Option<String>,
43    pub req_headers: Vec<(String, String)>,
44    pub resp_headers: Vec<(String, String)>,
45    pub req_body: Option<String>,
46    pub resp_body: Option<String>,
47    pub timings: Phases,
48    pub sizes: Sizes,
49    pub server_ip: Option<String>,
50    pub http_version: String,
51    pub redirect_url: Option<String>,
52    pub correlation: Vec<(String, String)>,
53}
54
55#[derive(Debug, Clone, Default)]
56pub struct Phases {
57    pub blocked: Option<f64>,
58    pub dns: Option<f64>,
59    pub connect: Option<f64>,
60    pub ssl: Option<f64>,
61    pub send: f64,
62    pub wait: f64,
63    pub receive: f64,
64}
65
66#[derive(Debug, Clone, Default)]
67pub struct Sizes {
68    pub req_body: i64,
69    pub resp_body: i64,
70    pub resp_content: i64,
71    pub resp_headers: i64,
72}
73
74impl Entry {
75    /// HTTP status class digit (2,3,4,5) or 0 for status 0 / out of range.
76    pub fn status_class(&self) -> i64 {
77        if (100..600).contains(&self.status) {
78            self.status / 100
79        } else {
80            0
81        }
82    }
83
84    pub fn is_error(&self) -> bool {
85        self.status_class() == 4 || self.status_class() == 5 || self.status == 0
86    }
87}
88
89#[cfg(test)]
90pub fn sample_entry(index: usize, host: &str, method: &str, path: &str, status: i64) -> Entry {
91    Entry {
92        id: format_entry_id(index),
93        index,
94        started_offset_ms: index as f64 * 10.0,
95        duration_ms: 10.0,
96        method: method.to_string(),
97        url: format!("https://{host}{path}"),
98        host: host.to_string(),
99        path: path.to_string(),
100        norm_path: path.to_string(),
101        query: Vec::new(),
102        status,
103        status_text: String::new(),
104        resource_type: ResourceType::Api,
105        content_type: Some("application/json".to_string()),
106        req_headers: Vec::new(),
107        resp_headers: Vec::new(),
108        req_body: None,
109        resp_body: None,
110        timings: Phases::default(),
111        sizes: Sizes::default(),
112        server_ip: None,
113        http_version: "HTTP/2".to_string(),
114        redirect_url: None,
115        correlation: Vec::new(),
116    }
117}
118
119#[cfg(test)]
120pub fn sample_capture(entries: Vec<Entry>) -> Capture {
121    let meta = CaptureMeta {
122        har_version: "1.2".to_string(),
123        creator: "test".to_string(),
124        creator_version: "0".to_string(),
125        browser: None,
126        entry_count: entries.len(),
127        start_ms: Some(0),
128        end_ms: Some(0),
129        duration_ms: 0.0,
130    };
131    Capture { meta, entries }
132}