I wrote a parser for the IRC RFC 2812 spec and would like input on my use of the strategy pattern for this.
I created an IrcMessage
class that is responsible for taking the server response string from the (examples below) and parsing it. The server response format is:
:<Prefix> <Command> <Space Separated Args> :Optional trailing content here, like a chat message.
Examples:
:[email protected] PRIVMSG #mychannel :Hello everyone!
:[email protected] 301 Bob :Away playing a game
:Scionwes[email protected] 305 :You are no longer marked as away
This class takes the string and passes it to a MessagePrefix
, MessageTrail
and MessageCommand
class. Each parsing the string and building their respective component.
/// <summary>
/// Breaks an IRC server response down in to a prefix, command w/ parameters and an optional trail.
/// Since the response format is always :<prefix> <Command> <arg, arg, arg ...> :trailing content
/// the Parser can easily break the response down in to objects representing each component of
/// the message.
/// </summary>
public class IrcMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="IrcMessage"/> class.
/// </summary>
/// <param name="message">The message.</param>
public IrcMessage(string message)
{
this.OriginalMessage = message;
this.PrefixMessage = new MessagePrefix(message);
this.TrailMessage = new MessageTrail(message);
this.CommandMessage = new MessageCommand(message, this.PrefixMessage, this.TrailMessage);
}
/// <summary>
/// Gets the original message sent from the server.
/// </summary>
public string OriginalMessage { get; private set; }
/// <summary>
/// Gets the prefix of the response.
/// </summary>
public MessagePrefix PrefixMessage { get; private set; }
/// <summary>
/// Gets the trailing message. Not all responses come with trailing content.
/// </summary>
public MessageTrail TrailMessage { get; private set; }
/// <summary>
/// Gets the command and its parameters sent from the server.
/// A command will always be given; parameters might be empty.
/// </summary>
public MessageCommand CommandMessage { get; private set; }
}
/// <summary>
/// Parses a server response for a prefix and stores it.
/// </summary>
public class MessagePrefix
{
/// <summary>
/// The original message
/// </summary>
private string originalMessage;
/// <summary>
/// Initializes a new instance of the <see cref="MessagePrefix"/> class.
/// </summary>
/// <param name="message">The message.</param>
public MessagePrefix(string message)
{
this.originalMessage = message;
if (!this.IsPrefixed)
{
return;
}
// Prefix always starts with a colon and ends with a space.
// :<Prefix> <Command>
this.EndIndex = message.IndexOf(" ");
this.Prefix = message.Substring(1, this.EndIndex - 1);
}
/// <summary>
/// Gets a value indicating whether the original message contained a properly formatted prefixed.
/// </summary>
public bool IsPrefixed
{
get
{
return this.originalMessage.StartsWith(":");
}
}
/// <summary>
/// Gets the index at the end of the prefix.
/// </summary>
public int EndIndex { get; private set; }
/// <summary>
/// Gets the prefix of the message. This is all text after the initial colon and before the first space.
/// </summary>
public string Prefix { get; private set; }
}
/// <summary>
/// Parses an IRC server response for the command and its arguments.
/// </summary>
public class MessageCommand
{
/// <summary>
/// Initializes a new instance of the <see cref="MessageCommand"/> class.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="prefix">The prefix.</param>
/// <param name="trail">The trail.</param>
public MessageCommand(string message, MessagePrefix prefix, MessageTrail trail)
{
// Remove all of the content after the Prefix but before the Trail.
// That is our Command and it's associated arguments.
// Formatted as
// :<Prefix> <Command> <space separated args> :<Trail>
string[] commandAndParameters = message
.Substring(prefix.EndIndex + 1, trail.TrailStart - prefix.EndIndex - 1)
.Split(' ');
// First item is always the command.
this.Command = commandAndParameters[0];
// If the command as args, then they compose the rest of the collection.
if (commandAndParameters.Length > 1)
{
this.Parameters = commandAndParameters.Skip(1);
}
}
/// <summary>
/// Gets the command sent by the server.
/// </summary>
public string Command { get; private set; }
/// <summary>
/// Gets the parameters associated with the command, if any..
/// </summary>
public IEnumerable<string> Parameters { get; private set; }
}
/// <summary>
/// Parses an IRC server response for trailing content.
/// </summary>
public class MessageTrail
{
/// <summary>
/// Initializes a new instance of the <see cref="MessageTrail"/> class.
/// </summary>
/// <param name="message">The message.</param>
public MessageTrail(string message)
{
// Find the index of the closing colon used to mark the end of the
// message meta and start of the trailing content.
// This is always the first space, followed by a colon in the response.
this.TrailStart = message.IndexOf(" :");
if (this.TrailStart >= 0)
{
// Set the trailing content to the location were the trailing content begins.
// The +2 is needed to begin after the ' :' index.
this.TrailingContent = message.Substring(this.TrailStart + 2);
}
else
{
this.TrailStart = message.Length;
}
this.HasTrail = this.TrailStart > 0 && this.TrailStart < message.Length;
}
/// <summary>
/// Gets a value indicating whether the server response has trailing content.
/// </summary>
public bool HasTrail { get; private set; }
/// <summary>
/// Gets the trailing content of the response.
/// </summary>
public string TrailingContent { get; private set; } = string.Empty;
/// <summary>
/// Gets the string index from the original message where the trailing content starts.
/// </summary>
public int TrailStart { get; private set; }
}
The following is my ICommandResponse, which is what I am using as my strategy pattern object. Each implementation of this correlates to a server code (or series of codes/commands) that can be processed with a result given back for the client UI to consume.
/// <summary>
/// Process a command sent from the server.
/// </summary>
public interface ICommandResponse
{
/// <summary>
/// Gets the command or Reply codes associated with this response.
/// </summary>
IEnumerable<string> CommandCodes { get; }
/// <summary>
/// Processes the command.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="connection">The connection.</param>
/// <param name="userInfo">The user information.</param>
/// <param name="args">The arguments.</param>
/// <returns>
/// Returns the output result of the process.
/// </returns>
string ProcessCommand(IrcMessage message, IClientConnection connection);
}
I take the content sent to me from the server and pass it to an IrcMessage
instance via its constructor. I then pass that instance to a ProcessMessage
method that finds an ICommandResponse
object that can handle the command code that the server sent. I don't include the CommandResponseFactory
code as it just searches for ICommandResponses
that have the command given within the ICommandResponse.CommandCodes
collection and returns the correct one.
private void ProcessMessage(IrcMessage message)
{
ICommandResponse response = CommandResponseFactory.GetResponseForCommand(message.CommandMessage.Command);
// temporary: we will just dump the message to the client.
if (response == null)
{
this.OnMessageReceived(message.OriginalMessage);
return;
}
string result = response.ProcessCommand(message, this);
if (message.CommandMessage.Command.Equals("001"))
{
this.OnConnected(result);
}
else
{
this.OnMessageReceived(result);
}
}
I've seen most people handle the response codes in a giant Switch(responseCode) statement. I didn't want to take that approach. With this, I can now take each one of the response codes that the server sends me, and handle them as objects.
A couple examples, the first being handling the Message of the Day response codes.
/// <summary>
/// Processes all incoming message of the day responses from the server
/// </summary>
internal class MessageOfTheDayReplies : ICommandResponse
{
/// <summary>
/// Gets the command or Reply codes associated with this response.
/// </summary>
public IEnumerable<string> CommandCodes { get; private set; } = new List<string> { "375", "376", "372" };
/// <summary>
/// Processes the command.
/// </summary>
/// <param name="command">The command.</param>
/// <param name="code">The code.</param>
/// <param name="connection">The connection.</param>
/// <param name="userInfo">The user information.</param>
/// <param name="args">The arguments.</param>
/// <returns>Returns back messages associated with the message of the day.</returns>
public string ProcessCommand(IrcMessage message, IClientConnection connection)
{
/* Expected server responses for each supported response code.
375 RPL_MOTDSTART
":- <server> Message of the day - "
372 RPL_MOTD
":- <text>"
376 RPL_ENDOFMOTD
":End of MOTD command"
When responding to the MOTD message and the MOTD file
is found, the file is displayed line by line, with
each line no longer than 80 characters, using
*/
if (message.CommandMessage.Command.Equals("375"))
{
return message.TrailMessage.HasTrail
? message.TrailMessage.TrailingContent
: "<\{connection.ServerInformation.Url}> Message of the Day - Begin";
}
else if (message.CommandMessage.Command.Equals("376"))
{
return message.TrailMessage.HasTrail
? message.TrailMessage.TrailingContent
: "<\{connection.ServerInformation.Url}> Message of the Day - End";
}
else
{
return message.TrailMessage.TrailingContent;
}
}
}
The 2nd being handling away replies.
/// <summary>
/// Processes away responses from the server
/// </summary>
internal class AwayReplies : ICommandResponse
{
/// <summary>
/// Gets the command or Reply codes associated with this response.
/// </summary>
public IEnumerable<string> CommandCodes { get; private set; } = new List<string> { "301", "305", "306" };
/// <summary>
/// Processes the command.
/// </summary>
/// <param name="command">The command.</param>
/// <param name="code">The code.</param>
/// <param name="connection">The connection.</param>
/// <param name="userInfo">The user information.</param>
/// <param name="args">The arguments.</param>
/// <returns>Returns the away message for the user messaged.</returns>
public string ProcessCommand(IrcMessage message, IClientConnection connection)
{
/* Expected server responses for each supported response code.
301 RPL_AWAY
"<nick> :<away message>"
305 RPL_UNAWAY
":You are no longer marked as being away"
306 RPL_NOWAWAY
":You have been marked as being away"
These replies are used with the AWAY command (if allowed).
RPL_AWAY is sent to any client sending a PRIVMSG to a client which is away.
RPL_AWAY is only sent by the server to which the client is connected.
Replies RPL_UNAWAY and RPL_NOWAWAY are sent when the
client removes and sets an AWAY message.
*/
string response = string.Empty;
if (message.CommandMessage.Command.Equals("301"))
{
var user = message.CommandMessage.Parameters.First();
response = message.TrailMessage.HasTrail
? "\{user} is away with message '\{message.TrailMessage.TrailingContent}'"
: "\{user} is away";
}
else if (message.CommandMessage.Command.Equals("305"))
{
response = message.TrailMessage.HasTrail
? message.TrailMessage.TrailingContent
: "You are no longer marked as being away.";
}
else if (message.CommandMessage.Command.Equals("306"))
{
response = message.TrailMessage.HasTrail
? message.TrailMessage.TrailingContent
: "You have been marked as being away.";
}
return response;
}
}
I wrote a unit test that checks to ensure the parser parsed the data properly. I've got a few other tests for different string combinations, but this one was covered all of the IrcMessage in a single test for a complete response string.
[TestMethod]
public void Parse_with_trailing_content()
{
// Arrange
string prefix = ":irc.localhost.localdomain";
string commandAndArgs = "433 Scionwest";
string trailingContent = " :Nickname is already in use";
// Act
var parser = new IrcMessage("\{prefix} \{commandAndArgs} \{trailingContent}");
// Assert
// We +2 the expected value to account for spaces used in the string formatting.
Assert.AreEqual(prefix.Length + commandAndArgs.Length + 2, parser.TrailMessage.TrailStart, "Trailing index was not found.");
Assert.IsTrue(parser.TrailMessage.TrailingContent.Equals(trailingContent.Substring(2)), "Trailing content not parsed.");
Assert.AreEqual(prefix.Length, parser.PrefixMessage.EndIndex, "Prefix ending index not found.");
Assert.IsTrue(parser.PrefixMessage.Prefix.Equals(prefix.Substring(1)), "Prefix was not equal");
Assert.IsTrue(parser.CommandMessage.Command.Equals(commandAndArgs.Split(' ').First()), "Command was not parsed.");
Assert.IsTrue(parser.CommandMessage.Parameters.First().Equals(commandAndArgs.Split(' ').Last()), "Command args not parsed.");
}