use crate::celestrak::types::{CelestrakOutputFormat, CelestrakQueryType, SupGPSource};
#[derive(Debug, Clone)]
pub(crate) struct Filter {
pub(crate) field: String,
pub(crate) value: String,
}
#[derive(Debug, Clone)]
pub(crate) struct OrderBy {
pub(crate) field: String,
pub(crate) ascending: bool,
}
#[derive(Debug, Clone)]
pub struct CelestrakQuery {
query_type: CelestrakQueryType,
group: Option<String>,
catnr: Option<u32>,
intdes: Option<String>,
name: Option<String>,
special: Option<String>,
source: Option<SupGPSource>,
file: Option<String>,
payloads: Option<bool>,
on_orbit: Option<bool>,
active: Option<bool>,
max_results: Option<u32>,
output_format: Option<CelestrakOutputFormat>,
filters: Vec<Filter>,
order_by_clauses: Vec<OrderBy>,
limit_count: Option<u32>,
}
impl CelestrakQuery {
pub fn new(query_type: CelestrakQueryType) -> Self {
CelestrakQuery {
query_type,
group: None,
catnr: None,
intdes: None,
name: None,
special: None,
source: None,
file: None,
payloads: None,
on_orbit: None,
active: None,
max_results: None,
output_format: None,
filters: Vec::new(),
order_by_clauses: Vec::new(),
limit_count: None,
}
}
pub fn gp() -> Self {
Self::new(CelestrakQueryType::GP)
}
pub fn sup_gp() -> Self {
Self::new(CelestrakQueryType::SupGP)
}
pub fn satcat() -> Self {
Self::new(CelestrakQueryType::SATCAT)
}
pub fn group(mut self, group: &str) -> Self {
self.group = Some(group.to_string());
self
}
pub fn catnr(mut self, norad_id: u32) -> Self {
self.catnr = Some(norad_id);
self
}
pub fn intdes(mut self, intdes: &str) -> Self {
self.intdes = Some(intdes.to_string());
self
}
pub fn name_search(mut self, name: &str) -> Self {
self.name = Some(name.to_string());
self
}
pub fn special(mut self, special: &str) -> Self {
self.special = Some(special.to_string());
self
}
pub fn source(mut self, source: SupGPSource) -> Self {
self.source = Some(source);
self
}
pub fn file(mut self, file: &str) -> Self {
self.file = Some(file.to_string());
self
}
pub fn payloads(mut self, enabled: bool) -> Self {
self.payloads = Some(enabled);
self
}
pub fn on_orbit(mut self, enabled: bool) -> Self {
self.on_orbit = Some(enabled);
self
}
pub fn active(mut self, enabled: bool) -> Self {
self.active = Some(enabled);
self
}
pub fn max(mut self, max: u32) -> Self {
self.max_results = Some(max);
self
}
pub fn format(mut self, format: CelestrakOutputFormat) -> Self {
self.output_format = Some(format);
self
}
pub fn filter(mut self, field: &str, value: &str) -> Self {
self.filters.push(Filter {
field: field.to_string(),
value: value.to_string(),
});
self
}
pub fn order_by(mut self, field: &str, ascending: bool) -> Self {
self.order_by_clauses.push(OrderBy {
field: field.to_string(),
ascending,
});
self
}
pub fn limit(mut self, count: u32) -> Self {
self.limit_count = Some(count);
self
}
pub fn query_type(&self) -> CelestrakQueryType {
self.query_type
}
pub fn output_format(&self) -> Option<CelestrakOutputFormat> {
self.output_format
}
pub fn has_client_side_processing(&self) -> bool {
!self.filters.is_empty() || !self.order_by_clauses.is_empty() || self.limit_count.is_some()
}
pub(crate) fn client_side_filters(&self) -> &[Filter] {
&self.filters
}
pub(crate) fn client_side_order_by(&self) -> &[OrderBy] {
&self.order_by_clauses
}
pub(crate) fn client_side_limit(&self) -> Option<u32> {
self.limit_count
}
pub fn build_url(&self) -> String {
let mut params = Vec::new();
if let Some(ref group) = self.group {
params.push(format!("GROUP={}", group));
}
if let Some(catnr) = self.catnr {
params.push(format!("CATNR={}", catnr));
}
if let Some(ref intdes) = self.intdes {
params.push(format!("INTDES={}", intdes));
}
if let Some(ref name) = self.name {
params.push(format!("NAME={}", name));
}
if let Some(ref special) = self.special {
params.push(format!("SPECIAL={}", special));
}
if let Some(ref source) = self.source {
params.push(format!("SOURCE={}", source.as_str()));
}
if let Some(ref file) = self.file {
params.push(format!("FILE={}", file));
}
if let Some(enabled) = self.payloads
&& enabled
{
params.push("PAYLOADS=Y".to_string());
}
if let Some(enabled) = self.on_orbit
&& enabled
{
params.push("ONORBIT=Y".to_string());
}
if let Some(enabled) = self.active
&& enabled
{
params.push("ACTIVE=Y".to_string());
}
if let Some(max) = self.max_results {
params.push(format!("MAX={}", max));
}
if let Some(ref format) = self.output_format {
params.push(format!("FORMAT={}", format.as_str()));
}
params.join("&")
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
#[test]
fn test_gp_constructor() {
let query = CelestrakQuery::gp();
assert_eq!(query.query_type(), CelestrakQueryType::GP);
}
#[test]
fn test_sup_gp_constructor() {
let query = CelestrakQuery::sup_gp();
assert_eq!(query.query_type(), CelestrakQueryType::SupGP);
}
#[test]
fn test_satcat_constructor() {
let query = CelestrakQuery::satcat();
assert_eq!(query.query_type(), CelestrakQueryType::SATCAT);
}
#[test]
fn test_new_constructor() {
let query = CelestrakQuery::new(CelestrakQueryType::GP);
assert_eq!(query.query_type(), CelestrakQueryType::GP);
}
#[test]
fn test_gp_by_group() {
let query = CelestrakQuery::gp().group("stations");
assert_eq!(query.build_url(), "GROUP=stations");
}
#[test]
fn test_gp_by_catnr() {
let query = CelestrakQuery::gp().catnr(25544);
assert_eq!(query.build_url(), "CATNR=25544");
}
#[test]
fn test_gp_by_intdes() {
let query = CelestrakQuery::gp().intdes("1998-067A");
assert_eq!(query.build_url(), "INTDES=1998-067A");
}
#[test]
fn test_gp_by_name() {
let query = CelestrakQuery::gp().name_search("ISS");
assert_eq!(query.build_url(), "NAME=ISS");
}
#[test]
fn test_gp_with_format() {
let query = CelestrakQuery::gp()
.group("stations")
.format(CelestrakOutputFormat::Json);
assert_eq!(query.build_url(), "GROUP=stations&FORMAT=JSON");
}
#[test]
fn test_gp_with_tle_format() {
let query = CelestrakQuery::gp()
.group("stations")
.format(CelestrakOutputFormat::ThreeLe);
assert_eq!(query.build_url(), "GROUP=stations&FORMAT=3LE");
}
#[test]
fn test_gp_with_special() {
let query = CelestrakQuery::gp().special("all");
assert_eq!(query.build_url(), "SPECIAL=all");
}
#[test]
fn test_gp_multiple_params() {
let query = CelestrakQuery::gp()
.group("stations")
.catnr(25544)
.format(CelestrakOutputFormat::Json);
assert_eq!(query.build_url(), "GROUP=stations&CATNR=25544&FORMAT=JSON");
}
#[test]
fn test_sup_gp_by_source() {
let query = CelestrakQuery::sup_gp().source(SupGPSource::SpaceX);
assert_eq!(query.build_url(), "SOURCE=spacex");
}
#[test]
fn test_sup_gp_by_file() {
let query = CelestrakQuery::sup_gp().file("starlink");
assert_eq!(query.build_url(), "FILE=starlink");
}
#[test]
fn test_sup_gp_by_catnr() {
let query = CelestrakQuery::sup_gp().catnr(25544);
assert_eq!(query.build_url(), "CATNR=25544");
}
#[test]
fn test_sup_gp_with_source_and_format() {
let query = CelestrakQuery::sup_gp()
.source(SupGPSource::Starlink)
.format(CelestrakOutputFormat::Json);
assert_eq!(query.build_url(), "SOURCE=starlink&FORMAT=JSON");
}
#[test]
fn test_sup_gp_with_name() {
let query = CelestrakQuery::sup_gp().name_search("STARLINK-1234");
assert_eq!(query.build_url(), "NAME=STARLINK-1234");
}
#[test]
fn test_satcat_by_group() {
let query = CelestrakQuery::satcat().group("stations");
assert_eq!(query.build_url(), "GROUP=stations");
}
#[test]
fn test_satcat_active() {
let query = CelestrakQuery::satcat().active(true);
assert_eq!(query.build_url(), "ACTIVE=Y");
}
#[test]
fn test_satcat_payloads() {
let query = CelestrakQuery::satcat().payloads(true);
assert_eq!(query.build_url(), "PAYLOADS=Y");
}
#[test]
fn test_satcat_on_orbit() {
let query = CelestrakQuery::satcat().on_orbit(true);
assert_eq!(query.build_url(), "ONORBIT=Y");
}
#[test]
fn test_satcat_with_max() {
let query = CelestrakQuery::satcat().active(true).max(100);
assert_eq!(query.build_url(), "ACTIVE=Y&MAX=100");
}
#[test]
fn test_satcat_multiple_flags() {
let query = CelestrakQuery::satcat()
.active(true)
.payloads(true)
.on_orbit(true)
.format(CelestrakOutputFormat::Json);
assert_eq!(
query.build_url(),
"PAYLOADS=Y&ONORBIT=Y&ACTIVE=Y&FORMAT=JSON"
);
}
#[test]
fn test_satcat_false_flags_not_in_url() {
let query = CelestrakQuery::satcat()
.active(false)
.payloads(false)
.on_orbit(false);
assert_eq!(query.build_url(), "");
}
#[test]
fn test_satcat_by_name() {
let query = CelestrakQuery::satcat()
.name_search("ISS")
.format(CelestrakOutputFormat::Json);
assert_eq!(query.build_url(), "NAME=ISS&FORMAT=JSON");
}
#[test]
fn test_client_side_filter() {
let query = CelestrakQuery::gp()
.group("stations")
.filter("INCLINATION", ">50");
assert!(query.has_client_side_processing());
assert_eq!(query.client_side_filters().len(), 1);
assert_eq!(query.client_side_filters()[0].field, "INCLINATION");
assert_eq!(query.client_side_filters()[0].value, ">50");
assert_eq!(query.build_url(), "GROUP=stations");
}
#[test]
fn test_client_side_order_by() {
let query = CelestrakQuery::gp()
.group("stations")
.order_by("EPOCH", false);
assert!(query.has_client_side_processing());
assert_eq!(query.client_side_order_by().len(), 1);
assert_eq!(query.client_side_order_by()[0].field, "EPOCH");
assert!(!query.client_side_order_by()[0].ascending);
}
#[test]
fn test_client_side_limit() {
let query = CelestrakQuery::gp().group("stations").limit(10);
assert!(query.has_client_side_processing());
assert_eq!(query.client_side_limit(), Some(10));
}
#[test]
fn test_no_client_side_processing() {
let query = CelestrakQuery::gp().group("stations");
assert!(!query.has_client_side_processing());
}
#[test]
fn test_multiple_client_side_filters() {
let query = CelestrakQuery::gp()
.group("active")
.filter("INCLINATION", ">50")
.filter("ECCENTRICITY", "<0.01")
.filter("OBJECT_TYPE", "<>DEBRIS");
assert_eq!(query.client_side_filters().len(), 3);
}
#[test]
fn test_output_format_accessor_none() {
let query = CelestrakQuery::gp();
assert_eq!(query.output_format(), None);
}
#[test]
fn test_output_format_accessor_set() {
let query = CelestrakQuery::gp().format(CelestrakOutputFormat::Json);
assert_eq!(query.output_format(), Some(CelestrakOutputFormat::Json));
}
#[test]
fn test_builder_immutability() {
let base = CelestrakQuery::gp().group("stations");
let extended = base.clone().filter("INCLINATION", ">50");
assert!(!base.has_client_side_processing());
assert!(extended.has_client_side_processing());
assert_eq!(base.build_url(), "GROUP=stations");
assert_eq!(extended.build_url(), "GROUP=stations");
}
#[test]
fn test_query_clone() {
let query = CelestrakQuery::gp()
.group("stations")
.filter("INCLINATION", ">50")
.order_by("EPOCH", false)
.limit(10);
let cloned = query.clone();
assert_eq!(query.build_url(), cloned.build_url());
assert_eq!(query.query_type(), cloned.query_type());
assert_eq!(
query.client_side_filters().len(),
cloned.client_side_filters().len()
);
assert_eq!(
query.client_side_order_by().len(),
cloned.client_side_order_by().len()
);
assert_eq!(query.client_side_limit(), cloned.client_side_limit());
}
#[test]
fn test_all_output_formats() {
let formats = vec![
(CelestrakOutputFormat::Tle, "FORMAT=TLE"),
(CelestrakOutputFormat::TwoLe, "FORMAT=2LE"),
(CelestrakOutputFormat::ThreeLe, "FORMAT=3LE"),
(CelestrakOutputFormat::Xml, "FORMAT=XML"),
(CelestrakOutputFormat::Kvn, "FORMAT=KVN"),
(CelestrakOutputFormat::Json, "FORMAT=JSON"),
(CelestrakOutputFormat::JsonPretty, "FORMAT=JSON-PRETTY"),
(CelestrakOutputFormat::Csv, "FORMAT=CSV"),
];
for (format, expected) in formats {
let query = CelestrakQuery::gp().format(format);
assert_eq!(query.build_url(), expected);
}
}
#[test]
fn test_empty_query() {
let query = CelestrakQuery::gp();
assert_eq!(query.build_url(), "");
}
#[test]
fn test_query_debug() {
let query = CelestrakQuery::gp().group("stations");
let debug = format!("{:?}", query);
assert!(debug.contains("stations"));
assert!(debug.contains("GP"));
}
}