use crate::errors::Nc2ParquetError;
use crate::filters::{NC2DPointFilter, NC3DPointFilter, NCFilter, NCListFilter, NCRangeFilter};
use crate::postprocess::ProcessingPipelineConfig;
use polars::prelude::{GzipLevel, ParquetCompression, ZstdLevel};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CompressionCodec {
Uncompressed,
Snappy,
Gzip,
Lz4,
Zstd,
}
fn default_compression() -> CompressionCodec {
CompressionCodec::Snappy
}
fn default_statistics() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OutputConfig {
#[serde(default = "default_compression")]
pub compression: CompressionCodec,
#[serde(skip_serializing_if = "Option::is_none")]
pub compression_level: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub row_group_size: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_page_size: Option<usize>,
#[serde(default = "default_statistics")]
pub statistics: bool,
}
impl Default for OutputConfig {
fn default() -> Self {
OutputConfig {
compression: default_compression(),
compression_level: None,
row_group_size: None,
data_page_size: None,
statistics: default_statistics(),
}
}
}
impl OutputConfig {
pub fn to_polars_compression(&self) -> ParquetCompression {
match self.compression {
CompressionCodec::Uncompressed => ParquetCompression::Uncompressed,
CompressionCodec::Snappy => ParquetCompression::Snappy,
CompressionCodec::Gzip => {
let level = self
.compression_level
.and_then(|l| GzipLevel::try_new(l as u8).ok());
ParquetCompression::Gzip(level)
}
CompressionCodec::Lz4 => ParquetCompression::Lz4Raw,
CompressionCodec::Zstd => {
let level = self
.compression_level
.and_then(|l| ZstdLevel::try_new(l as i32).ok());
ParquetCompression::Zstd(level)
}
}
}
pub fn validate(&self) -> Result<(), Nc2ParquetError> {
if let Some(level) = self.compression_level {
match self.compression {
CompressionCodec::Zstd => {
let l = level as i32;
if !(1..=22).contains(&l) {
return Err(Nc2ParquetError::Config(format!(
"Zstd compression level must be between 1 and 22, got {}",
level
)));
}
}
CompressionCodec::Gzip => {
if level > 9 {
return Err(Nc2ParquetError::Config(format!(
"Gzip compression level must be between 0 and 9, got {}",
level
)));
}
}
CompressionCodec::Snappy
| CompressionCodec::Lz4
| CompressionCodec::Uncompressed => {
return Err(Nc2ParquetError::Config(format!(
"{:?} compression does not accept a compression level",
self.compression
)));
}
}
}
if let Some(rg) = self.row_group_size
&& rg == 0
{
return Err(Nc2ParquetError::Config(
"row_group_size must be greater than 0".to_string(),
));
}
Ok(())
}
}
#[derive(Deserialize, Serialize, Clone)]
pub struct JobConfig {
pub nc_key: String,
pub variable_name: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub variable_names: Option<Vec<String>>,
pub filters: Vec<FilterConfig>,
pub parquet_key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub postprocessing: Option<ProcessingPipelineConfig>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub output: Option<OutputConfig>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(tag = "kind")]
pub enum FilterConfig {
#[serde(rename = "range")]
Range {
params: RangeParams,
},
#[serde(rename = "list")]
List {
params: ListParams,
},
#[serde(rename = "2d_point")]
Point2D {
params: Point2DParams,
},
#[serde(rename = "3d_point")]
Point3D {
params: Point3DParams,
},
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct RangeParams {
pub dimension_name: String,
pub min_value: f64,
pub max_value: f64,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ListParams {
pub dimension_name: String,
pub values: Vec<f64>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Point2DParams {
pub lat_dimension_name: String,
pub lon_dimension_name: String,
pub points: Vec<(f64, f64)>,
pub tolerance: f64,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Point3DParams {
pub time_dimension_name: String,
pub lat_dimension_name: String,
pub lon_dimension_name: String,
pub steps: Vec<f64>,
pub points: Vec<(f64, f64)>,
pub tolerance: f64,
}
impl JobConfig {
pub fn effective_variable_names(&self) -> Vec<String> {
if let Some(ref names) = self.variable_names {
names.clone()
} else {
vec![self.variable_name.clone()]
}
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Nc2ParquetError> {
let content = fs::read_to_string(path)?;
let config: JobConfig = serde_json::from_str(&content)
.map_err(|e| Nc2ParquetError::Serialization(e.to_string()))?;
Ok(config)
}
pub fn from_json(json_str: &str) -> Result<Self, Nc2ParquetError> {
let config: JobConfig = serde_json::from_str(json_str)
.map_err(|e| Nc2ParquetError::Serialization(e.to_string()))?;
Ok(config)
}
}
impl FilterConfig {
pub fn to_filter(&self) -> Result<Box<dyn NCFilter>, Nc2ParquetError> {
match self {
FilterConfig::Range { params } => {
let filter =
NCRangeFilter::new(¶ms.dimension_name, params.min_value, params.max_value);
Ok(Box::new(filter))
}
FilterConfig::List { params } => {
let filter = NCListFilter::new(¶ms.dimension_name, params.values.clone());
Ok(Box::new(filter))
}
FilterConfig::Point2D { params } => {
let filter = NC2DPointFilter::new(
¶ms.lat_dimension_name,
¶ms.lon_dimension_name,
params.points.clone(),
params.tolerance,
);
Ok(Box::new(filter))
}
FilterConfig::Point3D { params } => {
let filter = NC3DPointFilter::new(
¶ms.time_dimension_name,
¶ms.lat_dimension_name,
¶ms.lon_dimension_name,
params.steps.clone(),
params.points.clone(),
params.tolerance,
);
Ok(Box::new(filter))
}
}
}
pub fn kind(&self) -> &'static str {
match self {
FilterConfig::Range { .. } => "range",
FilterConfig::List { .. } => "list",
FilterConfig::Point2D { .. } => "2d_point",
FilterConfig::Point3D { .. } => "3d_point",
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct BatchConfig {
pub pattern: String,
pub output_dir: String,
pub variable_name: String,
pub filters: Vec<FilterConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub postprocessing: Option<ProcessingPipelineConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_template: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub output: Option<OutputConfig>,
pub fail_fast: bool,
}
#[derive(Debug)]
pub struct BatchResult {
pub succeeded: Vec<String>,
pub failed: Vec<(String, Nc2ParquetError)>,
pub total_files: usize,
}