From 57fbe38eb0fcb5e47a47e0f6bc86b7b021578059 Mon Sep 17 00:00:00 2001 From: Leynos Date: Wed, 23 Jul 2025 05:17:45 +0100 Subject: [PATCH] Add semver validation for manifest version --- .../behavioural-testing-in-rust-with-cucumber.md | 2 +- docs/netsuke-design.md | 15 ++++++++------- docs/roadmap.md | 4 ++-- examples/basic_c.yml | 2 +- examples/photo_edit.yml | 2 +- examples/visual_design.yml | 2 +- examples/website.yml | 2 +- examples/writing.yml | 2 +- tests/ast_tests.rs | 6 ++++++ tests/data/invalid_version.yml | 6 ++++++ tests/features/manifest.feature | 5 +++++ tests/steps/manifest_steps.rs | 16 ++++++++++++++++ 12 files changed, 49 insertions(+), 15 deletions(-) create mode 100644 tests/data/invalid_version.yml diff --git a/docs/behavioural-testing-in-rust-with-cucumber.md b/docs/behavioural-testing-in-rust-with-cucumber.md index 27a5b888..0ae173ac 100644 --- a/docs/behavioural-testing-in-rust-with-cucumber.md +++ b/docs/behavioural-testing-in-rust-with-cucumber.md @@ -1166,7 +1166,7 @@ aligned with what is needed. [^31]: Cucumber in cucumber – Rust – [Docs.rs](http://Docs.rs) — accessed on 14 July 2025 — - + [^32]: CLI (command-line interface) - Cucumber Rust Book, accessed on 14 July 2025, diff --git a/docs/netsuke-design.md b/docs/netsuke-design.md index 863d1628..17fa206e 100644 --- a/docs/netsuke-design.md +++ b/docs/netsuke-design.md @@ -403,7 +403,7 @@ deserialization and easy debugging. Rust -````rust +```rust // In src/ast.rs use serde::Deserialize; @@ -551,7 +551,7 @@ targets: - name: my_app sources: "{{ glob('src/*.c') }}" rule: compile -```` +``` The value of `sources`, `{{ glob('src/*.c') }}`, is not a valid YAML string from the perspective of a strict parser. Attempting to deserialize this @@ -587,9 +587,10 @@ interference, ensuring a robust and predictable ingestion pipeline. The AST structures are implemented in `src/ast.rs` and derive `Deserialize`. Unknown fields are rejected to surface user errors early. `StringOrList` provides a default `Empty` variant, so optional lists are trivial to represent. -The manifest version is parsed using the `semver` crate to validate that it -follows semantic versioning rules. Global and target variable maps now share -the `HashMap` type for consistency. This keeps YAML manifests +The manifest version is parsed into a `semver::Version`. Using the library's +`Deserialize` implementation ensures any manifest with an invalid SemVer string +fails to load. Global and target variable maps now share the +`HashMap` type for consistency. This keeps YAML manifests concise while ensuring forward compatibility. ### 3.5 Testing @@ -1000,14 +1001,14 @@ structures to the Ninja file syntax. Code snippet - ````ninja + ```ninja # Generated from an ir::Action rule cc command = gcc -c -o $out $in description = CC $out depfile = $out.d deps = gcc ```ninja - ```` + ``` 3. **Write Build Edges:** Iterate through the `graph.targets` map. For each `ir::BuildEdge`, write a corresponding Ninja `build` statement. This diff --git a/docs/roadmap.md b/docs/roadmap.md index bdb1c2c8..3eef0a8c 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -24,8 +24,8 @@ compilation pipeline from parsing to execution. #[serde(deny_unknown_fields)] to enable serde_yml parsing. *(done)* - - [ ] Implement parsing for the netsuke_version field and validate it using - the semver crate. + - [x] Implement parsing for the netsuke_version field and validate it using + the semver crate. *(done)* - [ ] Support `phony` and `always` boolean flags on targets. diff --git a/examples/basic_c.yml b/examples/basic_c.yml index eb26dce1..1f769831 100644 --- a/examples/basic_c.yml +++ b/examples/basic_c.yml @@ -1,4 +1,4 @@ -netsuke_version: "1.0" +netsuke_version: "1.0.0" vars: cc: "{{ env('CC') | default('gcc') }}" diff --git a/examples/photo_edit.yml b/examples/photo_edit.yml index ceaaae6b..eae905e8 100644 --- a/examples/photo_edit.yml +++ b/examples/photo_edit.yml @@ -1,4 +1,4 @@ -netsuke_version: "1.0" +netsuke_version: "1.0.0" vars: raw_dir: raw_photos diff --git a/examples/visual_design.yml b/examples/visual_design.yml index 0f2c3d0a..a4fbe0a7 100644 --- a/examples/visual_design.yml +++ b/examples/visual_design.yml @@ -1,4 +1,4 @@ -netsuke_version: "1.0" +netsuke_version: "1.0.0" vars: src_dir: design/svg diff --git a/examples/website.yml b/examples/website.yml index c883c28e..9cea51c5 100644 --- a/examples/website.yml +++ b/examples/website.yml @@ -1,4 +1,4 @@ -netsuke_version: "1.0" +netsuke_version: "1.0.0" vars: pages_dir: pages diff --git a/examples/writing.yml b/examples/writing.yml index 97ae2a57..d5e85b41 100644 --- a/examples/writing.yml +++ b/examples/writing.yml @@ -1,4 +1,4 @@ -netsuke_version: "1.0" +netsuke_version: "1.0.0" vars: chapters_dir: chapters diff --git a/tests/ast_tests.rs b/tests/ast_tests.rs index 45d5e19d..9725a799 100644 --- a/tests/ast_tests.rs +++ b/tests/ast_tests.rs @@ -213,3 +213,9 @@ fn invalid_enum_variants() { "#; assert!(serde_yml::from_str::(yaml).is_err()); } + +#[test] +fn invalid_manifest_version() { + let yaml = "netsuke_version: '1.0'"; + assert!(serde_yml::from_str::(yaml).is_err()); +} diff --git a/tests/data/invalid_version.yml b/tests/data/invalid_version.yml new file mode 100644 index 00000000..a2e8dc58 --- /dev/null +++ b/tests/data/invalid_version.yml @@ -0,0 +1,6 @@ +netsuke_version: "1.0" +targets: + - name: invalid + recipe: + kind: command + command: "echo invalid" diff --git a/tests/features/manifest.feature b/tests/features/manifest.feature index 431fae80..9b5c23fa 100644 --- a/tests/features/manifest.feature +++ b/tests/features/manifest.feature @@ -4,3 +4,8 @@ Feature: Manifest parsing When the manifest file "tests/data/minimal.yml" is parsed Then the manifest version is "1.0.0" And the first target name is "hello" + + Scenario: Invalid manifest version + When the manifest file "tests/data/invalid_version.yml" is parsed + Then manifest parsing should fail + And the manifest error message should contain "version" diff --git a/tests/steps/manifest_steps.rs b/tests/steps/manifest_steps.rs index eaf692e7..04465841 100644 --- a/tests/steps/manifest_steps.rs +++ b/tests/steps/manifest_steps.rs @@ -54,3 +54,19 @@ fn first_target_name(world: &mut CliWorld, name: String) { other => panic!("Expected StringOrList::String, got: {other:?}"), } } + +#[then("manifest parsing should fail")] +fn manifest_parsing_should_fail(world: &mut CliWorld) { + assert!(world.manifest.is_none(), "expected parsing to fail"); + assert!(world.manifest_error.is_some(), "error message missing"); +} + +#[expect( + clippy::needless_pass_by_value, + reason = "Cucumber requires owned String arguments" +)] +#[then(expr = "the manifest error message should contain {string}")] +fn manifest_error_contains(world: &mut CliWorld, text: String) { + let err = world.manifest_error.as_ref().expect("error"); + assert!(err.contains(&text), "{err} does not contain {text}"); +}