Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Rust

on:
push:
branches: [main]
paths:
- 'rust/**'
- 'conformance/**'
- 'examples/**'
- '.github/workflows/rust.yml'
pull_request:
paths:
- 'rust/**'
- 'conformance/**'
- 'examples/**'
- '.github/workflows/rust.yml'
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy

# The pure functional core: fmt + clippy (-D warnings) + the conformance
# harness (var-doc golden gate over 15 bundles + drift/hash units).
- run: cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test
working-directory: rust

# Standalone sample project (see examples/rust-cargotest): runs the
# Markdown specs through var-core via `cargo test`.
- run: cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test
working-directory: examples/rust-cargotest
13 changes: 10 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Build and test every language port from the repo root.
#
# make # same as `make check`: all four ports
# make # same as `make check`: every port
# make typescript # pnpm build + pnpm check (lint, typecheck, test, knip, jscpd)
# make python # pytest + ruff + no-reexports gate + examples/python-pytest
# make java # spotless:apply (formats Java + Kotlin, incl. the JVM sample
Expand All @@ -10,13 +10,14 @@
# make ruby # bundle + rake (rubocop + rspec + purity gate) +
# # examples/ruby-rspec and examples/ruby-minitest (Ruby 3.2,
# # pinned in ruby/.tool-versions)
# make rust # cargo fmt/clippy/test (var-core) + examples/rust-cargotest
# make coverage # test with coverage in all four ports (reports below)
#
# Each target runs the same gate as that port's CI workflow in .github/workflows/.

.PHONY: check commits typescript python java ruby coverage changelog prepare release
.PHONY: check commits typescript python java ruby rust coverage changelog prepare release

check: commits typescript python java ruby
check: commits typescript python java ruby rust

# Commits since the last release tag must be conventional (they drive the
# changelog and the version bump — see cliff.toml and CLAUDE.md).
Expand Down Expand Up @@ -48,6 +49,12 @@ ruby:
cd examples/ruby-rspec && bundle install && bundle exec rspec
cd examples/ruby-minitest && bundle install && bundle exec rake test

# Rust port: pure cargo (var-core), then the standalone sample project (which
# depends on var-core by path and runs the Markdown specs via `cargo test`).
rust:
cd rust && cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test
cd examples/rust-cargotest && cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test

# Coverage reports: typescript/coverage/index.html, python/htmlcov/index.html,
# java/<module>/target/site/jacoco/index.html (jacoco runs on every verify),
# ruby/coverage/index.html. lcov files (typescript/coverage/lcov.info,
Expand Down
70 changes: 70 additions & 0 deletions conformance/bundles/01-roman-numerals/numerals.steps.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//! Rust sibling of `numerals.steps.ts` (bundle `01-roman-numerals`).
//!
//! Full-replacement state (ADR 0006): the `{result}` map is the whole state.

use std::collections::BTreeMap;
use var::{Handler, HandlerError, Registry, Steps, Value};

pub const FILE: &str = "numerals.steps.rs";

fn roman(n: i64) -> Option<&'static str> {
match n {
1 => Some("I"),
4 => Some("IV"),
9 => Some("IX"),
40 => Some("XL"),
_ => None,
}
}

pub fn register(r: Registry) -> Registry {
let mut s = Steps::from_registry(r);
s.stimulus(
"I convert {int} to roman numerals",
FILE,
1,
Handler::sync1(|_state, n| {
let n = if let Value::Int(i) = n { i } else { 0 };
let mut m = BTreeMap::new();
if let Some(s) = roman(n) {
m.insert("result".to_string(), Value::from(s));
}
Ok(Some(Value::Map(m)))
}),
);
s.sensor(
"The result is {word}",
FILE,
2,
Handler::sync1(|state, expected| {
// {word} greedily captures trailing punctuation ("I." not "I"); strip
// it, then throw on mismatch rather than returning (which would make
// the core compare the RAW captured "I." and wrongly fail). Returning
// None opts out, matching the .ts/.java sensors.
let expected = if let Value::String(s) = expected {
s
} else {
String::new()
};
let cleaned = expected.trim_end_matches(['.', '!', '?']);
let result = match &state {
Value::Map(m) => match m.get("result") {
Some(Value::String(s)) => s.clone(),
_ => String::new(),
},
_ => String::new(),
};
if cleaned != result {
return Err(HandlerError::new(format!(
"expected {cleaned} but got {result}"
)));
}
Ok(None)
}),
);
s.into_registry()
}

pub fn state() -> Value {
Value::Map(BTreeMap::new())
}
52 changes: 52 additions & 0 deletions conformance/bundles/02-context-isolation/counter.steps.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//! Rust sibling of `counter.steps.ts` (bundle `02-context-isolation`).

use std::collections::BTreeMap;
use var::{Handler, HandlerError, Registry, Steps, Value};

pub const FILE: &str = "counter.steps.rs";

fn count_of(state: &Value) -> i64 {
match state {
Value::Map(m) => match m.get("count") {
Some(Value::Int(i)) => *i,
_ => 0,
},
_ => 0,
}
}

pub fn register(r: Registry) -> Registry {
let mut s = Steps::from_registry(r);
s.stimulus(
"I increment",
FILE,
1,
Handler::sync0(|state| {
let next = count_of(&state) + 1;
Ok(Some(Value::Map(BTreeMap::from([(
"count".to_string(),
Value::Int(next),
)]))))
}),
);
s.sensor(
"The count is {int}",
FILE,
2,
Handler::sync1(|state, n| {
let count = count_of(&state);
let expected = if let Value::Int(i) = n { i } else { 0 };
if count != expected {
return Err(HandlerError::new(format!(
"expected {expected} but got {count}"
)));
}
Ok(None)
}),
);
s.into_registry()
}

pub fn state() -> Value {
Value::Map(BTreeMap::from([("count".to_string(), Value::Int(0))]))
}
26 changes: 26 additions & 0 deletions conformance/bundles/03-expected-failure/division.steps.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//! Rust sibling of `division.steps.ts` (bundle `03-expected-failure`).

use var::{Handler, HandlerError, Registry, Steps, Value};

pub const FILE: &str = "division.steps.rs";

pub fn register(r: Registry) -> Registry {
let mut s = Steps::from_registry(r);
s.stimulus(
"I divide {int} by {int}",
FILE,
1,
Handler::sync2(|state, _a, b| {
let b = if let Value::Int(i) = b { i } else { 0 };
if b == 0 {
return Err(HandlerError::new("division by zero"));
}
Ok(Some(state))
}),
);
s.into_registry()
}

pub fn state() -> Value {
Value::Null
}
22 changes: 22 additions & 0 deletions conformance/bundles/04-tables-and-docstrings/echo.steps.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//! Rust sibling of `echo.steps.ts` (bundle `04-tables-and-docstrings`).

use var::{Handler, Registry, Steps, Value};

pub const FILE: &str = "echo.steps.rs";

pub fn register(r: Registry) -> Registry {
let mut s = Steps::from_registry(r);
// The doc string is this sensor's only slot, so it is returned bare; the
// core compares it against the input (compareDocString); equal passes.
s.sensor(
"I echo the following:",
FILE,
1,
Handler::sync1(|_state, doc| Ok(Some(doc))),
);
s.into_registry()
}

pub fn state() -> Value {
Value::Null
}
22 changes: 22 additions & 0 deletions conformance/bundles/05-ambiguous-match/cukes.steps.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//! Rust sibling of `cukes.steps.ts` (bundle `05-ambiguous-match`).

use var::{Handler, Registry, Steps, Value};

pub const FILE: &str = "cukes.steps.rs";

pub fn register(r: Registry) -> Registry {
let mut s = Steps::from_registry(r);
// Both expressions match "I have 5 cukes" → ambiguous-match diagnostic.
s.stimulus(
"I have {int} cukes",
FILE,
1,
Handler::sync1(|_state, _n| Ok(None)),
);
s.stimulus("I have 5 cukes", FILE, 2, Handler::sync0(|_state| Ok(None)));
s.into_registry()
}

pub fn state() -> Value {
Value::Null
}
22 changes: 22 additions & 0 deletions conformance/bundles/06-doc-string-mismatch/echo.steps.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//! Rust sibling of `echo.steps.ts` (bundle `06-doc-string-mismatch`).

use var::{Handler, Registry, Steps, Value};

pub const FILE: &str = "echo.steps.rs";

pub fn register(r: Registry) -> Registry {
let mut s = Steps::from_registry(r);
// Returns the WRONG string (bare — the doc string is the only slot); the
// core compares it to the doc string and throws DocStringMismatchError.
s.sensor(
"I echo the following:",
FILE,
1,
Handler::sync1(|_state, _doc| Ok(Some(Value::from("goodbye")))),
);
s.into_registry()
}

pub fn state() -> Value {
Value::Null
}
28 changes: 28 additions & 0 deletions conformance/bundles/07-row-check-mismatch/report.steps.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//! Rust sibling of `report.steps.ts` (bundle `07-row-check-mismatch`).

use std::collections::BTreeMap;
use var::{Handler, Registry, Steps, Value};

pub const FILE: &str = "report.steps.rs";

pub fn register(r: Registry) -> Registry {
let mut s = Steps::from_registry(r);
// Header-bound row step: returns its computed columns; the core diffs them
// against the row cells (rowChecks). score 99 ≠ 10 → CellMismatchError.
s.sensor(
"I report the score and grade",
FILE,
1,
Handler::sync1(|_state, _row| {
Ok(Some(Value::Map(BTreeMap::from([
("score".to_string(), Value::from("99")),
("grade".to_string(), Value::from("A")),
]))))
}),
);
s.into_registry()
}

pub fn state() -> Value {
Value::Null
}
20 changes: 20 additions & 0 deletions conformance/bundles/08-string-capture/greet.steps.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//! Rust sibling of `greet.steps.ts` (bundle `08-string-capture`).

use var::{Handler, Registry, Steps, Value};

pub const FILE: &str = "greet.steps.rs";

pub fn register(r: Registry) -> Registry {
let mut s = Steps::from_registry(r);
s.stimulus(
"I greet {string}",
FILE,
1,
Handler::sync1(|_state, _name| Ok(None)),
);
s.into_registry()
}

pub fn state() -> Value {
Value::Null
}
22 changes: 22 additions & 0 deletions conformance/bundles/09-expected-message-mismatch/boom.steps.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//! Rust sibling of `boom.steps.ts` (bundle `09-expected-message-mismatch`).

use var::{Handler, HandlerError, Registry, Steps, Value};

pub const FILE: &str = "boom.steps.rs";

pub fn register(r: Registry) -> Registry {
let mut s = Steps::from_registry(r);
// Throws a message that does NOT contain the expected substring "expected
// message", so the expected-failure is NOT satisfied → the example fails.
s.stimulus(
"I always boom",
FILE,
1,
Handler::sync0(|_state| Err(HandlerError::new("actual different error"))),
);
s.into_registry()
}

pub fn state() -> Value {
Value::Null
}
23 changes: 23 additions & 0 deletions conformance/bundles/10-error-fence-without-step/cukes.steps.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//! Rust sibling of `cukes.steps.ts` (bundle `10-error-fence-without-step`).

use var::{Handler, Registry, Steps, Value};

pub const FILE: &str = "cukes.steps.rs";

pub fn register(r: Registry) -> Registry {
let mut s = Steps::from_registry(r);
// The prose matches no step, so the `error` fence has nothing to run →
// error-fence-without-step diagnostic, and the example is dropped. This
// step exists only so the registry matches the other ports'.
s.stimulus(
"I have {int} cukes",
FILE,
1,
Handler::sync1(|_state, _n| Ok(None)),
);
s.into_registry()
}

pub fn state() -> Value {
Value::Null
}
Loading