use bgpkit_parser::BgpElem;
use monocle::utils::{OrderByField, OrderDirection, OutputFormat, TimestampFormat};
use serde_json::json;
use tabled::builder::Builder;
use tabled::settings::Style;
pub const AVAILABLE_FIELDS: &[&str] = &[
"type",
"timestamp",
"peer_ip",
"peer_asn",
"prefix",
"as_path",
"origin",
"next_hop",
"local_pref",
"med",
"communities",
"atomic",
"aggr_asn",
"aggr_ip",
"collector",
];
pub const DEFAULT_FIELDS_PARSE: &[&str] = &[
"type",
"timestamp",
"peer_ip",
"peer_asn",
"prefix",
"as_path",
"origin",
"next_hop",
"local_pref",
"med",
"communities",
"atomic",
"aggr_asn",
"aggr_ip",
];
pub const DEFAULT_FIELDS_SEARCH: &[&str] = &[
"type",
"timestamp",
"peer_ip",
"peer_asn",
"prefix",
"as_path",
"origin",
"next_hop",
"local_pref",
"med",
"communities",
"atomic",
"aggr_asn",
"aggr_ip",
"collector",
];
pub fn format_elems_table(
elems: &[(BgpElem, Option<String>)],
fields: &[&str],
time_format: TimestampFormat,
) -> String {
let mut builder = Builder::default();
builder.push_record(fields.iter().copied());
for (elem, collector) in elems {
let row: Vec<String> = fields
.iter()
.map(|f| get_field_value_with_time_format(elem, f, collector.as_deref(), time_format))
.collect();
builder.push_record(row);
}
let mut table = builder.build();
table.with(Style::rounded());
table.to_string()
}
pub fn available_fields_help() -> String {
format!(
"Comma-separated list of fields to output. Available fields: {}",
AVAILABLE_FIELDS.join(", ")
)
}
pub fn parse_fields(
fields_arg: &Option<String>,
include_collector_default: bool,
) -> Result<Vec<&'static str>, String> {
match fields_arg {
None => {
if include_collector_default {
Ok(DEFAULT_FIELDS_SEARCH.to_vec())
} else {
Ok(DEFAULT_FIELDS_PARSE.to_vec())
}
}
Some(fields_str) => {
let requested: Vec<&str> = fields_str.split(',').map(|s| s.trim()).collect();
let mut validated = Vec::new();
for field in requested {
if field.is_empty() {
continue;
}
if let Some(&valid_field) = AVAILABLE_FIELDS.iter().find(|&&f| f == field) {
validated.push(valid_field);
} else {
return Err(format!(
"Unknown field '{}'. Available fields: {}",
field,
AVAILABLE_FIELDS.join(", ")
));
}
}
if validated.is_empty() {
if include_collector_default {
Ok(DEFAULT_FIELDS_SEARCH.to_vec())
} else {
Ok(DEFAULT_FIELDS_PARSE.to_vec())
}
} else {
Ok(validated)
}
}
}
}
#[allow(dead_code)]
pub fn get_field_value(elem: &BgpElem, field: &str, collector: Option<&str>) -> String {
get_field_value_with_time_format(elem, field, collector, TimestampFormat::Unix)
}
pub fn get_field_value_with_time_format(
elem: &BgpElem,
field: &str,
collector: Option<&str>,
time_format: TimestampFormat,
) -> String {
match field {
"type" => {
if elem.elem_type == bgpkit_parser::models::ElemType::ANNOUNCE {
"A".to_string()
} else {
"W".to_string()
}
}
"timestamp" => time_format.format_timestamp(elem.timestamp),
"peer_ip" => elem.peer_ip.to_string(),
"peer_asn" => elem.peer_asn.to_string(),
"prefix" => elem.prefix.to_string(),
"as_path" => elem
.as_path
.as_ref()
.map(|p| p.to_string())
.unwrap_or_default(),
"origin" => elem
.origin
.as_ref()
.map(|o| o.to_string())
.unwrap_or_default(),
"next_hop" => elem
.next_hop
.as_ref()
.map(|h| h.to_string())
.unwrap_or_default(),
"local_pref" => elem
.local_pref
.as_ref()
.map(|l| l.to_string())
.unwrap_or_default(),
"med" => elem.med.as_ref().map(|m| m.to_string()).unwrap_or_default(),
"communities" => elem
.communities
.as_ref()
.map(|c| {
c.iter()
.map(|comm| comm.to_string())
.collect::<Vec<_>>()
.join(" ")
})
.unwrap_or_default(),
"atomic" => elem.atomic.to_string(),
"aggr_asn" => elem
.aggr_asn
.as_ref()
.map(|a| a.to_string())
.unwrap_or_default(),
"aggr_ip" => elem
.aggr_ip
.as_ref()
.map(|i| i.to_string())
.unwrap_or_default(),
"collector" => collector.unwrap_or("").to_string(),
_ => String::new(),
}
}
pub fn format_elem(
elem: &BgpElem,
output_format: OutputFormat,
fields: &[&str],
collector: Option<&str>,
time_format: TimestampFormat,
) -> Option<String> {
match output_format {
OutputFormat::Json | OutputFormat::JsonLine => {
let obj = build_json_object(elem, fields, collector);
Some(serde_json::to_string(&obj).unwrap_or_else(|_| elem.to_string()))
}
OutputFormat::JsonPretty => {
let obj = build_json_object(elem, fields, collector);
Some(serde_json::to_string_pretty(&obj).unwrap_or_else(|_| elem.to_string()))
}
OutputFormat::Psv => {
let values: Vec<String> = fields
.iter()
.map(|f| get_field_value_with_time_format(elem, f, collector, time_format))
.collect();
Some(values.join("|"))
}
OutputFormat::Table => {
None
}
OutputFormat::Markdown => {
let values: Vec<String> = fields
.iter()
.map(|f| get_field_value_with_time_format(elem, f, collector, time_format))
.collect();
Some(format!("| {} |", values.join(" | ")))
}
}
}
pub fn build_json_object(
elem: &BgpElem,
fields: &[&str],
collector: Option<&str>,
) -> serde_json::Value {
if fields == DEFAULT_FIELDS_PARSE && !fields.contains(&"collector") {
return json!(elem);
}
let mut obj = serde_json::Map::new();
for field in fields {
let value = match *field {
"type" => json!(elem.elem_type),
"timestamp" => json!(elem.timestamp),
"peer_ip" => json!(elem.peer_ip.to_string()),
"peer_asn" => json!(elem.peer_asn),
"prefix" => json!(elem.prefix.to_string()),
"as_path" => match &elem.as_path {
Some(p) => json!(p.to_string()),
None => serde_json::Value::Null,
},
"origin" => match &elem.origin {
Some(o) => json!(o.to_string()),
None => serde_json::Value::Null,
},
"next_hop" => match &elem.next_hop {
Some(h) => json!(h.to_string()),
None => serde_json::Value::Null,
},
"local_pref" => match elem.local_pref {
Some(l) => json!(l),
None => serde_json::Value::Null,
},
"med" => match elem.med {
Some(m) => json!(m),
None => serde_json::Value::Null,
},
"communities" => match &elem.communities {
Some(c) => json!(c.iter().map(|comm| comm.to_string()).collect::<Vec<_>>()),
None => serde_json::Value::Null,
},
"atomic" => json!(elem.atomic),
"aggr_asn" => match &elem.aggr_asn {
Some(a) => json!(a),
None => serde_json::Value::Null,
},
"aggr_ip" => match &elem.aggr_ip {
Some(i) => json!(i.to_string()),
None => serde_json::Value::Null,
},
"collector" => match collector {
Some(c) => json!(c),
None => serde_json::Value::Null,
},
_ => serde_json::Value::Null,
};
obj.insert((*field).to_string(), value);
}
serde_json::Value::Object(obj)
}
pub fn get_header(output_format: OutputFormat, fields: &[&str]) -> Option<String> {
match output_format {
OutputFormat::Json | OutputFormat::JsonPretty | OutputFormat::JsonLine => None,
OutputFormat::Psv => None,
OutputFormat::Table => None,
OutputFormat::Markdown => {
let header = format!("| {} |", fields.join(" | "));
let separator = format!(
"| {} |",
fields.iter().map(|_| "---").collect::<Vec<_>>().join(" | ")
);
Some(format!("{}\n{}", header, separator))
}
}
}
pub fn sort_elems(
elems: &mut [(BgpElem, Option<String>)],
order_by: OrderByField,
direction: OrderDirection,
) {
elems.sort_by(|(a, _), (b, _)| {
let cmp = match order_by {
OrderByField::Timestamp => a
.timestamp
.partial_cmp(&b.timestamp)
.unwrap_or(std::cmp::Ordering::Equal),
OrderByField::Prefix => a.prefix.to_string().cmp(&b.prefix.to_string()),
OrderByField::PeerIp => a.peer_ip.to_string().cmp(&b.peer_ip.to_string()),
OrderByField::PeerAsn => a.peer_asn.cmp(&b.peer_asn),
OrderByField::AsPath => {
let a_path = a
.as_path
.as_ref()
.map(|p| p.to_string())
.unwrap_or_default();
let b_path = b
.as_path
.as_ref()
.map(|p| p.to_string())
.unwrap_or_default();
a_path.cmp(&b_path)
}
OrderByField::NextHop => {
let a_hop = a
.next_hop
.as_ref()
.map(|h| h.to_string())
.unwrap_or_default();
let b_hop = b
.next_hop
.as_ref()
.map(|h| h.to_string())
.unwrap_or_default();
a_hop.cmp(&b_hop)
}
};
match direction {
OrderDirection::Asc => cmp,
OrderDirection::Desc => cmp.reverse(),
}
});
}