Status: Draft plan, needs review before implementation.
Prior work: 95b76f3 extracted router/, 31bf3c8 extracted cookie/.
Context: Round 3 extracts the three middleware implementations (rate_limit, request_id, security_headers) out of the root package into a new bobzhang/crescent/middleware sub-package. The Middleware/MiddlewareNext type aliases, the App::use_middleware method, and the execute_middlewares runtime all stay at root (they are intrinsic to App, and execute_middlewares is priv today).
Give middleware implementations a discoverable home (@middleware.rate_limit(...) etc.), remove 3 .mbt files and 3 test files from the root package, and set the precedent for cors/ to potentially relocate into the same namespace in a future round.
Source files (move, then re-qualify root types):
rate_limit.mbt→middleware/rate_limit.mbt. Body referencesMiddleware,HttpResponse,StatusCode(constructingTooManyRequests), and@async.now(). After the move these become@crescent.Middleware,@crescent.HttpResponse, and@async.now()respectively. TheTooManyRequestsenum variant should resolve via type-directed inference from the@crescent.HttpResponse(status_code=...)parameter type (the same mechanism that handledsame_site=Laxacross packages in round 2 — needs codex to double-check this holds forStatusCodeenum variants too).security.mbt→middleware/security.mbt. Body referencesMiddleware,Event,@httputil.set_missing_header_case_insensitive. After the move:@crescent.Middleware,@crescent.Event, and@httputil.*stays unchanged (but themiddleware/package needs its ownbobzhang/crescent/httputil @httputilimport).request_id.mbt→ split. Middleware function and its private state (request_id_counter,generate_request_id) move tomiddleware/request_id.mbt; the accessor methodEvent::request_id(self)has to stay at root because MoonBit's coherence rules disallow defining methods on external types (confirmed in round 2 plan review and bycors/'s free-function pattern). The accessor moves into rootevent.mbtwhereEventis defined.
Test files:
rate_limit_test.mbt→middleware/rate_limit_test.mbt. Currently blackbox-style (uses@crescent.App,@test_client.TestClient, etc.). After the move, the local@crescent.rate_limitreference drops to unqualifiedrate_limit(same package);@crescent.App/TestClientstay qualified.request_id_test.mbt→middleware/request_id_test.mbt. Same mechanical edit for@crescent.request_id→request_id. Note:event.request_id()inside the handler stays as a method call — method dispatch works across package boundaries.security_wbtest.mbt→ rewritten and renamed tomiddleware/security_test.mbt(blackbox). The current wbtest pokes intopriv fn execute_middlewaresdirectly, which won't be accessible from a sub-package. Rewrite each test to useApp+TestClientso we can assert on the response headers after a real dispatch. This also aligns with the user's_test.mbt-over-_wbtest.mbtpreference recorded in memory. Four tests to port:security_headers sets base headers by defaultsecurity_headers with opt-in policy headerssecurity_headers preserves handler-set headerssecurity_headers preserves handler responseEach becomes aApp()with a single route anduse_middleware(security_headers(...)), then aTestClient.get("/test")call and assertions onres.headers.*orres.body_text().
middleware.mbt(the root file) keepsMiddlewareNext,Middlewaretype aliases,App::use_middleware,execute_middlewares,execute_middleware_chain, andnormalize_middleware_base_path. These are infrastructure the sub-package builds on top of.event.mbtabsorbs theEvent::request_id(self) -> String?accessor (3 lines).test_client_wbtest.mbt— its one test that usessecurity_headers()gets rewritten to use a custom inline middleware (the test is really verifying "TestClient runs middleware", not "security_headers sets X-Frame-Options"). This avoids wbtest → middleware → crescent cycle concerns. Alternatively we could leave this test using the root-level binding if we re-export via a typealias, but a small rewrite is cleaner.
import {
"bobzhang/crescent",
"bobzhang/crescent/httputil" @httputil,
"moonbitlang/async",
}
warnings = "+missing_doc+unnecessary_annotation+74"
bobzhang/crescentforMiddleware,Event,HttpResponse,StatusCodevariants (default alias@crescent).bobzhang/crescent/httputilfor@httputil.set_missing_header_case_insensitiveused bysecurity.mbt.moonbitlang/asyncfor@async.now()used byrate_limit.mbtandrequest_id.mbt.
Codex: please verify these are sufficient. Any issues with the +74 warning suppression?
The root package's library code does NOT import middleware — that would cycle (middleware → crescent → middleware). Only the blackbox test layer of root needs middleware in scope (so fullstack_test.mbt can call @middleware.security_headers()). This requires adding it to the existing for "test" block, NOT the main import block:
import {
"bobzhang/crescent/middleware" @middleware,
"moonbitlang/async/os_error",
} for "test"
Rationale: blackbox tests are compiled as a separate package (bobzhang/crescent_blackbox_test) that is allowed to import both the root and any package that depends on root, because they're a downstream of root rather than part of root's own compilation. Codex — please sanity-check whether MoonBit's for "test" import scoping actually supports this pattern, or whether I need a different approach (e.g. inlining a custom security middleware in fullstack_test.mbt).
[P1] README.mbt.md has checked doctest blocks that call the moved functions. Original plan missed these. Fix: update each of the three locations below, and rely on the for "test" root middleware import so the blackbox-test-compiled README snippets can resolve @middleware.*:
README.mbt.md:143-177—moonbit checkblock defines_build_app()calling@crescent.security_headers()and@crescent.request_id(). Update to@middleware.security_headers()/@middleware.request_id().README.mbt.md:179-200—mbt checkblackbox test block calls unqualifiedsecurity_headers()andrequest_id(). Update to@middleware.security_headers()/@middleware.request_id().README.mbt.md:688-694— documentation table listing the built-in middleware. Update the display names to reflect the@middleware.*location (not compiled; purely docs).README.mbt.md:698-710—moonbit nocheckexample for a custom rate limiter. Not compiled, but update the narrative around "writing custom middleware" if it misstates whererate_limit/etc. live.
[P2] Alternative D rationale correction. MoonBit does support re-export mechanisms (pub using, #alias) per codex; the plan was wrong to say there is none. The real reason we can't re-export from root is the import cycle: root cannot import bobzhang/crescent/middleware for its main library compilation because middleware already imports root. Re-exports are therefore impossible in the main package context, which is why we must migrate call sites rather than alias.
-
event.mbt— append:///| /// Returns the request ID for this event, or `None` if the middleware is not active. pub fn Event::request_id(self : Event) -> String? { self.req.get_header("X-Request-Id") }
-
fullstack_test.mbt:114—@crescent.security_headers()→@middleware.security_headers(). (This is a_test.mbtblackbox test file, so the root'sfor "test"import is in scope.) -
test_client_wbtest.mbt:69-78— rewrite thetest client runs middlewaretest:async test "test client runs middleware" { let ran = Ref::new(false) let app = App() app.use_middleware((_event, next) => { ran.val = true next() }) app.get("/test", fn(_) noraise { "ok" }) let client = @test_client.TestClient(app) let res = client.get("/test") assert_eq(res.status, OK) assert_eq(ran.val, true) }
This test still verifies that TestClient dispatches through the middleware chain, but without depending on
security_headers. Coverage ofsecurity_headersitself moves tomiddleware/security_test.mbt. -
examples/route/moon.pkg— add"bobzhang/crescent/middleware" @middlewareto the import block. -
examples/route/main.mbt:9—@crescent.security_headers()→@middleware.security_headers().
crescent/
├── middleware.mbt (unchanged — keeps type aliases + use_middleware + runtime)
├── event.mbt (+3 lines for Event::request_id accessor)
├── rate_limit.mbt (DELETED)
├── request_id.mbt (DELETED)
├── security.mbt (DELETED)
├── rate_limit_test.mbt (DELETED)
├── request_id_test.mbt (DELETED)
├── security_wbtest.mbt (DELETED)
├── test_client_wbtest.mbt (one test rewritten to use custom inline middleware)
├── fullstack_test.mbt (@crescent.security_headers → @middleware.security_headers)
├── moon.pkg (+1 line in `for "test"` block)
├── pkg.generated.mbti (auto: removes rate_limit/request_id/security_headers)
├── middleware/ (NEW)
│ ├── moon.pkg
│ ├── rate_limit.mbt
│ ├── request_id.mbt
│ ├── security.mbt
│ ├── rate_limit_test.mbt (blackbox)
│ ├── request_id_test.mbt (blackbox)
│ ├── security_test.mbt (NEW blackbox — rewritten from wbtest)
│ └── pkg.generated.mbti
├── examples/route/
│ ├── moon.pkg (+1 middleware import)
│ └── main.mbt (1 call site updated)
└── ... (cookie/, router/, etc. unchanged)
Moved out of root:
@crescent.rate_limit(...)→@crescent/middleware.rate_limit(...)@crescent.request_id()→@crescent/middleware.request_id()@crescent.security_headers(...)→@crescent/middleware.security_headers(...)
Unchanged:
@crescent.Middleware,@crescent.MiddlewareNexttype aliasesApp::use_middleware(self, mw, base_path?)methodEvent::request_id()method (accessor moves within root toevent.mbtbut remains on the same type)
Migration for downstream users:
// Before
app.use_middleware(@crescent.rate_limit(requests_per_window=100, window_ms=60000))
// After
app.use_middleware(@middleware.rate_limit(requests_per_window=100, window_ms=60000))Plus adding "bobzhang/crescent/middleware" @middleware to their own moon.pkg import block.
moon check— catches missing qualifiers and thefor "test"import scoping question.moon test --verbose— must still report 504/504 passing, and the specific namesrate_limit_test.mbt,request_id_test.mbt,security_test.mbtshould appear under themiddleware/path in the verbose output.moon info && moon fmt— regenerate mbti. Root mbti losesrate_limit,request_id,security_headersfrom the// Valuessection. Newmiddleware/pkg.generated.mbtigains them. No other root mbti changes expected (Middleware type alias, use_middleware, Event::request_id all stay).- Spot-check the example builds:
moon checkshould coverexamples/route/as part of the full-workspace check.
A. Keep the Round-3 scope even smaller — only extract rate_limit and security, leave request_id at root
Rejected because the Event::request_id accessor is only 3 lines and splitting request_id.mbt is trivial. Leaving the middleware function at root just to avoid a split makes the sub-package feel arbitrary ("why these two but not that one?"). Codex — push back if you think the split adds more complexity than it's worth.
Rejected for this round because it adds a rename of an already-extracted package and touches every @cors.* call site in the codebase (examples/route/main.mbt:8, possibly others). Keeping the commit scoped to "extract from root" rather than "reorganize sub-packages" preserves bisect-ability. Worth considering as a standalone round-4 rename.
Rejected because a pure rename + content update is harder to review than a single commit that both moves and rewrites. Doing the rewrite inside the move commit is OK as long as the commit message explicitly calls it out (which the plan does).
D. Add pub type rate_limit = @middleware.rate_limit aliases at root for source-level backward compatibility
Rejected because pub type X = Y in MoonBit defines a new type (sometimes transparent), not a function alias. There is no fnalias / pub fn x = @middleware.x syntax I'm aware of. Users must migrate. Same call as round 1 and round 2. Codex — confirm this is the case, or point to a re-export feature I'm missing.
Rejected because they are consumed by App::use_middleware (which stays at root due to the method-orphan rule), and moving just the aliases without the method creates a weird cross-package dependency. They belong with use_middleware, which belongs with App.
-
Enum variant resolution across packages. Does
@crescent.HttpResponse(status_code=TooManyRequests)compile insidemiddleware/rate_limit.mbt, or does theTooManyRequestsvariant need to be qualified as@crescent.TooManyRequests? Round 2 showed type inference handles enum variants at call sites when the parameter type is explicit. I believe the same holds here becausestatus_codehas type@crescent.StatusCode, but please verify by readingrate_limit.mbt:23context and the round-2 README cookie doctest precedent. -
for "test"import scoping. Can root's blackbox tests import a package that itself imports root? Conceptually this is the dependency DAGmiddleware → root, withroot_blackbox_test → {root, middleware}as a downstream. Is this actually what MoonBit supports, or will I hit a cycle error atmoon checktime? If it's not supported, propose an alternative — e.g., drop the@middleware.security_headers()call infullstack_test.mbtand use an inline custom middleware instead, mirroring what I'm doing fortest_client_wbtest.mbt. -
Does
security_wbtest.mbtgenuinely use any priv-only access besidesexecute_middlewares? If not, a blackbox rewrite viaTestClientshould cover everything. Confirm by reading the file. -
Method orphan rule for request_id split. Am I right that
Event::request_idMUST stay at root, and that moving it tomiddleware/would fail with a coherence error? Round 2 plan review said yes based oncors/precedent — I'm not re-litigating unless you have new evidence. -
Is the plan's scope right for a single round? Too big? Too small? If you think round 3 should also rename
cors/→middleware/cors/, say so and I'll fold that in. Otherwise I'll keep it as proposed and defer cors relocation to round 4. -
Test file naming. The existing
rate_limit_test.mbt,request_id_test.mbtare blackbox (_test.mbt) — good, they move verbatim.security_wbtest.mbtneeds to becomesecurity_test.mbtafter the rewrite. Any MoonBit convention I should know about for test file naming in a sub-package that imports its parent?
Keep responses concise and actionable — I want to implement, not re-plan. Tag each finding [P1 blocker / P2 should fix / P3 nit].