Skip to content

Commit 86e9a6d

Browse files
Port eclexiaiser validator from Python to Rust (clears banned-language governance check) (#30)
The `governance / package anti-pattern policy` check fails because org policy fully bans Python and the repo shipped `.github/scripts/validate_eclexiaiser.py`. Rather than add an exemption pragma, this **ports** the validator to a dependency-free Rust program. - **No new toolchain / Cargo project / crates** — builds with the `rustc` preinstalled on `ubuntu-latest`. `dogfood-gate.yml` now does `rustc -O … && ./validate` instead of `python3 …`. - **Identical semantics** to the `tomllib` version: non-empty `project.name`, ≥1 `[[functions]]`, non-empty `name`+`source` per function; same messages; same "Valid: NAME (N function(s))" output. - **Verified** locally (Debian, `rustc -D warnings`) against the real `eclexiaiser.toml` plus fixtures covering inline comments, single-quoted values with `#`, and the missing-name / no-functions / missing-source failure paths. Removes the last banned-language file, so `git ls-files '*.py'` is empty and the policy passes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d929239 commit 86e9a6d

3 files changed

Lines changed: 141 additions & 32 deletions

File tree

.github/scripts/validate_eclexiaiser.py

Lines changed: 0 additions & 30 deletions
This file was deleted.
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
// Owner: Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
4+
//
5+
// Validates eclexiaiser.toml structure (called by dogfood-gate.yml).
6+
//
7+
// Ported from validate_eclexiaiser.py — Python is banned by governance policy.
8+
// Dependency-free (no crates) so it builds with the preinstalled `rustc` on
9+
// ubuntu-latest: `rustc -O validate_eclexiaiser.rs -o validate_eclexiaiser`.
10+
// The checks are deliberately shallow, matching the original tomllib validator.
11+
12+
use std::process::exit;
13+
14+
/// Return the slice of `s` before an unquoted `#` (TOML inline comment).
15+
fn strip_inline_comment(s: &str) -> &str {
16+
let bytes = s.as_bytes();
17+
let mut quote: Option<u8> = None;
18+
let mut i = 0;
19+
while i < bytes.len() {
20+
let c = bytes[i];
21+
match quote {
22+
Some(q) => {
23+
if c == q {
24+
quote = None;
25+
}
26+
}
27+
None => match c {
28+
b'"' | b'\'' => quote = Some(c),
29+
b'#' => return &s[..i],
30+
_ => {}
31+
},
32+
}
33+
i += 1;
34+
}
35+
s
36+
}
37+
38+
/// Strip one layer of matching single or double quotes, if present.
39+
fn unquote(s: &str) -> String {
40+
let t = s.trim();
41+
let b = t.as_bytes();
42+
if b.len() >= 2 && (b[0] == b'"' || b[0] == b'\'') && b[b.len() - 1] == b[0] {
43+
t[1..t.len() - 1].to_string()
44+
} else {
45+
t.to_string()
46+
}
47+
}
48+
49+
#[derive(PartialEq)]
50+
enum Section {
51+
Other,
52+
Project,
53+
Function,
54+
}
55+
56+
fn main() {
57+
const PATH: &str = "eclexiaiser.toml";
58+
let content = match std::fs::read_to_string(PATH) {
59+
Ok(c) => c,
60+
Err(e) => {
61+
eprintln!("ERROR: cannot read {PATH}: {e}");
62+
exit(1);
63+
}
64+
};
65+
66+
let mut project_name = String::new();
67+
// Each [[functions]] entry collects its own (name, source).
68+
let mut functions: Vec<(String, String)> = Vec::new();
69+
let mut section = Section::Other;
70+
71+
for raw in content.lines() {
72+
let line = strip_inline_comment(raw).trim();
73+
if line.is_empty() {
74+
continue;
75+
}
76+
77+
if let Some(inner) = line.strip_prefix("[[").and_then(|l| l.strip_suffix("]]")) {
78+
if inner.trim() == "functions" {
79+
functions.push((String::new(), String::new()));
80+
section = Section::Function;
81+
} else {
82+
section = Section::Other;
83+
}
84+
continue;
85+
}
86+
if let Some(inner) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) {
87+
section = if inner.trim() == "project" {
88+
Section::Project
89+
} else {
90+
Section::Other
91+
};
92+
continue;
93+
}
94+
95+
let Some(eq) = line.find('=') else { continue };
96+
let key = line[..eq].trim();
97+
let value = unquote(&line[eq + 1..]);
98+
match section {
99+
Section::Project if key == "name" => project_name = value,
100+
Section::Function => {
101+
if let Some(f) = functions.last_mut() {
102+
match key {
103+
"name" => f.0 = value,
104+
"source" => f.1 = value,
105+
_ => {}
106+
}
107+
}
108+
}
109+
_ => {}
110+
}
111+
}
112+
113+
if project_name.trim().is_empty() {
114+
eprintln!("ERROR: project.name is required");
115+
exit(1);
116+
}
117+
if functions.is_empty() {
118+
eprintln!("ERROR: at least one [[functions]] entry is required");
119+
exit(1);
120+
}
121+
for (name, source) in &functions {
122+
if name.trim().is_empty() {
123+
eprintln!("ERROR: function name cannot be empty");
124+
exit(1);
125+
}
126+
if source.trim().is_empty() {
127+
eprintln!("ERROR: function {name} has no source path");
128+
exit(1);
129+
}
130+
}
131+
132+
println!(
133+
"Valid: {} ({} function(s))",
134+
project_name.trim(),
135+
functions.len()
136+
);
137+
}

.github/workflows/dogfood-gate.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,10 @@ jobs:
261261
262262
echo "has_manifest=true" >> "$GITHUB_OUTPUT"
263263
264-
# Validate TOML structure using Python 3.11+ tomllib
265-
python3 .github/scripts/validate_eclexiaiser.py || {
264+
# Validate TOML structure with a dependency-free Rust validator.
265+
# (Python is banned by governance policy; rustc is preinstalled on ubuntu-latest.)
266+
rustc -O .github/scripts/validate_eclexiaiser.rs -o "$RUNNER_TEMP/validate_eclexiaiser"
267+
"$RUNNER_TEMP/validate_eclexiaiser" || {
266268
echo "::error file=eclexiaiser.toml::Invalid eclexiaiser.toml — see step output for details"
267269
exit 1
268270
}

0 commit comments

Comments
 (0)