import os
from typing import Final
import re
NOTICE = "DO NOT modify manually! Generate with `actor2bctor_and_doc.py`."
def substitute_for_sync(text: str) -> str:
text = (
text.replace(
"use super::*", "use std::thread::{spawn, JoinHandle}; use super::*"
)
.replace(
"""let maybe_msg = select! {
biased;
() = cancellation_token.cancelled() => return Ok(()),
m = env.msg_receiver.recv() => m,
}""",
"""if cancellation_token.is_cancelled() {
return Ok(());
}
let maybe_msg = env.msg_receiver.blocking_recv()""",
)
.replace(
"""select! {
biased;
() = cancellation_token.cancelled() => return Ok(()),
maybe_ok = self.handle_call_or_cast(msg, env) => maybe_ok,
}?""",
"""if cancellation_token.is_cancelled() {
return Ok(());
}
match msg {
Msg::Exit => return Ok(()) ,
_ => self.handle_call_or_cast(msg, env)?,
}""",
)
.replace("enum Msg<Call, Cast, Reply> {", "enum Msg<Call, Cast, Reply> { Exit,")
.replace(
"""match msg {
Msg""",
"""match msg {
Msg::Exit => unreachable!("Exit signals should be handled before handling `handle_call_or_cast`."),
Msg""",
)
.replace(
"self.cancellation_token.cancel()",
"""self.cancellation_token.cancel();
_ = self.msg_sender.blocking_send(Msg::Exit)""",
)
.replace(
"""
fn spawn_with_channel(
self,
""",
"""
fn spawn_with_channel(
mut self,
""",
)
.replace(
"""async move {
let exit_result = self.run_and_handle_exit(&mut env).await""",
"""move || {
let exit_result = self.run_and_handle_exit(&mut env)""",
)
.replace(
"""timeout(DECI_SECOND, server_ref.call(PingOrPong::Ping)).await {
Ok(Err(_)) |""",
"server_ref.call(PingOrPong::Ping) {",
)
.replace("const DECI_SECOND: Duration = Duration::from_millis(100);", "")
.replace(" task", " thread")
.replace(
"""/// <summary>This trait is not object-safe.</summary>
///
/// ```compile_fail
/// use tokio_gen_server::actor::ActorRunExt;
""",
"""/// <summary>This trait is object-safe.</summary>
///
/// ```
/// use tokio_gen_server::bctor::BctorRunExt;
""",
)
)
text = re.sub(r"\n\n(:?///.*\n)*.* fn blocking.* \{(:?\n.+)+\n {4}\}", "", text)
text = re.sub(
r"impl Future<Output = (['_\w()<>,: ]+)>(:? +\+ +['_\w]+)*",
r"\1",
text,
)
text = re.sub(r"receiver[\s\n]*\.await", "receiver.blocking_recv()", text)
text = re.sub(r"\.send\((.*)\)[\s\n]*\.await", r".blocking_send(\1)", text)
text = re.sub(
r"timeout\([_\w]+, handle\)[\n\s]*\.await\?\?",
"handle.join().unwrap()",
text,
)
text = re.sub(r"\n(:?\s*///.*\n)*.*fn .*join_set.*(?:\n.*[)\S])*\n", "\n", text)
text = re.sub(
r"\n\s*/// # Snippet for copying(?:\n\s*/// .*)+?\n\s*/// ```", "", text
)
text = re.sub(r"async { (.*) }", r"\1", text)
text = (
text.replace("Actor", "Bctor")
.replace("actor", "bctor")
.replace("async fn ", "fn ")
.replace(".recv(", ".blocking_recv(")
.replace(".await", "")
.replace("#[tokio::test]", "#[test]")
)
return text
def transform_file(input_filename: str, output_filename: str, header: str = ""):
with open(input_filename) as f:
input = f.read()
output = substitute_for_sync(input)
with open(output_filename, "w") as f:
_ = f.write(header + output)
_ = os.system(f"rustfmt {output_filename}")
ACTOR_SRC_NAME: Final = "src/actor.rs"
BCTOR_SRC_NAME: Final = "src/bctor.rs"
BCTOR_HEADER: Final = f"// {NOTICE}"
MOD_DOC: Final = (
BCTOR_HEADER
+ """
//! Blocking aCTOR. Mirrors functionalities in `actor` but blocking.
//!
//! Unlike Actors, Bctors are spawn using [`spawn`] from [`std`] and
//! cannot be cancelled during the handling of each message.
//! Bctors are supposed to be long-lived.
//!
"""
)
ACTOR_TEST_NAME: Final = "src/actor/tests.rs"
BCTOR_TEST_NAME: Final = "src/bctor/tests.rs"
def actor2bctor() -> None:
transform_file(ACTOR_SRC_NAME, BCTOR_SRC_NAME, MOD_DOC)
transform_file(ACTOR_TEST_NAME, BCTOR_TEST_NAME, BCTOR_HEADER + "\n")
_ = os.system(
"cargo clippy --all-targets --fix --allow-dirty --allow-staged --workspace"
)
_ = os.system("cargo fmt")
def concate_and_paste(
input_filename: str,
output_filename: str,
n_cutoff_line: int = 0,
header: str = "",
footer: str = "",
):
with open(input_filename) as f:
input_lines = f.readlines()
input_trimmed = "".join(input_lines[n_cutoff_line:])
with open(output_filename, "w") as f:
_ = f.write(header + input_trimmed + footer)
DOC_HEADER: Final = f"<!-- {NOTICE} -->"
ACTOR_DOC_NAME: Final = "src/actor_doc.md"
ACTOR_DOC_HEADER: Final = f"""{DOC_HEADER}
# An Elixir/Erlang-GenServer-like actor
Define 3 message types and at least one callback handler on your struct to
make it an actor.
A GenServer-like actor simply receives messages and acts upon them.
A message is either a "call" (request-reply) or a "cast" (fire-and-forget).
Upon a "call" message, we call your [`Actor::handle_call`];
upon a "cast" message, we call your [`Actor::handle_cast`].
Upon cancellation or error, we call your [`Actor::before_exit`],
so you can gracefully shut down.
## Usage
1. Define your actor `struct` that stores your states and
implement `Actor` for it.
1. Declare your message types.
If your actor do not expect any "cast", set `Cast` to `()`;
if your actor do not expect any "call",
set both `Call` and `Reply` to `()`.
> Tip: use your editor to automatically generate "required fields".
1. Implement `handle_call` and/or `handle_cast` for your actor.
> Tip:
> use your editor to automatically generate "provided implementations",
> then hover on the methods you need and copy the snippets in
> their docstrings.
1. Implement `init` and `before_exit` if needed.
1. Spawn your actor with [`ActorExt::spawn`]
or other similar methods and get the [`ActorRef`].
1. Use [`ActorRef`] to send messages to your actor.
## Example
<details>
```rust
"""
ACTOR_DOC_FOOTER: Final = """
```
</details>
---
<details>
<summary>This trait is not object-safe.</summary>
```compile_fail
use tokio_gen_server::prelude::*;
let _: Box<dyn Actor<Call = (), Cast = (), Reply = ()>>;
```
</details>
""".lstrip()
BCTOR_DOC_NAME: Final = "src/bctor_doc.md"
BCTOR_DOC_HEADER: Final = f"""{DOC_HEADER}
# An Elixir/Erlang-GenServer-like Blocking aCTOR
`Bctor` mirrors the functionality of [`Actor`], but blocking.
Please see [`Actor`]'s documentation for the usage.
Tokio channels are used for compatibility.
## Example
<details>
```rust
"""
BCTOR_DOC_FOOTER: Final = """
```
</details>
---
<details>
<summary>This trait is object-safe.</summary>
```
use tokio_gen_server::prelude::*;
let _: Box<dyn Bctor<Call = (), Cast = (), Reply = ()>>;
```
</details>
""".lstrip()
def gen_docs() -> None:
concate_and_paste(
ACTOR_TEST_NAME, ACTOR_DOC_NAME, 4, ACTOR_DOC_HEADER, ACTOR_DOC_FOOTER
)
concate_and_paste(
BCTOR_TEST_NAME, BCTOR_DOC_NAME, 5, BCTOR_DOC_HEADER, BCTOR_DOC_FOOTER
)
def main() -> None:
actor2bctor()
gen_docs()
main() if __name__ == "__main__" else None