Inspiration

I have always liked Ruby for the same reason many people do: it lets us express business logic in a very human way. A validation contract can stay readable, flexible, and close to the domain instead of turning into a wall of infrastructure code.

Unfortunately, the parts around that business logic—walking keys, coercing values, traversing nested structures, and repeatedly evaluating structural predicates—can become expensive when the workload grows.

Once I started experimenting with Rust-backed Ruby gems, the obvious dangerous thought appeared: what else can be moved to Rust? 😄

But a full rewrite of dry-validation would miss the point. Ruby rule blocks, macros, injected objects, context, and application-specific logic are valuable precisely because they are Ruby.

That led to the principle behind this project:

Keep Ruby where Ruby is expressive. Use Rust where Ruby becomes expensive.

An initial feasibility prototype existed before OpenAI Build Week. During the Build Week period, I focused on turning that experiment into something much more mature, reproducible, and honest about what it supports.

What it does

dry-validation-rust is a hybrid Ruby/Rust validation engine with a familiar contract-style DSL.

The Rust side handles the supported structural work: key normalization, schema traversal, coercion, nested arrays and hashes, native predicates, filtered output, and structural errors.

The Ruby side keeps the things that should remain Ruby: the public DSL, arbitrary rule blocks, macros, options, context, and injected domain objects.

The primary API uses the safe Dry::Validation::Rust namespace, so the engine can be evaluated side by side with existing code instead of immediately taking ownership of upstream constants. There is also an experimental exact-compatibility mode, but it is isolated and documented as such.

One rule became especially important while building the project: unsupported behavior must fail loudly. I would rather reject a contract than quietly accept it with different semantics and give a developer a pleasantly green but incorrect result.

So this project is not presented as complete dry-validation compatibility or as a production-ready drop-in replacement. It is an explicitly tested compatibility subset and a serious feasibility-stage developer tool.

How I built it

The Ruby layer defines the contract and turns the supported declarative schema into a stable execution plan.

A Rust native extension, built with Cargo and connected to CRuby through Magnus and rb-sys, executes that plan and returns normalized values and structured errors. Ruby then evaluates the dynamic rules that cannot be translated safely or meaningfully into Rust.

The architecture sounds simple in one paragraph. Making the boundary trustworthy was less simple. The repository therefore includes Ruby and Rust tests, compatibility fixtures, support documentation, package and isolated-install checks, CI across supported Ruby and platform combinations, dependency and security checks, verification scripts, and benchmark tooling.

The benchmark design also runs the Rust-backed and upstream implementations in separate processes. This avoids namespace collisions and makes each result traceable to a named workload, exact versions, raw measurements, and a recorded environment.

In other words, I tried not only to make the code work, but also to make its claims checkable.

How GPT-5.6 and Codex were used

GPT-5.6 became my architecture and repository-review partner.

I used it to analyze the feasibility prototype, look for the less exciting but much more dangerous problems around correctness, compatibility, the Ruby/Rust boundary, benchmarking, packaging, CI, security, and public-repository readiness, and then turn those findings into a staged maturity and refactoring roadmap.

The roadmap was intentionally more detailed than “please improve the repository.” Each stage described the files to inspect, the implementation scope, architectural invariants, tests, acceptance criteria, rollback signals, and the completion report expected from Codex.

I then used Codex to work through those stages against the real repository: inspect the code, implement changes, generate and refine tests, run verification commands, diagnose failures, and continue until the acceptance criteria were met—or report honestly when they were not.

GPT-5.6 and Codex accelerated a lot of work, but I kept the central product decisions on my side: what belongs in Rust, what must remain in Ruby, why the safe namespace is primary, why unsupported behavior should fail loudly, and which compatibility or performance claims have enough evidence to be published.

The runtime itself remains deterministic Ruby and Rust. It does not require an OpenAI API key.

Challenges I ran into

The hardest part was not translating Ruby syntax into Rust. It was deciding which semantics could be translated without lying to the user.

A Ruby validation DSL can contain arbitrary code, custom predicates, context, macros, configurable message behavior, and extension points. Reproducing the shape of the DSL is much easier than reproducing all of its meaning.

The native boundary brought another collection of cheerful little problems: Ruby exception propagation, Rust panic safety, ownership of Ruby values, nested error paths, native package layout, and the fact that the current Ruby-object execution model still operates under the GVL.

Benchmarking was another lesson in resisting attractive numbers. The upstream and hybrid engines cannot always coexist safely in one process, and a synthetic benchmark can tell almost any story when the workload is selected carelessly.

Because of that, the repository emphasizes isolated processes, exact versions, raw results, environment metadata, and workload-specific conclusions. A benchmark result should be evidence, not decoration.

Accomplishments that I am proud of

I am most proud of the boundary between Ruby and Rust.

The project does not pretend that arbitrary Ruby business logic should—or even can—be translated into Rust automatically. Instead, it moves the immutable structural work into Rust and keeps Ruby for the part where Ruby is genuinely valuable.

I am also proud that the repository treats evidence as part of the product. Unsupported features fail loudly. Compatibility claims are connected to executable checks. The built gem is tested through isolated installation. Benchmark claims are attached to reproducible scenarios instead of being presented as universal truths.

And, frankly, I am proud of how much the repository grew up during this work. What started as an exciting “can this even work?” prototype now has explicit delivery gates, repository instructions, reusable Codex skills, verification workflows, clean-machine testing, and a much clearer path toward broader validation.

What I learned

The main lesson was that hybrid-language performance work is not primarily about rewriting code in a faster language. It is about designing a boundary that remains understandable when values, errors, control flow, and ownership cross it.

I also learned that repository maturity is not administrative polish added after the “real” engineering. For a native extension, the package contents, supported platforms, toolchain, clean-install path, compatibility evidence, and benchmark methodology are part of the engineering itself.

And perhaps the most useful lesson: Rust is very good at making expensive work cheaper, but only after we decide carefully which work belongs there.

What's next for dry-validation-rust

The next steps are to broaden differential compatibility coverage, continue moving schema compilation toward immutable typed plans, improve precompiled distribution for supported platforms, and collect more evidence from realistic workloads.

Longer-term experiments include batch-oriented APIs and reducing dependence on live Ruby objects so selected native work could eventually run outside the GVL.

But the same rule will remain: new compatibility and performance claims should follow executable evidence, not arrive before it.

Built With

  • bundler
  • cargo
  • codex
  • docker
  • github-actions
  • github-container-registry
  • gpt-5.6
  • magnus
  • minitest
  • native-extensions
  • rake
  • rb-sys
  • ruby
  • rust
Share this project:

Updates