diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 34819c5..aff8754 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -3,14 +3,6 @@ # Docs: https://gh.io/actions-lockfile version: 'v0.0.2' workflows: - '.github/workflows/governance.yml': [] - '.github/workflows/hypatia-scan.yml': [] - '.github/workflows/label-triage.yml': [] - '.github/workflows/labels.yml': [] - '.github/workflows/mirror.yml': [] - '.github/workflows/rust-ci.yml': [] - '.github/workflows/scorecard.yml': [] - '.github/workflows/secret-scanner.yml': [] '.github/workflows/boj-build.yml': - 'actions/checkout@v4.1.7' '.github/workflows/cargo-audit.yml': @@ -33,14 +25,19 @@ workflows: - 'dependabot/fetch-metadata@v2.2.0' '.github/workflows/dogfood-gate.yml': - 'actions/checkout@v4.3.1' - - 'hyperpolymath/a2ml-ecosystem@main' + - 'hyperpolymath/deed-ecosystem@main' - 'hyperpolymath/k9-ecosystem@main' '.github/workflows/generator-generic-ossf-slsa3-publish.yml': - 'actions/checkout@v6.0.1' '.github/workflows/ghcr-publish.yml': - 'actions/checkout@v6.0.1' + '.github/workflows/governance.yml': [] + '.github/workflows/hypatia-scan.yml': [] '.github/workflows/instant-sync.yml': - 'peter-evans/repository-dispatch@v3.0.0' + '.github/workflows/label-triage.yml': [] + '.github/workflows/labels.yml': [] + '.github/workflows/mirror.yml': [] '.github/workflows/pages.yml': - 'actions/checkout@v4.4.0' - 'actions/deploy-pages@v4.0.5' @@ -88,6 +85,9 @@ workflows: - 'dtolnay/rust-toolchain@v1' - 'goto-bus-stop/setup-zig@v2.2.1' - 'softprops/action-gh-release@v2.2.1' + '.github/workflows/rust-ci.yml': [] + '.github/workflows/scorecard.yml': [] + '.github/workflows/secret-scanner.yml': [] '.github/workflows/stress-test.yml': - 'actions/checkout@v6.0.1' - 'dtolnay/rust-toolchain@v1' @@ -251,14 +251,14 @@ dependencies: commit: 'sha1-ec49483bfc012387b227434aba94f59a6ecd0900' owner_id: 75048950 repo_id: 623796603 - 'hyperpolymath/a2ml-ecosystem@main': + 'hyperpolymath/deed-ecosystem@main': ref: 'main' - commit: 'sha1-c572785aab3898d9955f9e0370439631265802c9' + commit: 'sha1-f7a40a4d5cc82b2e73f861119baa6818d77a448d' owner_id: 6759885 repo_id: 1275649586 'hyperpolymath/k9-ecosystem@main': ref: 'main' - commit: 'sha1-921f599d0b5bc6de4627338b014059ab6bcea5b8' + commit: 'sha1-2155aa26a21758f2ba119f61bc7e0e1981c106fb' owner_id: 6759885 repo_id: 1275650185 'ksxgithub/github-actions-deploy-aur@v3.0.1': diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index 82864e2..86b0165 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -41,7 +41,7 @@ jobs: - name: Validate A2ML manifests if: steps.detect.outputs.count > 0 - uses: hyperpolymath/a2ml-ecosystem/validate-action@main + uses: hyperpolymath/deed-ecosystem/validate-action@main with: path: '.' strict: 'false' diff --git a/benches/bunsenite_bench.rs b/benches/bunsenite_bench.rs index 0e836b5..183a345 100644 --- a/benches/bunsenite_bench.rs +++ b/benches/bunsenite_bench.rs @@ -7,7 +7,8 @@ //! creation overhead and the `validate`-only fast path. use bunsenite::NickelLoader; -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use std::hint::black_box; // --------------------------------------------------------------------------- // Payload corpus @@ -97,11 +98,9 @@ fn bench_throughput_parse(c: &mut Criterion) { ("large", LARGE_PAYLOAD), ] { group.throughput(Throughput::Bytes(payload.len() as u64)); - group.bench_with_input( - BenchmarkId::new(label, payload.len()), - payload, - |b, src| b.iter(|| loader.parse_string(black_box(src), "bench.ncl")), - ); + group.bench_with_input(BenchmarkId::new(label, payload.len()), payload, |b, src| { + b.iter(|| loader.parse_string(black_box(src), "bench.ncl")) + }); } group.finish(); @@ -124,11 +123,9 @@ fn bench_throughput_validate(c: &mut Criterion) { ("large", LARGE_PAYLOAD), ] { group.throughput(Throughput::Bytes(payload.len() as u64)); - group.bench_with_input( - BenchmarkId::new(label, payload.len()), - payload, - |b, src| b.iter(|| loader.validate(black_box(src), "bench.ncl")), - ); + group.bench_with_input(BenchmarkId::new(label, payload.len()), payload, |b, src| { + b.iter(|| loader.validate(black_box(src), "bench.ncl")) + }); } group.finish(); diff --git a/benches/parser.rs b/benches/parser.rs index 529a0e4..1cb51a1 100644 --- a/benches/parser.rs +++ b/benches/parser.rs @@ -5,7 +5,8 @@ //! Run with: cargo bench use bunsenite::NickelLoader; -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use std::hint::black_box; /// Simple configuration (~100 bytes) const SIMPLE_CONFIG: &str = r#" diff --git a/src/loader.rs b/src/loader.rs index 0e351b9..a962fcf 100644 --- a/src/loader.rs +++ b/src/loader.rs @@ -3,7 +3,7 @@ //! Nickel file loader and parser //! //! This module provides the core functionality for loading and parsing Nickel -//! configuration files using nickel-lang-core 0.9.1. +//! configuration files using nickel-lang-core 0.18.0. //! //! # Core Responsibilities //! @@ -12,18 +12,17 @@ //! 3. **Evaluation**: Executing the Nickel program to produce a final configuration. //! 4. **Export**: Converting the evaluated configuration into standard formats (JSON). //! -//! # API Compatibility Notes (nickel-lang-core 0.9.1) +//! # API Compatibility Notes (nickel-lang-core 0.18.0) //! -//! - `Program::new_from_source()` requires trace parameter: `std::io::sink()` -//! - `eval_full()` takes no arguments (changed in 0.9.1) -//! - Manual error conversion required via `serde_json::to_value()` -//! - NO `into_diagnostics()` method available (deprecated) +//! - In-memory sources are loaded through `ProgramBuilder`. +//! - `eval_full()` takes no arguments. +//! - Evaluated values are converted with `serde_json::to_value()`. use crate::error::{Error, Result}; use nickel_lang_core::eval::cache::lazy::CBNCache; -use nickel_lang_core::program::Program; +use nickel_lang_core::program::{Program, ProgramBuilder}; +use nickel_lang_core::typecheck::TypecheckMode; use serde_json::Value; -use std::io::Cursor; use std::path::Path; /// Type alias for the standard Program with CBN (Call-By-Need) caching. @@ -88,27 +87,18 @@ impl NickelLoader { /// assert!(result.is_ok()); /// ``` pub fn parse_string(&self, source: &str, name: &str) -> Result { - // Create a Program from source - // API in 0.9.1: Program::new_from_source(impl Read, impl Into, impl Write) - let mut program: NickelProgram = Program::new_from_source( - Cursor::new(source.as_bytes()), - name, - std::io::sink(), // Trace output (discarded) - ) - .map_err(|e| { - let msg = format!("{:?}", e); - Error::parse_error(name, msg) - })?; + let mut program: NickelProgram = ProgramBuilder::new() + .add_source_string(source, name) + .build() + .map_err(|e| Error::parse_error(name, format!("{:?}", e)))?; // Evaluate the program - // API change in 0.9.1: eval_full takes no arguments let eval_result = program.eval_full().map_err(|e| { let msg = format!("{:?}", e); Error::evaluation_error(name, msg) })?; // Convert to JSON - // API change in 0.9.1: Manual conversion required, no into_diagnostics() let json_value = serde_json::to_value(&eval_result) .map_err(|e| Error::serialization_error(format!("Failed to convert to JSON: {}", e)))?; @@ -171,13 +161,14 @@ impl NickelLoader { /// assert!(loader.validate("{ foo = }", "bad.ncl").is_err()); /// ``` pub fn validate(&self, source: &str, name: &str) -> Result<()> { - // Just try to create a Program - this performs parsing and type-checking - let _program: NickelProgram = - Program::new_from_source(Cursor::new(source.as_bytes()), name, std::io::sink()) - .map_err(|e| { - let msg = format!("{:?}", e); - Error::parse_error(name, msg) - })?; + let mut program: NickelProgram = ProgramBuilder::new() + .add_source_string(source, name) + .build() + .map_err(|e| Error::parse_error(name, format!("{:?}", e)))?; + + program + .typecheck(TypecheckMode::Walk) + .map_err(|e| Error::parse_error(name, format!("{:?}", e)))?; Ok(()) } diff --git a/tests/aspect_test.rs b/tests/aspect_test.rs index b2a0208..bd8328f 100644 --- a/tests/aspect_test.rs +++ b/tests/aspect_test.rs @@ -104,9 +104,7 @@ fn aspect_moderately_deep_nesting_no_panic() { /// important guarantee is no crash. #[test] fn aspect_large_flat_record_no_panic() { - let fields: String = (0..1000) - .map(|i| format!(" field_{i} = {i},\n")) - .collect(); + let fields: String = (0..1000).map(|i| format!(" field_{i} = {i},\n")).collect(); let source = format!("{{\n{fields}}}"); let result = loader().parse_string(&source, "large.ncl"); // Must not panic. Nickel may or may not handle 1000 fields; we only @@ -202,7 +200,11 @@ fn aspect_internal_error_not_recoverable() { fn aspect_error_message_accessor_round_trips() { let msg = "custom diagnostic text"; let err = Error::parse_error("test.ncl", msg); - assert_eq!(err.message(), msg, "message() should return the original message verbatim"); + assert_eq!( + err.message(), + msg, + "message() should return the original message verbatim" + ); } /// A `SerializationError` must be non-recoverable (it indicates an internal diff --git a/tests/e2e_test.rs b/tests/e2e_test.rs index 2fde617..d7f165f 100644 --- a/tests/e2e_test.rs +++ b/tests/e2e_test.rs @@ -134,7 +134,9 @@ fn e2e_array_round_trip() { assert_eq!(tags[1], "nickel"); assert_eq!(tags[2], "config"); - let counts = json["counts"].as_array().expect("counts should be an array"); + let counts = json["counts"] + .as_array() + .expect("counts should be an array"); assert_eq!(counts.len(), 3); } @@ -249,8 +251,7 @@ fn e2e_validate_then_parse_consistent() { #[test] fn e2e_key_count_matches_source() { // Source has exactly 5 top-level fields. - let source = - r#"{ a = 1, b = 2, c = 3, d = 4, e = 5 }"#; + let source = r#"{ a = 1, b = 2, c = 3, d = 4, e = 5 }"#; let json = loader() .parse_string(source, "keycount.ncl") .expect("E2E: key-count config must parse"); diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 1075402..284a325 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -6,7 +6,7 @@ //! that configuration parsing, error handling, and metadata constants //! behave correctly from an external consumer's perspective. -use bunsenite::{NickelLoader, Error, NAME, RSR_TIER, TPCF_PERIMETER, VERSION}; +use bunsenite::{Error, NickelLoader, NAME, RSR_TIER, TPCF_PERIMETER, VERSION}; /// Verify that NickelLoader can be constructed with default settings. #[test] @@ -31,7 +31,11 @@ fn test_parse_simple_record() { let loader = NickelLoader::new(); let input = r#"{ name = "bunsenite", version = "1.0.0" }"#; let result = loader.parse_string(input, "simple.ncl"); - assert!(result.is_ok(), "Simple record should parse: {:?}", result.err()); + assert!( + result.is_ok(), + "Simple record should parse: {:?}", + result.err() + ); let json = result.unwrap(); assert!(json.to_string().contains("bunsenite")); assert!(json.to_string().contains("1.0.0")); @@ -43,7 +47,11 @@ fn test_parse_numeric_values() { let loader = NickelLoader::new(); let input = r#"{ port = 8080, retries = 3 }"#; let result = loader.parse_string(input, "numeric.ncl"); - assert!(result.is_ok(), "Numeric record should parse: {:?}", result.err()); + assert!( + result.is_ok(), + "Numeric record should parse: {:?}", + result.err() + ); let json = result.unwrap(); let text = json.to_string(); assert!(text.contains("8080"), "Should contain port value"); @@ -56,7 +64,11 @@ fn test_parse_boolean_values() { let loader = NickelLoader::new(); let input = r#"{ enabled = true, debug = false }"#; let result = loader.parse_string(input, "bool.ncl"); - assert!(result.is_ok(), "Boolean record should parse: {:?}", result.err()); + assert!( + result.is_ok(), + "Boolean record should parse: {:?}", + result.err() + ); } /// Verify that empty records parse without error. @@ -65,7 +77,11 @@ fn test_parse_empty_record() { let loader = NickelLoader::new(); let input = "{}"; let result = loader.parse_string(input, "empty.ncl"); - assert!(result.is_ok(), "Empty record should parse: {:?}", result.err()); + assert!( + result.is_ok(), + "Empty record should parse: {:?}", + result.err() + ); } /// Verify that invalid Nickel syntax produces an error, not a panic. @@ -84,7 +100,10 @@ fn test_error_recoverability() { assert!(parse_err.is_recoverable(), "Parse errors are recoverable"); let internal_err = Error::internal("unexpected state"); - assert!(!internal_err.is_recoverable(), "Internal errors are not recoverable"); + assert!( + !internal_err.is_recoverable(), + "Internal errors are not recoverable" + ); } /// Verify crate metadata constants are correctly populated. diff --git a/tests/property_test.rs b/tests/property_test.rs index c1dfd73..4fc99f6 100644 --- a/tests/property_test.rs +++ b/tests/property_test.rs @@ -34,21 +34,18 @@ const CORPUS: &[(&str, &str)] = &[ // 0 — single string field ("single_string", r#"{ greeting = "hello" }"#), // 1 — three scalar types - ("multi_scalar", r#"{ name = "cfg", count = 42, active = true }"#), - // 2 — nested record (one level) ( - "nested_one", - r#"{ outer = { inner = "value", num = 7 } }"#, + "multi_scalar", + r#"{ name = "cfg", count = 42, active = true }"#, ), + // 2 — nested record (one level) + ("nested_one", r#"{ outer = { inner = "value", num = 7 } }"#), // 3 — array of strings ("array_strings", r#"{ tags = ["a", "b", "c", "d"] }"#), // 4 — array of integers ("array_ints", r#"{ nums = [10, 20, 30] }"#), // 5 — three-level nesting - ( - "deep_nest", - r#"{ l1 = { l2 = { l3 = "leaf" } } }"#, - ), + ("deep_nest", r#"{ l1 = { l2 = { l3 = "leaf" } } }"#), // 6 — computed arithmetic ("computed", r#"{ x = 3 * 4, y = 100 - 1 }"#), // 7 — string concatenation @@ -279,8 +276,5 @@ fn property_deep_nesting_preserved() { assert!(json["l1"].is_object(), "l1 should be an object"); assert!(json["l1"]["l2"].is_object(), "l1.l2 should be an object"); - assert_eq!( - json["l1"]["l2"]["l3"], "leaf", - "l1.l2.l3 should be 'leaf'" - ); + assert_eq!(json["l1"]["l2"]["l3"], "leaf", "l1.l2.l3 should be 'leaf'"); }