manabrew-protocol 0.42.0

Wire protocol for manabrew multiplayer
Documentation
//! Generates the TypeScript wire-protocol types from this crate via ts-rs.
//! Self-contained so a published `manabrew-protocol` ships its own generator:
//!
//!   cargo run -p manabrew-protocol --bin gen-protocol -- <out-dir>
//!
//! Output is not formatted (ts-rs emits single-line types); run the consumer's
//! own formatter afterwards if desired.
use std::fs;
use std::path::{Path, PathBuf};

use manabrew_protocol::deck_dto::Deck;
use manabrew_protocol::display::DisplayEvent;
use manabrew_protocol::prompts::{PromptInput, PromptOutput};
use manabrew_protocol::protocol::{ResumeRoomRequest, PROTOCOL_VERSION};
use manabrew_protocol::transport::{
    AgentPrompt, ClientToServerMessage, DirectiveInput, ProtocolError, StateUpdate,
};
use ts_rs::TS;

const HEADER: &str =
    "// AUTO-GENERATED by `cargo run -p manabrew-protocol --bin gen-protocol`. Do not edit.\n";
const MARK: &str = "generated by";

fn prune(dir: &Path) {
    let Ok(entries) = fs::read_dir(dir) else {
        return;
    };
    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
            prune(&path);
        } else if path.extension().is_some_and(|e| e == "ts")
            && fs::read_to_string(&path)
                .map(|c| c.to_lowercase().contains(MARK))
                .unwrap_or(false)
        {
            let _ = fs::remove_file(&path);
        }
    }
}

fn ts_modules(dir: &Path) -> Vec<String> {
    let mut names = Vec::new();
    if let Ok(entries) = fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.extension().is_some_and(|e| e == "ts") {
                if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
                    if stem != "index" {
                        names.push(stem.to_string());
                    }
                }
            }
        }
    }
    names.sort();
    names
}

fn main() {
    let out = PathBuf::from(
        std::env::args()
            .nth(1)
            .unwrap_or_else(|| "bindings".to_string()),
    );

    prune(&out);

    // A handful of roots cover the whole graph transitively; ts-rs dedups types
    // into their `export_to` files within this process.
    PromptInput::export_all_to(&out).expect("export PromptInput");
    PromptOutput::export_all_to(&out).expect("export PromptOutput");
    AgentPrompt::export_all_to(&out).expect("export AgentPrompt");
    StateUpdate::export_all_to(&out).expect("export StateUpdate");
    DirectiveInput::export_all_to(&out).expect("export DirectiveInput");
    ClientToServerMessage::export_all_to(&out).expect("export ClientToServerMessage");
    ProtocolError::export_all_to(&out).expect("export ProtocolError");
    DisplayEvent::export_all_to(&out).expect("export DisplayEvent");
    Deck::export_all_to(&out).expect("export Deck");
    ResumeRoomRequest::export_all_to(&out).expect("export ResumeRoomRequest");

    let prompts_dir = out.join("prompts");
    let mut prompts_index = String::from(HEADER);
    for module in ts_modules(&prompts_dir) {
        prompts_index.push_str(&format!("export * from \"./{module}\";\n"));
    }
    prompts_index.push_str("\nimport type { PromptInput } from \"./promptInput\";\n");
    prompts_index.push_str("export type PromptType = PromptInput[\"type\"];\n");
    fs::write(prompts_dir.join("index.ts"), prompts_index).expect("write prompts/index.ts");

    let transport_dir = out.join("transport");
    fs::write(transport_dir.join("envelope.ts"), ENVELOPE).expect("write transport/envelope.ts");
    fs::write(
        transport_dir.join("index.ts"),
        format!("{HEADER}export * from \"./messages\";\nexport * from \"./envelope\";\n"),
    )
    .expect("write transport/index.ts");

    fs::write(
        out.join("version.ts"),
        format!(
            "{HEADER}export const VERSION = \"{}\";\nexport const PROTOCOL_VERSION = {PROTOCOL_VERSION};\n",
            env!("CARGO_PKG_VERSION")
        ),
    )
    .expect("write version.ts");

    fs::write(
        out.join("index.ts"),
        format!(
            "{HEADER}export * from \"./prompts\";\nexport * from \"./transport\";\nexport type * from \"./display\";\nexport type * from \"./game\";\nexport type * from \"./deck\";\nexport type * from \"./lobby\";\nexport * from \"./version\";\n"
        ),
    )
    .expect("write index.ts");
}

/// The prompt-request envelope and distributive `Prompt` union — TS generics
/// ts-rs cannot express, emitted verbatim so the generated tree is complete.
const ENVELOPE: &str = r#"// AUTO-GENERATED by `cargo run -p manabrew-protocol --bin gen-protocol`. Do not edit.
import type { PromptInput } from "../prompts";

export interface PromptRequestEnvelope {
  promptId?: string;
  decidingPlayerId?: string;
  sourceCardId?: string;
}

export type PromptRequest<TInput extends { type: string }> = PromptRequestEnvelope & {
  input: TInput;
};

type DistributeRequest<TInput extends { type: string }> = TInput extends unknown
  ? PromptRequest<TInput>
  : never;

export type Prompt = DistributeRequest<PromptInput>;
"#;