This file defines the project's structural conventions. Apply them when adding new code or modifying existing code. Reorganize new modules to fit this layout rather than inventing new top-level folders.
We use the Rust 2018+ style: a parent module lives in <name>.rs next
to its <name>/ subfolder of children — never inside it as mod.rs.
See https://doc.rust-lang.org/stable/book/ch07-02-defining-modules-to-control-scope-and-privacy.html.
src/
├── main.rs — entry point, tracing setup, runs MusicBotClient.
├── bot.rs — framework wiring & MusicBotData. No event/business logic.
├── checks.rs — declares `channel_checks`, `player_checks`.
├── checks/ — command/button checks invoked via poise's `check =`.
├── commands.rs — declares feature submodules + `help`.
├── commands/ — thin command handlers grouped by feature area.
├── embeds.rs — declares feature embed submodules.
├── embeds/ — Discord embeds, grouped by feature area.
├── handlers.rs — declares error/queue/voice handlers.
├── handlers/ — every async/event handler (Serenity/Songbird/poise).
├── player.rs — declares `player`, `track`.
├── player/ — Music bot player only.
│ ├── player.rs — Player struct, state transitions, activity helpers.
│ └── track.rs — Track / Playlist / TrackSource / PlaybackError types.
├── service.rs — declares all service files.
├── service/ — business-logic services that back the commands.
├── sources.rs — declares `local_player`, `spotify_player`, `youtube_player`.
├── sources/ — track sources used by commands and services.
│ ├── local_player.rs
│ ├── spotify_player.rs
│ └── youtube_player.rs
├── utils.rs — declares `string_utils`, `time_utils`.
└── utils/ — pure, shared helpers reusable across services/commands.
├── string_utils.rs — number_to_emoji, sanitize_name, MAX_NAME_LEN.
└── time_utils.rs — get_current_time, humanize_duration, parse_text, …
-
Embeds: All Discord embeds live under
embeds/. Group them in subfolders that mirror the feature area (embeds/music/queue_embed.rs,embeds/activity/break_embed.rs). Do not define embeds inline inside commands or services — define a variant on the feature's embed enum and call.to_embed()from there. -
Utils: When a helper is used by more than one service or command, move it to
utils/and group by topic (utils/time_utils.rs,utils/string_utils.rs). Helpers used by exactly one module stay local to that module. Each topic file should expose stateless, pure functions — no Discord types, no I/O. -
Handlers: Every event handler — Serenity
FullEvent, SongbirdEventHandler, poise error/lifecycle hooks — lives inhandlers/.bot.rsshould not contain inline event-handling logic; delegate to ahandlers/<name>_handler.rsentry point. -
Services vs commands: Commands are thin orchestrators. They:
- Parse arguments and validate via
checks/. - Call into
service/,player/, orsources/to do the work. - Send embeds via
service::embed_service::SendEmbed.
Substantive logic (loops, retry handling, multi-step flows, state mutation) belongs in a service. If a command grows beyond ~50 lines or acquires nested helpers, push the body into a service.
- Parse arguments and validate via
-
Sources: Track origins (
spotify,youtube,local) live insources/as flat files named<name>_player.rs. Both commands and services may call into them. A new source goes in as a sibling file — no per-source subfolder unless it grows enough to need one. -
Coupled commands stay together: When commands hand off to each other (
breakends and auto-startsgather), put them in a sharedcommands/<feature>/folder. Don't split them acrossutility/and another folder just because the entry points have different names. -
Checks: The
checks/folder contains only check functions used by#[poise::command(... check = "…")]and by interactive button handlers. Determinations of "is this action doable right now" go here. Lower-level state predicates that the player uses internally to guard its own methods stay onPlayer. -
Module names: Files keep their
_service,_handler,_embed,_player,_checks,_utilssuffix in the filename so the role is obvious inusestatements. Picking the right suffix is part of choosing the right folder.No
mod.rs. A module that has children lives in<name>.rsnext to the<name>/folder it owns. Never add amod.rsinside the folder. -
Player folder:
player/is only for the music bot's playback engine — thePlayerstate machine and the value types (Track,Playlist,TrackSource,PlaybackError) it owns. Anything that isn't part of playing audio (notifications, reminders, gather/break state) belongs inservice/. Split long files inplayer/by topic (state machine vs. data types) rather than letting them grow. -
Function design: Avoid functions with large parameter lists. If a function requires many parameters (e.g. 7–9+), it is usually a design smell. Instead:
- Group related values into structs.
- Introduce traits or domain types when appropriate.
- Prefer passing structured data over long argument lists.
-
No temporary hacks: Do not implement quick temporary fixes just to "make it work". If the current design makes something awkward, step back and propose a proper abstraction (structs, traits, service boundary, etc.). Prefer improving the design over adding fragile one-off logic.
-
Database queries: When writing SQL queries (e.g. with
sqlx):- Prefer
query_as!()instead ofquery!()when returning structured data. - Define a Rust struct representing the result row.
- Bind query results directly into that struct.
Example:
struct UserRow { id: i64, username: String, created_at: chrono::DateTime<chrono::Utc>, } let users: Vec<UserRow> = sqlx::query_as!( UserRow, "SELECT id, username, created_at FROM users" ) .fetch_all(&database_pool) .await?;
This keeps database access strongly typed and avoids loosely structured tuples when multiple fields are returned.
- Prefer
rustfmt.toml pins max_width = 200. A GitHub Action
(.github/workflows/fmt.yml) runs cargo fmt --all on every push to
master and auto-commits any changes as style: cargo fmt. Feature
branches don't need to be perfectly formatted — that gets ironed out on
merge — but running cargo fmt locally keeps diffs small.
- Embeds go in
embeds/<area>/<feature>_embed.rs. - Business logic goes in
service/<feature>_service.rs(or extends an existing service when it fits). - Source-specific code goes in
sources/<source>_player.rs. - Event/listener code goes in
handlers/<event>_handler.rs. - Command file in
commands/<area>/cmd_<name>.rsthat calls the service. - Shared helpers (time, string, parsing) extract into
utils/. - Register the command in
bot.rs.
If a refactor would violate any of the above, fix the layout first and then make the change.