feature: Compile-check the Book's code examples (and fix 3 doc bugs they caught)#603
Conversation
…hey caught
Adds a `book-examples` JVM module (not published, aggregated into the JVM
build so CI compiles it) with one source per JVM-runnable book chapter —
Design, Logging, Data, Rx, Control, HTTP, RPC — plus a real UniTest suite
for the Testing chapter. The book's authoring guide requires runnable
snippets to compile; nothing enforced it until now, so they could drift
as the API evolved.
Compiling them immediately surfaced three real doc bugs, all fixed here:
- `Resource.withResource` does not exist — the API is `Control.withResource`
/ `Control.withResources`. Fixed in both the book (ch08) and the
`control/resource.md` reference.
- The JSON access DSL (`json("user")("name").toStringValue`, `json / "k"`)
lives in `package object json` as implicit classes, so it needs
`import wvlet.uni.json.*`, not `import wvlet.uni.json.JSON`. Fixed in both
the book (ch06) and `core/json.md`.
`./sbt bookExamples/test` passes (3/3); `pnpm docs:build` passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a new book-examples module containing compile-checked Scala examples corresponding to chapters in "The Uni Book" to prevent API drift, along with documentation updates to reflect API changes (such as replacing Resource with Control.withResource). Feedback on the changes highlights two issues: first, helper classes in Ch04TestingTest.scala are defined as inner classes, which will cause dependency injection to fail at runtime due to the missing outer class instance; second, the CircuitBreaker in Ch08Control.scala is instantiated inside the combined method, preventing it from maintaining state across invocations.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| class Ch04TestingTest extends UniTest: | ||
| test("adds two numbers") { | ||
| val result = 2 + 3 | ||
| result shouldBe 5 | ||
| } | ||
|
|
||
| test("a list contains a value") { | ||
| Seq("a", "b", "c") shouldContain "b" | ||
| } | ||
|
|
||
| // Substitute, don't mock: take the real wiring and override one binding. | ||
| class Database: | ||
| def lookup(id: String): Option[String] = None | ||
| class FakeDatabase extends Database: | ||
| override def lookup(id: String): Option[String] = Some("Alice") | ||
| class UserService(db: Database): | ||
| def findUser(id: String): Option[String] = db.lookup(id) | ||
|
|
||
| test("looks up a user via a Design override") { | ||
| val appDesign = Design.newDesign | ||
| .bindSingleton[Database] | ||
| .bindSingleton[UserService] | ||
|
|
||
| val testDesign = appDesign + | ||
| Design.newDesign.bindInstance[Database](FakeDatabase()) | ||
|
|
||
| testDesign.build[UserService] { users => | ||
| users.findUser("123") shouldBe Some("Alice") | ||
| } | ||
| } |
There was a problem hiding this comment.
Defining Database, FakeDatabase, and UserService as inner classes of Ch04TestingTest makes them non-static. Since UserService requires an outer instance of Ch04TestingTest for instantiation, Design.build[UserService] will fail at runtime because the DI container cannot satisfy the outer class dependency. Moving these helper classes to a companion object makes them static nested classes, resolving the DI instantiation issue.
class Ch04TestingTest extends UniTest:
import Ch04TestingTest.*
test("adds two numbers") {
val result = 2 + 3
result shouldBe 5
}
test("a list contains a value") {
Seq("a", "b", "c") shouldContain "b"
}
test("looks up a user via a Design override") {
val appDesign = Design.newDesign
.bindSingleton[Database]
.bindSingleton[UserService]
val testDesign = appDesign +
Design.newDesign.bindInstance[Database](FakeDatabase())
testDesign.build[UserService] { users =>
users.findUser("123") shouldBe Some("Alice")
}
}
object Ch04TestingTest:
class Database:
def lookup(id: String): Option[String] = None
class FakeDatabase extends Database:
override def lookup(id: String): Option[String] = Some("Alice")
class UserService(db: Database):
def findUser(id: String): Option[String] = db.lookup(id)| def combined(url: String): String = | ||
| val breaker = CircuitBreaker.withConsecutiveFailures(5) | ||
| breaker.run { | ||
| Retry.withBackOff(maxRetry = 3).run { | ||
| Control.withResource(openConnection()) { conn => | ||
| conn.get(url) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Defining the CircuitBreaker inside the combined method creates a new instance of the breaker on every invocation. A circuit breaker must be shared across multiple calls to maintain its state (failure count, open/closed status) and function correctly. Defining it as a member field of the Ch08Control object ensures the state is preserved across calls, matching the pattern described in the book.
private val combinedBreaker = CircuitBreaker.withConsecutiveFailures(5)
def combined(url: String): String =
combinedBreaker.run {
Retry.withBackOff(maxRetry = 3).run {
Control.withResource(openConnection()) { conn =>
conn.get(url)
}
}
}`docs/uni-walkthrough.md` was a 566-line single-page tour that is now **orphaned** (not referenced in any nav, sidebar, or index — reachable only by direct URL or site search) and fully **superseded** by The Uni Book (narrative) plus the module references (API). It also still used the old "dependency injection" wording and contained 566 lines of unverified code at risk of the same drift the [book-examples PR](#603) just found. Rather than delete it (which would 404 any old bookmark or external link), this replaces its content with a concise **router page**: a short "this has moved" note and a table mapping each former section to its new home — the relevant Book chapter and the relevant reference page. The URL stays alive; the stale code is gone. `pnpm docs:build` passes (all the `/book/*` and `/core/*` targets resolve). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Code format CI check flagged the new book-examples sources and the build.sbt edit. Apply scalafmtAll + scalafmtSbt. No behavior change; bookExamples/Test/compile still passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Book's authoring guide says every non-illustrative snippet must compile against the current
uni— but nothing enforced it, so the examples could silently drift as the API evolves. This adds abook-examplesmodule that makes the book self-verifying, and fixes the drift that compiling immediately surfaced.The module
A JVM-only
book-examplesproject (mirrors the existingnettyproject pattern;publish / skip, aggregated into the JVM build so CI compiles it). One source file per JVM-runnable chapter — Design, Logging, Data, Rx, Control, HTTP, RPC — using the same APIs as the chapter's snippets. Examples are compiled, not run (servers configured but not started);Ch04TestingTestis a realUniTestsuite that also runs.(Chapters 12–15 — Scala.js/Vite and Scala Native FFI — need the JS/Native toolchains and external bundlers, so they're verified against their reference sources instead, as the README notes.)
Doc bugs it caught — all fixed here
Resource.withResourcedoes not exist. The real API isControl.withResource/Control.withResources. Fixed in the book (ch08) andcontrol/resource.md(was wrong in 10 places).import wvlet.uni.json.*.json("user")("name").toStringValueandjson / "k"are implicit-class extensions inpackage object json;import wvlet.uni.json.JSONdoesn't bring them into scope. Fixed in the book (ch06) andcore/json.md.These are exactly the kind of copy-paste-breaking errors the module now prevents.
pnpm docs:buildpasses.🤖 Generated with Claude Code