// 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