use crate::{data::DataType, dds::*, url_builder::*};
use std::collections::HashMap;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq)]
pub enum CoordinateConstraint {
Indices {
start: usize,
end: usize,
stride: Option<usize>,
},
Single(usize),
}
impl CoordinateConstraint {
pub fn single(index: usize) -> Self {
CoordinateConstraint::Single(index)
}
pub fn range(start: usize, end: usize) -> Self {
CoordinateConstraint::Indices {
start,
end,
stride: None,
}
}
pub fn range_with_stride(start: usize, end: usize, stride: usize) -> Self {
CoordinateConstraint::Indices {
start,
end,
stride: Some(stride),
}
}
pub fn first() -> Self {
CoordinateConstraint::Single(0)
}
pub fn last(size: u32) -> Self {
CoordinateConstraint::Single((size.saturating_sub(1)) as usize)
}
pub fn validate(&self, coord_name: &str, size: u32) -> Result<(), QueryError> {
match self {
CoordinateConstraint::Single(index) => {
if *index >= size as usize {
return Err(QueryError::IndexOutOfBounds(
*index,
coord_name.to_string(),
size,
));
}
}
CoordinateConstraint::Indices { start, end, .. } => {
if *start >= size as usize {
return Err(QueryError::IndexOutOfBounds(
*start,
coord_name.to_string(),
size,
));
}
if *end >= size as usize {
return Err(QueryError::IndexOutOfBounds(
*end,
coord_name.to_string(),
size,
));
}
if start > end {
return Err(QueryError::InvalidCoordinateRange(
format!("Start index {start} is greater than end index {end} for coordinate '{coord_name}'")
));
}
}
}
Ok(())
}
pub fn to_index_ranges(&self) -> Vec<IndexRange> {
match self {
CoordinateConstraint::Single(index) => vec![IndexRange::Single(*index as isize)],
CoordinateConstraint::Indices { start, end, stride } => {
vec![IndexRange::Range {
start: *start as isize,
end: *end as isize,
stride: stride.map(|s| s as isize),
}]
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum VariableType {
Array,
Grid,
Structure,
Sequence,
}
#[derive(Debug, Clone)]
pub struct VariableInfo {
pub name: String,
pub data_type: DataType,
pub coordinates: Vec<String>,
pub dimensions: Vec<(String, u32)>,
pub variable_type: VariableType,
}
#[derive(Debug, Clone)]
pub struct CoordinateInfo {
pub name: String,
pub data_type: DataType,
pub size: u32,
pub variables_using: Vec<String>,
}
#[derive(Debug, Error)]
pub enum QueryError {
#[error("Variable '{0}' not found in dataset")]
VariableNotFound(String),
#[error("Coordinate '{0}' not found in dataset")]
CoordinateNotFound(String),
#[error("Coordinate '{0}' not available for variable '{1}'")]
CoordinateNotAvailableForVariable(String, String),
#[error("Invalid coordinate range: {0}")]
InvalidCoordinateRange(String),
#[error("Index {0} out of bounds for coordinate '{1}' (size: {2})")]
IndexOutOfBounds(usize, String, u32),
#[error("URL generation error: {0}")]
UrlGenerationError(String),
#[error("No variables selected for query")]
NoVariablesSelected,
}
pub struct DatasetQuery<'a> {
dataset: &'a DdsDataset,
base_url: String,
selected_variables: Vec<String>,
coordinate_constraints: HashMap<String, CoordinateConstraint>,
}
impl<'a> DatasetQuery<'a> {
pub fn new(dataset: &'a DdsDataset, base_url: String) -> Self {
Self {
dataset,
base_url,
selected_variables: Vec::new(),
coordinate_constraints: HashMap::new(),
}
}
pub fn select_variable(mut self, name: &str) -> Result<Self, QueryError> {
if !self.dataset.has_variable(name) {
return Err(QueryError::VariableNotFound(name.to_string()));
}
if !self.selected_variables.contains(&name.to_string()) {
self.selected_variables.push(name.to_string());
}
Ok(self)
}
pub fn select_variables(mut self, names: &[&str]) -> Result<Self, QueryError> {
for name in names {
self = self.select_variable(name)?;
}
Ok(self)
}
pub fn select_by_coordinate(
mut self,
coord_name: &str,
constraint: CoordinateConstraint,
) -> Result<Self, QueryError> {
if !self.dataset.has_coordinate(coord_name) {
return Err(QueryError::CoordinateNotFound(coord_name.to_string()));
}
if let Some(coord_info) = self.dataset.get_coordinate_info(coord_name) {
constraint.validate(coord_name, coord_info.size)?;
}
if !self.selected_variables.is_empty() {
for var_name in &self.selected_variables {
if let Some(var_info) = self.dataset.get_variable_info(var_name) {
if !var_info.coordinates.contains(&coord_name.to_string()) {
return Err(QueryError::CoordinateNotAvailableForVariable(
coord_name.to_string(),
var_name.to_string(),
));
}
}
}
}
self.coordinate_constraints
.insert(coord_name.to_string(), constraint);
Ok(self)
}
pub fn dods_url(self) -> Result<String, QueryError> {
if self.selected_variables.is_empty() {
return Err(QueryError::NoVariablesSelected);
}
let mut url_builder = UrlBuilder::new(&self.base_url);
for var_name in &self.selected_variables {
url_builder = url_builder.add_variable(var_name);
}
for var_name in &self.selected_variables {
if let Some(var_info) = self.dataset.get_variable_info(var_name) {
let mut constraint_indices = Vec::new();
for coord_name in &var_info.coordinates {
if let Some(constraint) = self.coordinate_constraints.get(coord_name) {
constraint_indices.extend(constraint.to_index_ranges());
}
}
if !constraint_indices.is_empty() {
url_builder =
url_builder.add_multidimensional_constraint(var_name, constraint_indices);
}
}
}
url_builder
.dods_url()
.map_err(|e| QueryError::UrlGenerationError(e.to_string()))
}
pub fn das_url(&self) -> String {
UrlBuilder::new(&self.base_url).das_url()
}
pub fn dds_url(&self) -> String {
UrlBuilder::new(&self.base_url).dds_url()
}
pub fn validate(&self) -> Result<(), QueryError> {
if self.selected_variables.is_empty() {
return Err(QueryError::NoVariablesSelected);
}
for (coord_name, constraint) in &self.coordinate_constraints {
if let Some(coord_info) = self.dataset.get_coordinate_info(coord_name) {
constraint.validate(coord_name, coord_info.size)?;
}
}
Ok(())
}
pub fn estimated_size(&self) -> usize {
let mut total_size = 0;
for var_name in &self.selected_variables {
if let Some(var_info) = self.dataset.get_variable_info(var_name) {
let mut var_size = var_info.data_type.byte_count();
for (coord_name, coord_size) in &var_info.dimensions {
let effective_size =
if let Some(constraint) = self.coordinate_constraints.get(coord_name) {
match constraint {
CoordinateConstraint::Single(_) => 1,
CoordinateConstraint::Indices { start, end, stride } => {
let range_size = end - start + 1;
if let Some(stride_val) = stride {
range_size.div_ceil(*stride_val)
} else {
range_size
}
}
}
} else {
*coord_size as usize
};
var_size *= effective_size;
}
total_size += var_size;
}
}
total_size
}
pub fn selected_variables(&self) -> &[String] {
&self.selected_variables
}
pub fn active_constraints(&self) -> &HashMap<String, CoordinateConstraint> {
&self.coordinate_constraints
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_dataset() -> DdsDataset {
let dds_content = r#"Dataset {
Float32 latitude[latitude = 5];
Float32 longitude[longitude = 10];
Int32 time[time = 100];
Grid {
ARRAY:
Float32 temperature[time = 100][latitude = 5][longitude = 10];
MAPS:
Int32 time[time = 100];
Float32 latitude[latitude = 5];
Float32 longitude[longitude = 10];
} temperature;
Grid {
ARRAY:
Float32 wind_speed[time = 100][latitude = 5][longitude = 10];
MAPS:
Int32 time[time = 100];
Float32 latitude[latitude = 5];
Float32 longitude[longitude = 10];
} wind_speed;
} test_dataset;"#;
DdsDataset::from_bytes(dds_content).unwrap()
}
#[test]
fn test_coordinate_constraint_creation() {
let single = CoordinateConstraint::single(5);
assert_eq!(single, CoordinateConstraint::Single(5));
let range = CoordinateConstraint::range(0, 10);
assert_eq!(
range,
CoordinateConstraint::Indices {
start: 0,
end: 10,
stride: None
}
);
let range_with_stride = CoordinateConstraint::range_with_stride(0, 20, 2);
assert_eq!(
range_with_stride,
CoordinateConstraint::Indices {
start: 0,
end: 20,
stride: Some(2)
}
);
let first = CoordinateConstraint::first();
assert_eq!(first, CoordinateConstraint::Single(0));
let last = CoordinateConstraint::last(100);
assert_eq!(last, CoordinateConstraint::Single(99));
}
#[test]
fn test_coordinate_constraint_validation() {
let constraint = CoordinateConstraint::single(5);
assert!(constraint.validate("test", 10).is_ok());
assert!(constraint.validate("test", 5).is_err());
let range_constraint = CoordinateConstraint::range(0, 9);
assert!(range_constraint.validate("test", 10).is_ok());
assert!(range_constraint.validate("test", 5).is_err());
let invalid_range = CoordinateConstraint::range(10, 5);
assert!(invalid_range.validate("test", 20).is_err()); }
#[test]
fn test_basic_query_building() {
let dataset = create_test_dataset();
let query = dataset
.query("https://example.com/data")
.select_variable("temperature")
.unwrap();
assert_eq!(query.selected_variables(), &["temperature"]);
let url = query.dods_url().unwrap();
assert_eq!(url, "https://example.com/data.dods?temperature");
}
#[test]
fn test_multiple_variable_selection() {
let dataset = create_test_dataset();
let query = dataset
.query("https://example.com/data")
.select_variables(&["temperature", "wind_speed"])
.unwrap();
assert_eq!(query.selected_variables(), &["temperature", "wind_speed"]);
let url = query.dods_url().unwrap();
assert_eq!(url, "https://example.com/data.dods?temperature,wind_speed");
}
#[test]
fn test_coordinate_constraints() {
let dataset = create_test_dataset();
let query = dataset
.query("https://example.com/data")
.select_variable("temperature")
.unwrap()
.select_by_coordinate("time", CoordinateConstraint::range(0, 10))
.unwrap()
.select_by_coordinate("latitude", CoordinateConstraint::single(2))
.unwrap()
.select_by_coordinate(
"longitude",
CoordinateConstraint::range_with_stride(0, 8, 2),
)
.unwrap();
let url = query.dods_url().unwrap();
assert_eq!(
url,
"https://example.com/data.dods?temperature[0:10][2][0:2:8]"
);
}
#[test]
fn test_query_validation_errors() {
let dataset = create_test_dataset();
let result = dataset
.query("https://example.com/data")
.select_variable("nonexistent");
assert!(matches!(result, Err(QueryError::VariableNotFound(_))));
let result = dataset
.query("https://example.com/data")
.select_variable("temperature")
.unwrap()
.select_by_coordinate("nonexistent", CoordinateConstraint::single(0));
assert!(matches!(result, Err(QueryError::CoordinateNotFound(_))));
let result = dataset
.query("https://example.com/data")
.select_variable("latitude")
.unwrap() .select_by_coordinate("time", CoordinateConstraint::single(0));
assert!(matches!(
result,
Err(QueryError::CoordinateNotAvailableForVariable(_, _))
));
let result = dataset
.query("https://example.com/data")
.select_variable("temperature")
.unwrap()
.select_by_coordinate("latitude", CoordinateConstraint::single(10)); assert!(matches!(result, Err(QueryError::IndexOutOfBounds(_, _, _))));
let result = dataset.query("https://example.com/data").dods_url();
assert!(matches!(result, Err(QueryError::NoVariablesSelected)));
}
#[test]
fn test_estimated_size() {
let dataset = create_test_dataset();
let query = dataset
.query("https://example.com/data")
.select_variable("temperature")
.unwrap();
assert_eq!(query.estimated_size(), 20000);
let query = dataset
.query("https://example.com/data")
.select_variable("temperature")
.unwrap()
.select_by_coordinate("time", CoordinateConstraint::range(0, 10))
.unwrap()
.select_by_coordinate("latitude", CoordinateConstraint::single(2))
.unwrap()
.select_by_coordinate(
"longitude",
CoordinateConstraint::range_with_stride(0, 8, 2),
)
.unwrap();
assert_eq!(query.estimated_size(), 220);
}
#[test]
fn test_das_dds_urls() {
let dataset = create_test_dataset();
let query = dataset
.query("https://example.com/data")
.select_variable("temperature")
.unwrap();
assert_eq!(query.das_url(), "https://example.com/data.das");
assert_eq!(query.dds_url(), "https://example.com/data.dds");
}
#[test]
fn test_query_introspection() {
let dataset = create_test_dataset();
let query = dataset
.query("https://example.com/data")
.select_variable("temperature")
.unwrap()
.select_by_coordinate("time", CoordinateConstraint::range(0, 10))
.unwrap();
assert_eq!(query.selected_variables(), &["temperature"]);
let constraints = query.active_constraints();
assert_eq!(constraints.len(), 1);
assert!(constraints.contains_key("time"));
assert_eq!(constraints["time"], CoordinateConstraint::range(0, 10));
assert!(query.validate().is_ok());
}
}