use std::collections::BTreeMap;
use serde::Deserialize;
use serde_json::{Map, Value};
use time::format_description::FormatItem;
use time::macros::format_description;
use time::{Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset};
use super::{
CurrentData, DailyData, ForecastResponse, HourlyData, Minutely15Data, MonthlyData,
SeriesValues, VariableDescriptor, VariableSeries,
};
use crate::json_sanitize::sanitize_open_meteo_floats;
use crate::{Error, Result};
const DATE_TIME_FORMAT: &[FormatItem<'_>] =
format_description!("[year]-[month]-[day]T[hour]:[minute]");
const DATE_FORMAT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]");
pub(crate) fn decode_forecast_json(bytes: &[u8]) -> Result<ForecastResponse> {
let sanitized = sanitize_open_meteo_floats(bytes);
let raw: RawForecastResponse = serde_json::from_slice(sanitized.as_ref())?;
decode_raw_forecast(raw)
}
pub(crate) fn decode_forecast_json_many(bytes: &[u8]) -> Result<Vec<ForecastResponse>> {
let sanitized = sanitize_open_meteo_floats(bytes);
if sanitized
.as_ref()
.iter()
.find(|byte| !byte.is_ascii_whitespace())
== Some(&b'[')
{
let raw: Vec<RawForecastResponse> = serde_json::from_slice(sanitized.as_ref())?;
raw.into_iter().map(decode_raw_forecast).collect()
} else {
decode_forecast_json(sanitized.as_ref()).map(|response| vec![response])
}
}
fn decode_raw_forecast(raw: RawForecastResponse) -> Result<ForecastResponse> {
let offset_seconds = raw
.utc_offset_seconds
.ok_or_else(|| Error::InvalidResponse {
reason: "response is missing `utc_offset_seconds`".into(),
})?;
Ok(ForecastResponse {
latitude: raw.latitude,
longitude: raw.longitude,
elevation: raw.elevation,
generation_time_ms: raw.generation_time_ms,
utc_offset_seconds: offset_seconds,
timezone: raw.timezone,
timezone_abbreviation: raw.timezone_abbreviation,
current: decode_current(raw.current, raw.current_units, offset_seconds)?,
hourly: decode_timed_group(
raw.hourly,
raw.hourly_units,
offset_seconds,
TimeGroupKind::Instant,
)?
.map(|group| HourlyData {
time: group.time,
variables: group.variables,
}),
minutely_15: decode_timed_group(
raw.minutely_15,
raw.minutely_15_units,
offset_seconds,
TimeGroupKind::Instant,
)?
.map(|group| Minutely15Data {
time: group.time,
variables: group.variables,
}),
daily: decode_timed_group(
raw.daily,
raw.daily_units,
offset_seconds,
TimeGroupKind::Calendar,
)?
.map(|group| DailyData {
time: group.time,
variables: group.variables,
}),
monthly: decode_timed_group(
raw.monthly,
raw.monthly_units,
offset_seconds,
TimeGroupKind::Calendar,
)?
.map(|group| MonthlyData {
time: group.time,
variables: group.variables,
}),
})
}
#[derive(Debug, Deserialize)]
struct RawForecastResponse {
latitude: f64,
longitude: f64,
elevation: Option<f64>,
#[serde(rename = "generationtime_ms")]
generation_time_ms: Option<f32>,
utc_offset_seconds: Option<i32>,
timezone: Option<String>,
timezone_abbreviation: Option<String>,
current_units: Option<BTreeMap<String, String>>,
current: Option<Map<String, Value>>,
hourly_units: Option<BTreeMap<String, String>>,
hourly: Option<Map<String, Value>>,
minutely_15_units: Option<BTreeMap<String, String>>,
minutely_15: Option<Map<String, Value>>,
daily_units: Option<BTreeMap<String, String>>,
daily: Option<Map<String, Value>>,
monthly_units: Option<BTreeMap<String, String>>,
monthly: Option<Map<String, Value>>,
}
struct TimedGroup {
time: Vec<OffsetDateTime>,
variables: Vec<VariableSeries>,
}
#[derive(Debug, Clone, Copy)]
enum TimeGroupKind {
Instant,
Calendar,
}
fn decode_current(
current: Option<Map<String, Value>>,
units: Option<BTreeMap<String, String>>,
offset_seconds: i32,
) -> Result<Option<CurrentData>> {
let Some(current) = current else {
return Ok(None);
};
let units = units.unwrap_or_default();
let time = match current.get("time") {
Some(value) => Some(parse_time_value(
value,
offset_seconds,
TimeGroupKind::Instant,
)?),
None => None,
};
let mut variables = Vec::new();
for (key, value) in current {
if key == "time" || key == "interval" {
continue;
}
let values = match value {
Value::Number(number) => {
let value = number.as_f64().ok_or_else(|| Error::InvalidResponse {
reason: format!("current field `{key}` is not representable as f32"),
})?;
SeriesValues::F32(vec![Some(value as f32)])
}
Value::String(value) => SeriesValues::Strings(vec![Some(value)]),
Value::Null => SeriesValues::F32(vec![None]),
_ => continue,
};
variables.push(VariableSeries {
descriptor: VariableDescriptor::from_api_name(&key),
unit: units.get(&key).cloned(),
values,
});
}
Ok(Some(CurrentData { time, variables }))
}
fn decode_timed_group(
group: Option<Map<String, Value>>,
units: Option<BTreeMap<String, String>>,
offset_seconds: i32,
kind: TimeGroupKind,
) -> Result<Option<TimedGroup>> {
let Some(group) = group else {
return Ok(None);
};
let units = units.unwrap_or_default();
let Some(time_value) = group.get("time") else {
return Err(Error::InvalidResponse {
reason: "time-series group is missing `time`".into(),
});
};
let time = parse_time_array(time_value, offset_seconds, kind)?;
let mut variables = Vec::new();
for (key, value) in group {
if key == "time" {
continue;
}
let values = parse_series_values(&key, &value)?;
if values.len() != time.len() {
return Err(Error::InvalidResponse {
reason: format!(
"field `{key}` has {} values but `time` has {}",
values.len(),
time.len()
),
});
}
variables.push(VariableSeries {
descriptor: VariableDescriptor::from_api_name(&key),
unit: units.get(&key).cloned(),
values,
});
}
Ok(Some(TimedGroup { time, variables }))
}
fn parse_series_values(key: &str, value: &Value) -> Result<SeriesValues> {
let array = value.as_array().ok_or_else(|| Error::InvalidResponse {
reason: format!("field `{key}` is not an array"),
})?;
if array
.iter()
.all(|value| value.is_number() || value.is_null())
{
let mut values = Vec::with_capacity(array.len());
for value in array {
if value.is_null() {
values.push(None);
} else {
let value = value.as_f64().ok_or_else(|| Error::InvalidResponse {
reason: format!("field `{key}` contains a non-f32 numeric value"),
})?;
values.push(Some(value as f32));
}
}
Ok(SeriesValues::F32(values))
} else if array
.iter()
.all(|value| value.is_string() || value.is_null())
{
Ok(SeriesValues::Strings(
array
.iter()
.map(|value| value.as_str().map(str::to_owned))
.collect(),
))
} else {
Err(Error::InvalidResponse {
reason: format!("field `{key}` contains mixed or unsupported values"),
})
}
}
fn parse_time_array(
value: &Value,
offset_seconds: i32,
kind: TimeGroupKind,
) -> Result<Vec<OffsetDateTime>> {
let array = value.as_array().ok_or_else(|| Error::InvalidResponse {
reason: "`time` is not an array".into(),
})?;
array
.iter()
.map(|value| parse_time_value(value, offset_seconds, kind))
.collect()
}
fn parse_time_value(
value: &Value,
offset_seconds: i32,
kind: TimeGroupKind,
) -> Result<OffsetDateTime> {
match value {
Value::String(value) => parse_time_str(value, offset_seconds),
Value::Number(value) => {
let unix = value.as_i64().ok_or_else(|| Error::InvalidResponse {
reason: "`time` unix timestamp is not an integer".into(),
})?;
parse_unix_time(unix, offset_seconds, kind).map_err(|err| Error::TimeParse {
value: unix.to_string(),
reason: err.to_string(),
})
}
_ => Err(Error::InvalidResponse {
reason: "`time` contains unsupported value".into(),
}),
}
}
fn parse_unix_time(
unix: i64,
offset_seconds: i32,
kind: TimeGroupKind,
) -> std::result::Result<OffsetDateTime, time::error::ComponentRange> {
let offset = UtcOffset::from_whole_seconds(offset_seconds)?;
let instant = OffsetDateTime::from_unix_timestamp(unix)?.to_offset(offset);
match kind {
TimeGroupKind::Instant => Ok(instant),
TimeGroupKind::Calendar => Ok(instant
.date()
.with_time(Time::MIDNIGHT)
.assume_offset(offset)),
}
}
fn parse_time_str(value: &str, offset_seconds: i32) -> Result<OffsetDateTime> {
let offset = UtcOffset::from_whole_seconds(offset_seconds).map_err(|err| Error::TimeParse {
value: value.to_owned(),
reason: err.to_string(),
})?;
if value.len() == 10 {
let date = Date::parse(value, DATE_FORMAT).map_err(|err| Error::TimeParse {
value: value.to_owned(),
reason: err.to_string(),
})?;
return Ok(date.with_time(Time::MIDNIGHT).assume_offset(offset));
}
let local =
PrimitiveDateTime::parse(value, DATE_TIME_FORMAT).map_err(|err| Error::TimeParse {
value: value.to_owned(),
reason: err.to_string(),
})?;
Ok(local.assume_offset(offset))
}