Skip to content

feat!: add consuming into_raw_value to RpcSend#37

Merged
prestwich merged 6 commits into
mainfrom
feat/rpcsend-into-raw-value
Feb 14, 2026
Merged

feat!: add consuming into_raw_value to RpcSend#37
prestwich merged 6 commits into
mainfrom
feat/rpcsend-into-raw-value

Conversation

@prestwich

Copy link
Copy Markdown
Member

Summary

  • Replaces the RpcSend marker trait with a consuming into_raw_value(self) method, removing Serialize as a supertrait
  • Enables lazy serialization of iterators without Mutex<Option<T>> wrappers — types are consumed directly during serialization
  • Opens the door for custom RpcSend impls (pre-serialized data, non-serde sources) by allowing types to bypass Serialize entirely
  • All existing Serialize types continue to work via the blanket impl — no downstream breakage for typical usage
  • Bumps version to 0.6.0 (breaking: Serialize supertrait removed, notify signature changed to take T by value)

Test plan

  • cargo clippy -p ajj --all-features --all-targets — clean
  • cargo clippy -p ajj --no-default-features --all-targets — clean
  • cargo +nightly fmt --check — clean
  • cargo t -p ajj — all 18 doc-tests + 15 unit tests + 3 integration tests pass
  • Existing ctx.notify(&item) callers verified to work via &T: RpcSend blanket impl

🤖 Generated with Claude Code

…` supertrait

`RpcSend` previously required `Serialize` as a supertrait, forcing lazy
producers like iterators to use `Mutex<Option<T>>` wrappers for
interior mutability during serialization. This replaces the marker trait
with a consuming `into_raw_value(self)` method, enabling direct
ownership transfer during serialization.

The blanket impl for `T: Serialize` is preserved, so all existing code
that returns `Serialize` types from handlers continues to work. Types
that do not implement `Serialize` can now implement `RpcSend` directly
with custom serialization logic.

BREAKING CHANGE: `RpcSend` no longer has `Serialize` as a supertrait.
Code that used `T: RpcSend` and relied on the `Serialize` bound will
need to add an explicit `Serialize` bound. `HandlerCtx::notify` now
takes `T` by value instead of `&T` (callers passing `&item` still work
via the blanket impl).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/types/resp/payload.rs
Comment thread src/types/resp/ser.rs Outdated
return Some(Self::serialization_failure(id));
}
};
let resp = Response { id, payload: &raw };

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this one extra serialization round?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code]

Yes — the payload was serialized to Box<RawValue> via into_raw(), then the Response struct wrapping it was serialized again via to_raw_value. While Box<RawValue>'s Serialize impl just copies bytes (no re-parsing), it's still an unnecessary serde machinery invocation.

Fixed by building the JSON envelope directly via format! with id.get() and result.get() (or serde_json::to_string(&error) for the error case), bypassing the Response struct entirely for build_response.

- Deprecate `ErrorPayload::serialize_payload` in favor of `into_raw`
- Eliminate extra serialization round in `build_response` by constructing
  the JSON envelope directly via format!, bypassing the Response struct

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/types/resp/ser.rs Outdated
}
Err(error) => match serde_json::to_string(&error) {
Ok(error_json) => {
format!(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no hand crafting json. use types.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code]

Fixed — build_response now uses Response { id, payload: &raw }.to_json() (types, not format strings).

prestwich and others added 2 commits February 14, 2026 18:19
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Response now owns the payload via Cell<Option<...>> so the Serialize
impl can consume it. Bounds are T: RpcSend, E: RpcSend instead of
T: Serialize, E: Serialize. build_response no longer needs to call
into_raw explicitly — it's handled inside the Serialize impl.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/types/resp/ser.rs Outdated
Comment on lines +43 to +44
pub(crate) fn serialization_failure(id: &RawValue) -> Box<RawValue> {
RawValue::from_string(format!(
r#"{{"jsonrpc":"2.0","id":{},"error":{{"code":-32700,"message":"response serialization error"}}}}"#,
id.get()
))
.expect("valid json")
let resp = Response::new(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this reserializes on each invocation. memoize in a lazy and clone on demand

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code]

Addressed both comments:

  • "no hand crafting json. use types": build_response now uses Response { id, payload: &raw }.to_json().
  • "memoize in a lazy and clone on demand": parse_error() now clones from a LazyLock<Box<RawValue>> (serialized once). serialization_failure() borrows a static ResponsePayload for the error payload — only the envelope serialization (with the dynamic id) happens per call.

prestwich and others added 2 commits February 14, 2026 18:39
- parse_error() now clones from a LazyLock<Box<RawValue>> instead of
  serializing on every call
- serialization_failure() borrows a static ResponsePayload instead of
  reconstructing it per invocation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@prestwich prestwich merged commit e3df3b6 into main Feb 14, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant