Generator: emit colon-free title + servers block + auto-discover @server-feature mods (Closes #91, #92, #93) #94

Merged
timur merged 2 commits from issue-91-92-93-generator-fixes into development 2026-05-20 11:57:32 +00:00
Owner

Closes #91, #92, #93.

Three small generator fixes from the same root — the hero_logic#46 PR had to post-process the generator output in build.rs. Landing them together means downstream services drop their build.rs hacks in one sweep.

#91 — Drop colon from info.title

Per-domain openrpc.json now uses "<service> - <domain>" (space-dash-space, e.g. RecipeService - recipes). Pre-#91 it was <service>:<domain> which hero_rpc_derive::openrpc_client! rejected when deriving Rust idents from the title. Aggregate strip logic in write_aggregate_openrpc_json updated to match.

Format choice: ASCII space-dash-space over _ or em-dash. Reads cleanly in doc viewers, splits trivially in tooling (rsplit_once(" - ")), survives the macro's ident derivation, and avoids ambiguity with snake_case domain names.

#92 — Emit servers block

Generator now reads [[binaries.sockets]] entries from the parsed service.toml (already available on OschemaBuildConfig::service_toml per #55 §1) and emits one OpenRPC server entry per protocol = "openrpc" socket:

"servers": [
  {
    "name": "<binary-name>",
    "url": "unix://${HERO_SOCKET_DIR}/<socket-path>",
    "description": "<binary-name> over Unix domain socket"
  }
]

URL format matches what openrpc_client!::connect() expects (see crates/derive/src/openrpc_client.rs ~L840): it strips unix://, splits on ${HERO_SOCKET_DIR}, and resolves the relative path at runtime via herolib_core::base::resolve_socket_path_with_override. So no consumer-side resolution code changes.

Per-domain narrowing: for multi-binary services, per-domain openrpc.json filters to binaries whose name carries the domain suffix (_<domain>). When nothing matches (single-binary services like recipe_server), the full list is emitted. Aggregate keeps the deduped union across every per-domain spec.

#93 — Auto-discover @server-feature modules

generate_server_lib_rs (in build/emit/rust_server.rs) now mirrors the SDK side's @sdk-feature: scan: walks the server crate's src/ for *.rs files and <dir>/mod.rs modules whose top-of-file doc comment carries //! @server-feature:, and emits pub mod <name>; for each alongside the codegen-managed domain modules. The marker value (text after the colon, if any) becomes a #[cfg(feature = "<value>")] gate.

Marker-required, not discover-everything: the SDK side picks up every hand-written sibling because SDK crates are mostly client surface. Server crates routinely carry internal helpers (state types, layer impls, util fns) that should NOT be re-exported as top-level modules. Opt-in via the marker keeps the public surface explicit.

So after this lands, hero_logic_server's four hand-written modules (engine, seed, services, tracing_layer) survive regen as soon as they get a one-line //! @server-feature: marker. No build.rs rewrite.

Bonus — OschemaBuildConfig::service_toml_at

Added a focused builder method that only sets the parsed ServiceToml without flipping nested_layout: true or other modern-service defaults the way from_service_toml_at (a constructor) does. Lets non-nested layouts opt into servers emission without giving up their existing layout config. Used in example/recipe_server/crates/hero_recipes/build.rs to wire the example end-to-end.

Downstream impact (verified, not changed in this PR)

hero_logic#46's build.rs post-process patches (commit 8e76e12 on issue-44-template-pattern-upgrade) map 1:1 to these three issues:

Patch Now redundant via
"LogicService:logic""LogicService" title rewrite #91 (no colon emitted)
servers[] block injection #92 (servers emitted natively)
hero_logic_server/src/lib.rs rewrite restoring 4 modules #93 (auto-discover via marker)

Adding the //! @server-feature: marker to those four modules and deleting the post-process block lands the cleanup. Not done in this PR — that belongs in hero_logic#46.

Test plan

  • cargo test -p hero_rpc_generator --lib — 131 passed, 0 failed.
  • cargo build --workspace clean.
  • example/recipe_server regenerated: aggregate title "RecipeService", per-domain title "RecipeService - recipes", servers block present in both with unix://${HERO_SOCKET_DIR}/hero_recipes_server/rpc.sock.
  • No hand-written modules to discover in recipe_server — confirmed regen didn't change hero_recipes_server/src/lib.rs structure.

🤖 Generated with Claude Code

Closes #91, #92, #93. Three small generator fixes from the same root — the hero_logic#46 PR had to post-process the generator output in `build.rs`. Landing them together means downstream services drop their build.rs hacks in one sweep. ## #91 — Drop colon from `info.title` Per-domain `openrpc.json` now uses `"<service> - <domain>"` (space-dash-space, e.g. `RecipeService - recipes`). Pre-#91 it was `<service>:<domain>` which `hero_rpc_derive::openrpc_client!` rejected when deriving Rust idents from the title. Aggregate strip logic in `write_aggregate_openrpc_json` updated to match. **Format choice:** ASCII space-dash-space over `_` or em-dash. Reads cleanly in doc viewers, splits trivially in tooling (`rsplit_once(" - ")`), survives the macro's ident derivation, and avoids ambiguity with snake_case domain names. ## #92 — Emit `servers` block Generator now reads `[[binaries.sockets]]` entries from the parsed `service.toml` (already available on `OschemaBuildConfig::service_toml` per #55 §1) and emits one OpenRPC server entry per `protocol = "openrpc"` socket: ```json "servers": [ { "name": "<binary-name>", "url": "unix://${HERO_SOCKET_DIR}/<socket-path>", "description": "<binary-name> over Unix domain socket" } ] ``` URL format matches what `openrpc_client!::connect()` expects (see `crates/derive/src/openrpc_client.rs` ~L840): it strips `unix://`, splits on `${HERO_SOCKET_DIR}`, and resolves the relative path at runtime via `herolib_core::base::resolve_socket_path_with_override`. So no consumer-side resolution code changes. **Per-domain narrowing:** for multi-binary services, per-domain `openrpc.json` filters to binaries whose name carries the domain suffix (`_<domain>`). When nothing matches (single-binary services like recipe_server), the full list is emitted. Aggregate keeps the deduped union across every per-domain spec. ## #93 — Auto-discover `@server-feature` modules `generate_server_lib_rs` (in `build/emit/rust_server.rs`) now mirrors the SDK side's `@sdk-feature:` scan: walks the server crate's `src/` for `*.rs` files and `<dir>/mod.rs` modules whose top-of-file doc comment carries `//! @server-feature:`, and emits `pub mod <name>;` for each alongside the codegen-managed domain modules. The marker value (text after the colon, if any) becomes a `#[cfg(feature = "<value>")]` gate. **Marker-required, not discover-everything:** the SDK side picks up *every* hand-written sibling because SDK crates are mostly client surface. Server crates routinely carry internal helpers (state types, layer impls, util fns) that should NOT be re-exported as top-level modules. Opt-in via the marker keeps the public surface explicit. So after this lands, `hero_logic_server`'s four hand-written modules (`engine`, `seed`, `services`, `tracing_layer`) survive regen as soon as they get a one-line `//! @server-feature:` marker. No `build.rs` rewrite. ## Bonus — `OschemaBuildConfig::service_toml_at` Added a focused builder method that *only* sets the parsed `ServiceToml` without flipping `nested_layout: true` or other modern-service defaults the way `from_service_toml_at` (a constructor) does. Lets non-nested layouts opt into `servers` emission without giving up their existing layout config. Used in `example/recipe_server/crates/hero_recipes/build.rs` to wire the example end-to-end. ## Downstream impact (verified, not changed in this PR) hero_logic#46's `build.rs` post-process patches (commit `8e76e12` on `issue-44-template-pattern-upgrade`) map 1:1 to these three issues: | Patch | Now redundant via | |---|---| | `"LogicService:logic"` → `"LogicService"` title rewrite | #91 (no colon emitted) | | `servers[]` block injection | #92 (servers emitted natively) | | `hero_logic_server/src/lib.rs` rewrite restoring 4 modules | #93 (auto-discover via marker) | Adding the `//! @server-feature:` marker to those four modules and deleting the post-process block lands the cleanup. Not done in this PR — that belongs in hero_logic#46. ## Test plan - [x] `cargo test -p hero_rpc_generator --lib` — 131 passed, 0 failed. - [x] `cargo build --workspace` clean. - [x] `example/recipe_server` regenerated: aggregate title `"RecipeService"`, per-domain title `"RecipeService - recipes"`, servers block present in both with `unix://${HERO_SOCKET_DIR}/hero_recipes_server/rpc.sock`. - [x] No hand-written modules to discover in recipe_server — confirmed regen didn't change `hero_recipes_server/src/lib.rs` structure. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
- #91: per-domain openrpc.json `info.title` is now `<service> - <domain>`
  (space-dash-space) — `openrpc_client!`'s Rust-ident derivation rejected
  the previous `:` separator. Aggregate strip logic updated.

- #92: emit a `servers[]` block in every generated openrpc.json, derived
  from `service.toml` `[[binaries.sockets]]` entries with
  `protocol = "openrpc"`. URL format is
  `unix://${HERO_SOCKET_DIR}/<path>` so `openrpc_client!::connect()` can
  resolve via `resolve_socket_path_with_override` without build.rs hacks.
  Per-domain narrows to binaries whose name carries the domain suffix
  (`_<domain>`); aggregate keeps the union.

- #93: `generate_server_lib_rs` now mirrors the SDK side's `@sdk-feature:`
  scan — auto-discovers hand-written `*.rs` files and `<dir>/mod.rs`
  modules carrying `//! @server-feature:` and emits `pub mod` lines for
  them so regen no longer wipes them. Marker-required (server crates carry
  internal helpers; SDK-style discover-everything would over-expose).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chore(recipe_server): wire service.toml + regenerate openrpc specs
Some checks failed
Test / test (push) Failing after 3m11s
Test / test (pull_request) Failing after 2m58s
122ff10b27
Add `OschemaBuildConfig::service_toml_at` — a focused builder method that
only sets the parsed `ServiceToml` without flipping `nested_layout` or any
other defaults (unlike `from_service_toml_at`, which is a constructor and
applies the modern-service defaults).

Use it from `example/recipe_server`'s `hero_recipes/build.rs` so the
example exercises the new `servers` emission path. Regenerate the docs:

- `docs/openrpc.json` aggregate: `"title": "RecipeService"`, single
  `servers[]` entry → `unix://${HERO_SOCKET_DIR}/hero_recipes_server/rpc.sock`.
- `docs/recipes/openrpc.json` per-domain: `"title": "RecipeService - recipes"`
  (no colon — passes `openrpc_client!`'s ident derivation), same `servers[]`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
timur merged commit df51a93a20 into development 2026-05-20 11:57:32 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_rpc!94
No description provided.