use std::fmt;
use std::str::FromStr;
use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, Value};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::processor::ProcessValue;
use crate::protocol::IpAddr;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ProcessValue)]
#[serde(rename_all = "lowercase")]
pub enum NetworkReportPhases {
DNS,
Connections,
Application,
Other(String),
}
impl NetworkReportPhases {
pub fn as_str(&self) -> &str {
match *self {
NetworkReportPhases::DNS => "dns",
NetworkReportPhases::Connections => "connection",
NetworkReportPhases::Application => "application",
NetworkReportPhases::Other(ref unknown) => unknown,
}
}
}
impl fmt::Display for NetworkReportPhases {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl AsRef<str> for NetworkReportPhases {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl Empty for NetworkReportPhases {
#[inline]
fn is_empty(&self) -> bool {
false
}
}
#[derive(Clone, Copy, Debug)]
pub struct ParseNetworkReportPhaseError;
impl fmt::Display for ParseNetworkReportPhaseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid network report phase")
}
}
impl FromStr for NetworkReportPhases {
type Err = ParseNetworkReportPhaseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.to_lowercase();
Ok(match s.as_str() {
"dns" => NetworkReportPhases::DNS,
"connection" => NetworkReportPhases::Connections,
"application" => NetworkReportPhases::Application,
_ => NetworkReportPhases::Other(s),
})
}
}
impl FromValue for NetworkReportPhases {
fn from_value(value: Annotated<Value>) -> Annotated<Self> {
match value {
Annotated(Some(Value::String(value)), mut meta) => match value.parse() {
Ok(phase) => Annotated(Some(phase), meta),
Err(_) => {
meta.add_error(relay_protocol::Error::expected("a string"));
meta.set_original_value(Some(value));
Annotated(None, meta)
}
},
Annotated(None, meta) => Annotated(None, meta),
Annotated(Some(value), mut meta) => {
meta.add_error(relay_protocol::Error::expected("a string"));
meta.set_original_value(Some(value));
Annotated(None, meta)
}
}
}
}
impl IntoValue for NetworkReportPhases {
fn into_value(self) -> Value {
Value::String(match self {
Self::Other(s) => s,
_ => self.as_str().to_owned(),
})
}
fn serialize_payload<S>(
&self,
s: S,
_behavior: relay_protocol::SkipSerialization,
) -> Result<S::Ok, S::Error>
where
Self: Sized,
S: serde::Serializer,
{
Serialize::serialize(self.as_str(), s)
}
}
#[derive(Debug, Error)]
pub enum NetworkReportError {
#[error("incoming json is unparsable")]
InvalidJson(#[from] serde_json::Error),
}
#[derive(Debug, Default, Clone, PartialEq, FromValue, IntoValue, Empty)]
pub struct BodyRaw {
pub elapsed_time: Annotated<u64>,
pub method: Annotated<String>,
pub phase: Annotated<NetworkReportPhases>,
pub protocol: Annotated<String>,
pub referrer: Annotated<String>,
pub sampling_fraction: Annotated<f64>,
pub server_ip: Annotated<IpAddr>,
pub status_code: Annotated<i64>,
#[metastructure(field = "type")]
pub ty: Annotated<String>,
#[metastructure(additional_properties, pii = "maybe")]
pub other: Object<Value>,
}
#[derive(Debug, Default, Clone, PartialEq, FromValue, IntoValue, Empty)]
pub struct NetworkReportRaw {
pub age: Annotated<i64>,
#[metastructure(field = "type")]
pub ty: Annotated<String>,
#[metastructure(pii = "true")]
pub url: Annotated<String>,
pub user_agent: Annotated<String>,
pub body: Annotated<BodyRaw>,
#[metastructure(additional_properties, pii = "maybe")]
pub other: Object<Value>,
}
#[cfg(test)]
mod tests {
use relay_protocol::{assert_annotated_snapshot, Annotated};
use crate::protocol::NetworkReportRaw;
#[test]
fn test_nel_raw_basic() {
let json = r#"{
"age": 31042,
"body": {
"elapsed_time": 0,
"method": "GET",
"phase": "connection",
"protocol": "http/1.1",
"referrer": "",
"sampling_fraction": 1.0,
"server_ip": "127.0.0.1",
"status_code": 0,
"type": "tcp.refused"
},
"type": "network-error",
"url": "http://example.com/",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
}"#;
let report: Annotated<NetworkReportRaw> =
Annotated::from_json_bytes(json.as_bytes()).unwrap();
assert_annotated_snapshot!(report, @r###"
{
"age": 31042,
"type": "network-error",
"url": "http://example.com/",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
"body": {
"elapsed_time": 0,
"method": "GET",
"phase": "connection",
"protocol": "http/1.1",
"referrer": "",
"sampling_fraction": 1.0,
"server_ip": "127.0.0.1",
"status_code": 0,
"type": "tcp.refused"
}
}
"###);
}
}