From 07ea4dd585806132e381a7ca8e5afc67c5c51073 Mon Sep 17 00:00:00 2001 From: Leynos Date: Tue, 5 Aug 2025 13:12:16 +0100 Subject: [PATCH 1/2] Add Recipe serialiser Support manifest serialisation by implementing Serialize for Recipe and covering all variants with tests. --- src/ast.rs | 22 +++++++++++++++++++++- tests/ast_tests.rs | 18 +++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index cf4a49ea..634684e0 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -18,7 +18,11 @@ //! ``` use semver::Version; -use serde::{Deserialize, Serialize, de::Deserializer}; +use serde::{ + Deserialize, Serialize, + de::Deserializer, + ser::{SerializeMap, Serializer}, +}; fn deserialize_actions<'de, D>(deserializer: D) -> Result, D::Error> where @@ -158,6 +162,22 @@ impl<'de> Deserialize<'de> for Recipe { } } } +// Serialise into a single-key map so `#[serde(flatten)]` on parent +// structs inserts the appropriate recipe field directly. +impl Serialize for Recipe { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(1))?; + match self { + Self::Command { command } => map.serialize_entry("command", command)?, + Self::Script { script } => map.serialize_entry("script", script)?, + Self::Rule { rule } => map.serialize_entry("rule", rule)?, + } + map.end() + } +} /// A single build target. /// diff --git a/tests/ast_tests.rs b/tests/ast_tests.rs index a5818d0a..8ad1cb29 100644 --- a/tests/ast_tests.rs +++ b/tests/ast_tests.rs @@ -1,8 +1,9 @@ -//! Unit tests for Netsuke manifest AST deserialisation. +//! Unit tests for Netsuke manifest AST serialisation and deserialisation. use netsuke::{ast::*, manifest}; use rstest::rstest; use semver::Version; +use serde_yml::Value; /// Convenience wrapper around the library manifest parser for tests. fn parse_manifest(yaml: &str) -> anyhow::Result { @@ -362,3 +363,18 @@ fn invalid_manifests_fail(#[case] file: &str) { let path = format!("tests/data/{file}"); assert!(manifest::from_path(&path).is_err()); } + +#[rstest] +#[case(Recipe::Command { command: "echo".into() }, "command: echo")] +#[case(Recipe::Script { script: "run".into() }, "script: run")] +#[case( + Recipe::Rule { + rule: StringOrList::List(vec!["a".into(), "b".into()]), + }, + concat!("rule:\n", " - a\n", " - b"), +)] +fn serialize_recipe_variants(#[case] recipe: Recipe, #[case] expected_yaml: &str) { + let actual: Value = serde_yml::to_value(&recipe).expect("serialize"); + let expected: Value = serde_yml::from_str(expected_yaml).expect("yaml"); + assert_eq!(actual, expected); +} From 575a36cf08353bd9c3898689ba93679a195c88ec Mon Sep 17 00:00:00 2001 From: Leynos Date: Tue, 5 Aug 2025 19:04:08 +0100 Subject: [PATCH 2/2] Derive Recipe serialisation via untagged enum --- src/ast.rs | 25 +++---------------------- src/ir.rs | 8 ++++---- 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index 634684e0..36232e76 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -18,11 +18,7 @@ //! ``` use semver::Version; -use serde::{ - Deserialize, Serialize, - de::Deserializer, - ser::{SerializeMap, Serializer}, -}; +use serde::{Deserialize, Serialize, de::Deserializer}; fn deserialize_actions<'de, D>(deserializer: D) -> Result, D::Error> where @@ -110,7 +106,8 @@ pub struct Rule { /// Exactly one variant must be provided for a rule or target. The fields are /// flattened in the manifest, so the presence of `command`, `script`, or `rule` /// determines the variant. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize)] +#[serde(untagged)] pub enum Recipe { /// A single shell command. Command { command: String }, @@ -162,22 +159,6 @@ impl<'de> Deserialize<'de> for Recipe { } } } -// Serialise into a single-key map so `#[serde(flatten)]` on parent -// structs inserts the appropriate recipe field directly. -impl Serialize for Recipe { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut map = serializer.serialize_map(Some(1))?; - match self { - Self::Command { command } => map.serialize_entry("command", command)?, - Self::Script { script } => map.serialize_entry("script", script)?, - Self::Rule { rule } => map.serialize_entry("rule", rule)?, - } - map.end() - } -} /// A single build target. /// diff --git a/src/ir.rs b/src/ir.rs index 296a2ef4..deb4eefd 100644 --- a/src/ir.rs +++ b/src/ir.rs @@ -341,10 +341,10 @@ fn find_cycle(targets: &HashMap) -> Option> { states: &mut HashMap, ) -> Option> { for dep in deps { - if targets.contains_key(dep) - && let Some(cycle) = visit(targets, dep, stack, states) - { - return Some(cycle); + if targets.contains_key(dep) { + if let Some(cycle) = visit(targets, dep, stack, states) { + return Some(cycle); + } } } None