use chrono::Utc;
use super::error::FeaturesError;
use super::query::QueryParams;
use super::types::{
Collection, Collections, ConformanceClasses, Feature, FeatureCollection, LandingPage, Link,
};
pub const MAX_LIMIT: u32 = 10_000;
pub struct FeaturesServer {
pub title: String,
pub description: String,
pub base_url: String,
pub collections: Vec<Collection>,
}
impl FeaturesServer {
pub fn new(title: impl Into<String>, base_url: impl Into<String>) -> Self {
Self {
title: title.into(),
description: String::new(),
base_url: base_url.into(),
collections: vec![],
}
}
pub fn add_collection(&mut self, collection: Collection) {
self.collections.push(collection);
}
pub fn landing_page(&self) -> LandingPage {
let base = &self.base_url;
LandingPage {
title: self.title.clone(),
description: if self.description.is_empty() {
None
} else {
Some(self.description.clone())
},
links: vec![
Link::new(base.clone(), "self")
.with_type("application/json")
.with_title("This document"),
Link::new(format!("{base}/api"), "service-desc")
.with_type("application/vnd.oai.openapi+json;version=3.0")
.with_title("The API definition"),
Link::new(format!("{base}/conformance"), "conformance")
.with_type("application/json")
.with_title("Conformance classes"),
Link::new(format!("{base}/collections"), "data")
.with_type("application/json")
.with_title("Access the data"),
],
}
}
pub fn conformance(&self) -> ConformanceClasses {
ConformanceClasses::with_crs()
}
pub fn list_collections(&self) -> Collections {
let base = &self.base_url;
Collections {
links: vec![
Link::new(format!("{base}/collections"), "self")
.with_type("application/json")
.with_title("Collections"),
],
collections: self.collections.clone(),
}
}
pub fn get_collection(&self, id: &str) -> Option<&Collection> {
self.collections.iter().find(|c| c.id == id)
}
pub fn build_items_response(
&self,
collection_id: &str,
features: Vec<Feature>,
params: &QueryParams,
total_matched: Option<u64>,
) -> Result<FeatureCollection, FeaturesError> {
if self.get_collection(collection_id).is_none() {
return Err(FeaturesError::CollectionNotFound(collection_id.to_string()));
}
let limit = params.effective_limit();
if limit > MAX_LIMIT {
return Err(FeaturesError::LimitExceeded {
requested: limit,
max: MAX_LIMIT,
});
}
let offset = params.effective_offset() as usize;
let limit_usize = limit as usize;
let total = features.len();
let page: Vec<Feature> = features
.into_iter()
.skip(offset)
.take(limit_usize)
.collect();
let number_returned = page.len() as u64;
let number_matched = total_matched.unwrap_or(total as u64);
let base = &self.base_url;
let items_base = format!("{base}/collections/{collection_id}/items");
let mut links: Vec<Link> = vec![
Link::new(
format!("{items_base}?limit={limit}&offset={offset}"),
"self",
)
.with_type("application/geo+json")
.with_title("This page"),
];
let next_offset = offset + limit_usize;
if next_offset < total {
links.push(
Link::new(
format!("{items_base}?limit={limit}&offset={next_offset}"),
"next",
)
.with_type("application/geo+json")
.with_title("Next page"),
);
}
if offset > 0 {
let prev_offset = offset.saturating_sub(limit_usize);
links.push(
Link::new(
format!("{items_base}?limit={limit}&offset={prev_offset}"),
"prev",
)
.with_type("application/geo+json")
.with_title("Previous page"),
);
}
Ok(FeatureCollection {
type_: "FeatureCollection".to_string(),
features: page,
links: Some(links),
time_stamp: Some(Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()),
number_matched: Some(number_matched),
number_returned: Some(number_returned),
})
}
}