use crate::Record;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct FiscalYearObligation {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fiscal_year: Option<i64>,
#[serde(
default,
deserialize_with = "crate::models::budget::number_or_numeric_string",
skip_serializing_if = "Option::is_none"
)]
pub obligated: Option<f64>,
#[serde(flatten, default)]
pub extra: HashMap<String, Value>,
}
impl FiscalYearObligation {
pub fn from_record(record: &Record) -> serde_json::Result<Option<Vec<Self>>> {
match record.get("obligations_by_fiscal_year") {
None | Some(Value::Null) => Ok(None),
Some(v) => serde_json::from_value(v.clone()).map(Some),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn record(v: Value) -> Record {
v.as_object().cloned().expect("object")
}
#[test]
fn decodes_years_in_order_including_a_negative_year() {
let r = record(json!({
"key": "CONT_AWD_1",
"obligations_by_fiscal_year": [
{"fiscal_year": 2024, "obligated": 125_000.5},
{"fiscal_year": 2025, "obligated": -1_200.0},
{"fiscal_year": 2026, "obligated": "300.25"}
]
}));
let years = FiscalYearObligation::from_record(&r)
.expect("decode")
.expect("present");
assert_eq!(years.len(), 3);
assert_eq!(years[0].fiscal_year, Some(2024));
assert_eq!(years[0].obligated, Some(125_000.5));
assert_eq!(years[1].obligated, Some(-1_200.0));
assert_eq!(years[2].obligated, Some(300.25));
}
#[test]
fn null_and_absent_are_none() {
for v in [json!({"obligations_by_fiscal_year": null}), json!({})] {
assert!(FiscalYearObligation::from_record(&record(v))
.expect("decode")
.is_none());
}
}
#[test]
fn non_list_is_an_error() {
let r = record(json!({"obligations_by_fiscal_year": {"fiscal_year": 2025}}));
assert!(FiscalYearObligation::from_record(&r).is_err());
}
}