Restructure into a library crate with modular bot and irc

Split the single-binary crate into a reusable `rustbot` library (src/lib.rs)
plus a thin supervisor binary. The monolithic irc.rs and bot.rs become module
folders:

  irc/    message (wire parsing), connection (TCP/TLS transport), encoding
  bot/    mod (state + event loop), caps (IRCv3 cap negotiation + SASL),
          commands (message routing + command table)
  config  Config + layered loading, moved out of main.rs and bot.rs

main.rs is now just the per-network connect/reconnect supervisor over the
library API. Unit tests move out of the source files into tests/ integration
tests (config.rs, protocol.rs) that drive the public surface — prefix parsing
is now covered through Message::parse, and config layering through the new
public config::parse_str. Behavior is unchanged; no new dependencies.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jean Chevronnet 2026-07-29 15:38:55 +00:00
parent e7e805cb87
commit eb3b8fec40
No known key found for this signature in database
GPG key ID: 439666D63A9477E4
16 changed files with 1189 additions and 1116 deletions

55
tests/config.rs Normal file
View file

@ -0,0 +1,55 @@
//! Integration tests for config layering and `[network]` sections, exercising
//! the public `rustbot::config::parse_str` (no filesystem or environment).
use rustbot::config::parse_str;
#[test]
fn flat_config_is_a_single_network() {
let cfgs = parse_str("server = irc.example.org\nnick = flatbot\nchannels = #a #b\n");
assert_eq!(cfgs.len(), 1);
assert_eq!(cfgs[0].server, "irc.example.org");
assert_eq!(cfgs[0].nick, "flatbot");
assert_eq!(cfgs[0].channels, vec!["#a", "#b"]);
// The label defaults to the server host.
assert_eq!(cfgs[0].name, "irc.example.org");
}
#[test]
fn sections_inherit_top_level_defaults_and_override() {
let cfgs = parse_str(
"nick = shared\nchannels = #lobby\n\
[libera]\nserver = irc.libera.chat\ntls = true\n\
[oftc]\nserver = irc.oftc.net\nnick = other\nchannels = #oftc\n",
);
assert_eq!(cfgs.len(), 2);
let libera = &cfgs[0];
assert_eq!(libera.name, "libera");
assert_eq!(libera.server, "irc.libera.chat");
assert_eq!(libera.nick, "shared"); // inherited
assert_eq!(libera.channels, vec!["#lobby"]); // inherited
assert!(libera.tls);
assert_eq!(libera.port, 6697); // TLS-port convention applied per network
let oftc = &cfgs[1];
assert_eq!(oftc.name, "oftc");
assert_eq!(oftc.server, "irc.oftc.net");
assert_eq!(oftc.nick, "other"); // overridden
assert_eq!(oftc.channels, vec!["#oftc"]); // overridden
assert!(!oftc.tls); // default
}
#[test]
fn a_section_may_rename_itself() {
let cfgs = parse_str("[net]\nname = pretty\nserver = irc.example.org\n");
assert_eq!(cfgs.len(), 1);
assert_eq!(cfgs[0].name, "pretty");
}
#[test]
fn comments_and_blanks_are_ignored() {
// A lone commented/blank file yields the built-in default single network.
let cfgs = parse_str("# comment\n; also\n\n \n");
assert_eq!(cfgs.len(), 1);
assert_eq!(cfgs[0].server, "irc.tchatou.fr"); // built-in default
}