// now: Nix-based distributed command runner // Copyright (C) 2026 Eric Rodrigues Pires // // This program is free software: you can redistribute it and/or modify it under // the terms of the GNU Affero General Public License as published by the Free // Software Foundation, either version 3 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for // more details. // // You should have received a copy of the GNU Affero General Public License along // with this program. If not, see . use std::path::PathBuf; use ahash::HashMap; use serde::{Deserialize, Serialize, de::Visitor, ser::SerializeStruct}; use crate::{ environment::eval_id, workflow::{NowJobContainer, NowStep, NowStepDownload, NowStepEnvVar, NowStepSecret}, }; impl Serialize for NowJobContainer { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { match self { NowJobContainer::Single(job) => job.serialize(serializer), NowJobContainer::Multiple(job_vec) => job_vec.serialize(serializer), } } } impl<'de> Deserialize<'de> for NowJobContainer { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { struct NowJobContainerVisitor; impl<'de> Visitor<'de> for NowJobContainerVisitor { type Value = NowJobContainer; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a job or list of jobs") } fn visit_seq(self, mut seq: A) -> Result where A: serde::de::SeqAccess<'de>, { let mut job_vec = seq.size_hint().map(Vec::with_capacity).unwrap_or_default(); while let Some(job) = seq.next_element()? { job_vec.push(job); } Ok(NowJobContainer::Multiple(job_vec)) } fn visit_map(self, map: A) -> Result where A: serde::de::MapAccess<'de>, { Ok(NowJobContainer::Single(Deserialize::deserialize( serde::de::value::MapAccessDeserializer::new(map), )?)) } } deserializer.deserialize_any(NowJobContainerVisitor) } } impl Serialize for NowStep { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { let mut strct = serializer.serialize_struct("NowStep", 5)?; strct.serialize_field("name", &self.name)?; strct.serialize_field("runDrv", &self.run_drv)?; strct.serialize_field("teardownDrv", &self.teardown_drv)?; strct.serialize_field("env", &self.env)?; strct.serialize_field("outputVar", &self.output_var)?; strct.serialize_field("uploadKey", &self.upload_key)?; strct.end() } } impl<'de> Deserialize<'de> for NowStep { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { struct NowStepVisitor; impl<'de> Visitor<'de> for NowStepVisitor { type Value = NowStep; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a step") } fn visit_map(self, mut map: A) -> Result where A: serde::de::MapAccess<'de>, { let mut name: Option = None; let mut run_drv: Option = None; let mut teardown_drv: Option = None; let mut env: Option> = None; let mut output_var: Option = None; let mut upload_key: Option = None; while let Some(key) = map.next_key::()? { match key.as_ref() { "name" => name = Some(map.next_value()?), "runDrv" => run_drv = Some(map.next_value()?), "teardownDrv" => teardown_drv = map.next_value()?, "env" => env = Some(map.next_value()?), "outputVar" => output_var = map.next_value()?, _ if matches!(key.split_once(eval_id()), Some(("__nowUpload_", ""))) => { upload_key = map.next_value()?; } _ => {} // Ignore unknown keys } } let name = name.ok_or_else(|| serde::de::Error::missing_field("name"))?; let run_drv = run_drv.ok_or_else(|| serde::de::Error::missing_field("runDrv"))?; let env = env.ok_or_else(|| serde::de::Error::missing_field("env"))?; Ok(NowStep { name, run_drv, teardown_drv, env, output_var, upload_key, }) } } deserializer.deserialize_struct( "NowStep", &["name", "run_drv", "teardown_drv", "env", "upload_key"], NowStepVisitor, ) } } impl Serialize for NowStepSecret { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { let mut strct = serializer.serialize_struct("NowStepSecret", 1)?; strct.serialize_field("secretName", &self.secret_name)?; strct.end() } } impl<'de> Deserialize<'de> for NowStepSecret { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { struct NowStepSecretVisitor; impl<'de> Visitor<'de> for NowStepSecretVisitor { type Value = NowStepSecret; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("an env secret") } fn visit_map(self, mut map: A) -> Result where A: serde::de::MapAccess<'de>, { let key = map .next_key::()? .ok_or_else(|| serde::de::Error::custom("missing key for map"))?; match key.split_once(eval_id()) { Some(("__nowSecret_", "")) => Ok(NowStepSecret { secret_name: map.next_value()?, }), Some(_) | None => Err(serde::de::Error::custom("invalid map key")), } } } deserializer.deserialize_struct("NowStepSecret", &["secret_name"], NowStepSecretVisitor) } } impl Serialize for NowStepDownload { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { let mut strct = serializer.serialize_struct("NowStepDownload", 1)?; strct.serialize_field("downloadName", &self.download_name)?; strct.end() } } impl<'de> Deserialize<'de> for NowStepDownload { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { struct NowStepDownloadVisitor; impl<'de> Visitor<'de> for NowStepDownloadVisitor { type Value = NowStepDownload; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("an env download") } fn visit_map(self, mut map: A) -> Result where A: serde::de::MapAccess<'de>, { let key = map .next_key::()? .ok_or_else(|| serde::de::Error::custom("missing key for map"))?; match key.split_once(eval_id()) { Some(("__nowDownload_", "")) => Ok(NowStepDownload { download_name: map.next_value()?, }), Some(_) | None => Err(serde::de::Error::custom("invalid map key")), } } } deserializer.deserialize_struct( "NowStepDownload", &["download_name"], NowStepDownloadVisitor, ) } } pub(crate) mod now_job_timeout { use std::{str::FromStr, time::Duration}; use serde::{Deserializer, Serializer}; pub fn serialize(value: &Option, serializer: S) -> Result where S: Serializer, { match value { Some(value) => serializer.serialize_some( &jiff::Span::try_from(*value) .map_err(serde::ser::Error::custom)? .round(jiff::SpanRound::new().largest(jiff::Unit::Hour)) .map_err(serde::ser::Error::custom)?, ), None => serializer.serialize_none(), } } pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { struct Visitor; impl<'de> serde::de::Visitor<'de> for Visitor { type Value = Option; fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("an optional duration") } fn visit_str(self, v: &str) -> Result where E: serde::de::Error, { Ok(Some( humantime::Duration::from_str(v) .map_err(|error| serde::de::Error::custom(error))? .into(), )) } fn visit_some(self, deserializer: D) -> Result where D: Deserializer<'de>, { deserializer.deserialize_str(self) } fn visit_none(self) -> Result where E: serde::de::Error, { Ok(None) } } deserializer.deserialize_option(Visitor {}) } } #[cfg(test)] mod serde_tests { use std::str::FromStr; use crate::environment::EVAL_ID; use super::*; #[test] fn deserialize_job_single() { EVAL_ID.get_or_init(|| "test".into()); let json = format!( "{{ \"buildSystem\": \"x86_64-linux\", \"checkout\": \"default\", \"env\": {{}}, \"hostSystem\": \"x86_64-linux\", \"name\": \"Fix formatting\", \"needs\": null, \"requiredSystemFeatures\": [], \"sandbox\": null, \"steps\": [ {{ \"__nowUpload_{}\": null, \"env\": {{}}, \"name\": \"format-0\", \"runDrv\": \"/nix/store/7djw1vhncf4953h80pq7xwvddrq0k88i-now-step.drv\", \"teardownDrv\": null }} ], \"strategy\": null, \"timeout\": null }}", eval_id() ); let job: NowJobContainer = serde_json::from_str(&json).unwrap(); assert!(matches!(job, NowJobContainer::Single(_))); } #[test] fn serde_now_job_timeout() { #[derive(Serialize, Deserialize)] struct Wrapper(#[serde(with = "now_job_timeout")] Option); let value: Wrapper = serde_json::from_str("null").unwrap(); assert!(value.0.is_none()); assert_eq!(serde_json::to_string(&value).unwrap(), "null"); let value: Wrapper = serde_json::from_str(r#""1h""#).unwrap(); assert_eq!( value.0.unwrap(), humantime::Duration::from_str("1h").unwrap().into() ); assert_eq!(serde_json::to_string(&value).unwrap(), r#""PT1H""#); } }