diff --git a/ibis/ibismessage.h b/ibis/ibismessage.h
--- a/ibis/ibismessage.h
+++ b/ibis/ibismessage.h
@@ -173,6 +173,36 @@
 IBIS_AVAILABLE_IN_0_1
 void ibis_message_set_tags(IbisMessage *message, IbisTags *tags);
 
+/**
+ * ibis_message_serialize:
+ * @message: The instance.
+ *
+ * Serializes @message into a [type@GLib.Bytes].
+ *
+ * This output does include the trailing `\r\n`.
+ *
+ * Returns: (transfer full): The serialized message.
+ *
+ * Since: 0.1
+ */
+IBIS_AVAILABLE_IN_0_1
+GBytes *ibis_message_serialize(IbisMessage *message);
+
+/**
+ * ibis_message_serialize_to_string:
+ * @message: The instance.
+ *
+ * Serializes @message into a string.
+ *
+ * This string does include the trailing `\r\n`.
+ *
+ * Returns: (transfer full): The serialized message.
+ *
+ * Since: 0.1
+ */
+IBIS_AVAILABLE_IN_0_1
+char *ibis_message_serialize_to_string(IbisMessage *message);
+
 G_END_DECLS
 
 #endif /* IBIS_MESSAGE_H */
# HG changeset patch
# Node ID 2bafb7b1e7b2
# Parent  5a6bbdc3dcde
diff --git a/ibis/ibismessage.c b/ibis/ibismessage.c
--- a/ibis/ibismessage.c
+++ b/ibis/ibismessage.c
@@ -46,6 +46,72 @@
 };
 
 /******************************************************************************
+ * Serialization
+ *****************************************************************************/
+static inline void
+ibis_message_serialize_add_delimiter(GString *str) {
+	if(str->len > 0) {
+		g_string_append(str, " ");
+	}
+}
+
+static void
+ibis_message_serialize_params(IbisMessage *message, GString *str) {
+	gboolean first = TRUE;
+
+	for(guint i = 0; message->params[i] != NULL; i++) {
+		if(!first) {
+			g_string_append(str, " ");
+		}
+
+		/* If this is the last param we need to prefix it with a :. */
+		if(message->params[i + 1] == NULL) {
+			g_string_append_printf(str, ":%s", message->params[i]);
+		} else {
+			g_string_append(str, message->params[i]);
+		}
+
+		if(first) {
+			first = FALSE;
+		}
+	}
+}
+
+static GString *
+ibis_message_serialize_internal(IbisMessage *message) {
+	GString *str = g_string_new("");
+
+	if(IBIS_IS_TAGS(message->tags)) {
+		char *tag_data = ibis_tags_serialize(message->tags);
+
+		g_string_append(str, tag_data);
+		g_free(tag_data);
+	}
+
+	if(!ibis_str_is_empty(message->source)) {
+		ibis_message_serialize_add_delimiter(str);
+		g_string_append_printf(str, ":%s", message->source);
+	}
+
+	if(!ibis_str_is_empty(message->command)) {
+		ibis_message_serialize_add_delimiter(str);
+		g_string_append(str, message->command);
+	}
+
+	if(message->params != NULL) {
+		ibis_message_serialize_add_delimiter(str);
+		ibis_message_serialize_params(message, str);
+	}
+
+	/* Finally add the trailing \r\n if the string isn't empty. */
+	if(str->len > 0) {
+		g_string_append(str, "\r\n");
+	}
+
+	return str;
+}
+
+/******************************************************************************
  * GObject Implementation
  *****************************************************************************/
 G_DEFINE_FINAL_TYPE(IbisMessage, ibis_message, G_TYPE_OBJECT)
@@ -300,3 +366,17 @@
 		g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_PARAMS]);
 	}
 }
+
+GBytes *
+ibis_message_serialize(IbisMessage *message) {
+	g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL);
+
+	return g_string_free_to_bytes(ibis_message_serialize_internal(message));
+}
+
+char *
+ibis_message_serialize_to_string(IbisMessage *message) {
+	g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL);
+
+	return g_string_free_and_steal(ibis_message_serialize_internal(message));
+}
diff --git a/ibis/tests/test_message.c b/ibis/tests/test_message.c
--- a/ibis/tests/test_message.c
+++ b/ibis/tests/test_message.c
@@ -24,7 +24,7 @@
  * Tests
  *****************************************************************************/
 static void
-test_ibis_message_new_valid(void) {
+test_ibis_message_new(void) {
 	IbisMessage *message = NULL;
 
 	message = ibis_message_new("PRIVMSG");
@@ -35,26 +35,6 @@
 }
 
 static void
-test_ibis_message_new_empty(void) {
-	if(g_test_subprocess()) {
-		ibis_message_new("");
-	}
-
-	g_test_trap_subprocess(NULL, 0, G_TEST_SUBPROCESS_DEFAULT);
-	g_test_trap_assert_stderr("*!ibis_str_is_empty*failed*");
-}
-
-static void
-test_ibis_message_new_null(void) {
-	if(g_test_subprocess()) {
-		ibis_message_new(NULL);
-	}
-
-	g_test_trap_subprocess(NULL, 0, G_TEST_SUBPROCESS_DEFAULT);
-	g_test_trap_assert_stderr("*!ibis_str_is_empty*failed*");
-}
-
-static void
 test_ibis_message_properties(void) {
 	IbisMessage *message = NULL;
 	IbisTags *tags1 = NULL;
@@ -130,20 +110,463 @@
 }
 
 /******************************************************************************
+ * Serialization Tests
+ *****************************************************************************/
+static void
+test_ibis_message_serialize_empty(void) {
+	IbisMessage *message = NULL;
+	char *serialized = NULL;
+
+	message = g_object_new(IBIS_TYPE_MESSAGE, NULL);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==, "");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+}
+
+static void
+test_ibis_message_serialize_source(void) {
+	IbisMessage *message = NULL;
+	char *serialized = NULL;
+
+	message = g_object_new(IBIS_TYPE_MESSAGE, "source", "source", NULL);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==, ":source\r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+}
+
+static void
+test_ibis_message_serialize_command(void) {
+	IbisMessage *message = NULL;
+	char *serialized = NULL;
+
+	message = ibis_message_new("command");
+
+	serialized = ibis_message_serialize_to_string(message);
+	g_assert_cmpstr(serialized, ==, "command\r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+}
+
+static void
+test_ibis_message_serialize_parameters(void) {
+	IbisMessage *message = NULL;
+	char *serialized = NULL;
+
+	message = g_object_new(IBIS_TYPE_MESSAGE, NULL);
+	ibis_message_set_params(message, "foo", "bar", "baz", NULL);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==, "foo bar :baz\r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+}
+
+/******************************************************************************
+ * irc-docs tests
+ *****************************************************************************/
+static void
+test_ibis_message_ircdocs_verb_and_params(void) {
+	IbisMessage *message = NULL;
+	char *serialized = NULL;
+
+	message = ibis_message_new("foo");
+	ibis_message_set_params(message, "bar", "baz", "asdf", NULL);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==, "foo bar baz :asdf\r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+}
+
+static void
+test_ibis_message_ircdocs_source_and_command(void) {
+	IbisMessage *message = NULL;
+	char *serialized = NULL;
+
+	message = ibis_message_new("AWAY");
+	ibis_message_set_source(message, "src");
+
+	serialized = ibis_message_serialize_to_string(message);
+	g_assert_cmpstr(serialized, ==, ":src AWAY\r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+}
+
+static void
+test_ibis_message_ircdocs_source_command_and_trailing_param(void) {
+	IbisMessage *message = NULL;
+	char *serialized = NULL;
+
+	message = ibis_message_new("AWAY");
+	ibis_message_set_source(message, "src");
+	ibis_message_set_params(message, "", NULL);
+
+	serialized = ibis_message_serialize_to_string(message);
+	g_assert_cmpstr(serialized, ==, ":src AWAY :\r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+}
+
+static void
+test_ibis_message_ircdocs_simple_with_source(void) {
+	IbisMessage *message = NULL;
+	char *serialized = NULL;
+
+	message = ibis_message_new("foo");
+	ibis_message_set_source(message, "coolguy");
+	ibis_message_set_params(message, "bar", "baz", "asdf", NULL);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==, ":coolguy foo bar baz :asdf\r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+}
+
+static void
+test_ibis_message_ircdocs_simple_with_trailing_param(void) {
+	IbisMessage *message = NULL;
+	char *serialized = NULL;
+
+	message = ibis_message_new("foo");
+	ibis_message_set_params(message, "bar", "baz", "asdf quux", NULL);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==, "foo bar baz :asdf quux\r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+}
+
+static void
+test_ibis_message_ircdocs_simple_with_empty_trailing_param(void) {
+	IbisMessage *message = NULL;
+	char *serialized = NULL;
+
+	message = ibis_message_new("foo");
+	ibis_message_set_params(message, "bar", "baz", "", NULL);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==, "foo bar baz :\r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+}
+
+static void
+test_ibis_message_ircdocs_simple_with_empty_trailing_param_with_colon(void) {
+	IbisMessage *message = NULL;
+	char *serialized = NULL;
+
+	message = ibis_message_new("foo");
+	ibis_message_set_params(message, "bar", "baz", ":asdf", NULL);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==, "foo bar baz ::asdf\r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+}
+
+static void
+test_ibis_message_ircdocs_simple_with_source_and_trailing_param(void) {
+	IbisMessage *message = NULL;
+	char *serialized = NULL;
+
+	message = ibis_message_new("foo");
+	ibis_message_set_source(message, "coolguy");
+	ibis_message_set_params(message, "bar", "baz", "asdf quux", NULL);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==, ":coolguy foo bar baz :asdf quux\r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+}
+
+static void
+test_ibis_message_ircdocs_simple_with_source_and_trailing_param_with_whitespace(void)
+{
+	IbisMessage *message = NULL;
+	char *serialized = NULL;
+
+	message = ibis_message_new("foo");
+	ibis_message_set_source(message, "coolguy");
+	ibis_message_set_params(message, "bar", "baz", "  asdf quux ", NULL);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==, ":coolguy foo bar baz :  asdf quux \r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+}
+
+static void
+test_ibis_message_ircdocs_trailing_containing_another(void) {
+	IbisMessage *message = NULL;
+	char *serialized = NULL;
+
+	message = ibis_message_new("PRIVMSG");
+	ibis_message_set_source(message, "coolguy");
+	ibis_message_set_params(message, "bar", "lol :)", NULL);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==, ":coolguy PRIVMSG bar :lol :)\r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+}
+
+static void
+test_ibis_message_ircdocs_simple_with_source_and_empty_trailing_param(void) {
+	IbisMessage *message = NULL;
+	char *serialized = NULL;
+
+	message = ibis_message_new("foo");
+	ibis_message_set_source(message, "coolguy");
+	ibis_message_set_params(message, "bar", "baz", "", NULL);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==, ":coolguy foo bar baz :\r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+}
+
+static void
+test_ibis_message_ircdocs_simple_with_source_and_trailing_param_only_whitespace(void)
+{
+	IbisMessage *message = NULL;
+	char *serialized = NULL;
+
+	message = ibis_message_new("foo");
+	ibis_message_set_source(message, "coolguy");
+	ibis_message_set_params(message, "bar", "baz", "  ", NULL);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==, ":coolguy foo bar baz :  \r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+}
+
+static void
+test_ibis_message_ircdocs_param_contains_tab(void) {
+	IbisMessage *message = NULL;
+	char *serialized = NULL;
+
+	message = ibis_message_new("foo");
+	ibis_message_set_source(message, "coolguy");
+	ibis_message_set_params(message, "b\tar", "baz", NULL);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==, ":coolguy foo b\tar :baz\r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+}
+
+static void
+test_ibis_message_ircdocs_empty_tag_and_space_trailing(void) {
+	IbisMessage *message = NULL;
+	IbisTags *tags = NULL;
+	char *serialized = NULL;
+
+	tags = ibis_tags_new();
+	ibis_tags_add(tags, "asd", "");
+
+	message = ibis_message_new("foo");
+	ibis_message_set_tags(message, tags);
+	ibis_message_set_source(message, "coolguy");
+	ibis_message_set_params(message, "bar", "baz", "  ", NULL);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==, "@asd :coolguy foo bar baz :  \r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+	g_assert_finalize_object(tags);
+}
+
+static void
+test_ibis_message_ircdocs_missing_tag_and_space_trailing(void) {
+	IbisMessage *message = NULL;
+	IbisTags *tags = NULL;
+	char *serialized = NULL;
+
+	tags = ibis_tags_new();
+	ibis_tags_add(tags, "asd", NULL);
+
+	message = ibis_message_new("foo");
+	ibis_message_set_tags(message, tags);
+	ibis_message_set_source(message, "coolguy");
+	ibis_message_set_params(message, "bar", "baz", "  ", NULL);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==, "@asd :coolguy foo bar baz :  \r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+	g_assert_finalize_object(tags);
+}
+
+static void
+test_ibis_message_ircdocs_tags_with_escaped_values(void) {
+	IbisMessage *message = NULL;
+	IbisTags *tags = NULL;
+	char *serialized = NULL;
+
+	tags = ibis_tags_new();
+	ibis_tags_add(tags, "a", "b\\and\nk");
+	ibis_tags_add(tags, "d", "gh;764");
+
+	message = ibis_message_new("foo");
+	ibis_message_set_tags(message, tags);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==, "@a=b\\\\and\\nk;d=gh\\:764 foo\r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+	g_assert_finalize_object(tags);
+}
+
+static void
+test_ibis_message_ircdocs_tags_with_escaped_values_and_params(void) {
+	IbisMessage *message = NULL;
+	IbisTags *tags = NULL;
+	char *serialized = NULL;
+
+	tags = ibis_tags_new();
+	ibis_tags_add(tags, "a", "b\\and\nk");
+	ibis_tags_add(tags, "d", "gh;764");
+
+	message = ibis_message_new("foo");
+	ibis_message_set_tags(message, tags);
+	ibis_message_set_params(message, "par1", "par2", NULL);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==,
+	                "@a=b\\\\and\\nk;d=gh\\:764 foo par1 :par2\r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+	g_assert_finalize_object(tags);
+}
+
+static void
+test_ibis_message_ircdocs_tags_with_long_strange_values(void) {
+	IbisMessage *message = NULL;
+	IbisTags *tags = NULL;
+	char *serialized = NULL;
+
+	tags = ibis_tags_new();
+	ibis_tags_add(tags, "foo", "\\\\;\\s \r\n");
+
+	message = ibis_message_new("COMMAND");
+	ibis_message_set_tags(message, tags);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==,
+	                "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND\r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+	g_assert_finalize_object(tags);
+}
+
+/******************************************************************************
  * Main
  *****************************************************************************/
 int
 main(int argc, char *argv[]) {
 	g_test_init(&argc, &argv, NULL);
 
-	g_test_add_func("/ibis/message/new/valid", test_ibis_message_new_valid);
-	g_test_add_func("/ibis/message/new/empty", test_ibis_message_new_empty);
-	g_test_add_func("/ibis/message/new/null", test_ibis_message_new_null);
+	g_test_add_func("/ibis/message/new", test_ibis_message_new);
 
 	g_test_add_func("/ibis/message/properties", test_ibis_message_properties);
 
 	g_test_add_func("/ibis/message/set-params-variadic",
 	                test_ibis_message_set_params_variadic);
 
+	g_test_add_func("/ibis/message/serialize/empty",
+	                test_ibis_message_serialize_empty);
+	g_test_add_func("/ibis/message/serialize/source",
+	                test_ibis_message_serialize_source);
+	g_test_add_func("/ibis/message/serialize/command",
+	                test_ibis_message_serialize_command);
+	g_test_add_func("/ibis/message/serialize/parameters",
+	                test_ibis_message_serialize_parameters);
+
+	/* These tests are based on the msg-join tests from
+	 * https://github.com/ircdocs/parser-tests/blob/master/tests/msg-join.yaml
+	 */
+	g_test_add_func("/ibis/message/ircdocs/verb-and-params",
+	                test_ibis_message_ircdocs_verb_and_params);
+	g_test_add_func("/ibis/message/ircdocs/source-and-command",
+	                test_ibis_message_ircdocs_source_and_command);
+	g_test_add_func("/ibis/message/ircdocs/source-command-and-trailing-param",
+	                test_ibis_message_ircdocs_source_command_and_trailing_param);
+	g_test_add_func("/ibis/message/ircdocs/simple-with-source",
+	                test_ibis_message_ircdocs_simple_with_source);
+	g_test_add_func("/ibis/message/ircdocs/simple-with-trailing-param",
+	                test_ibis_message_ircdocs_simple_with_trailing_param);
+	g_test_add_func("/ibis/message/ircdocs/simple-with-empty-trailing-param",
+	                test_ibis_message_ircdocs_simple_with_empty_trailing_param);
+	g_test_add_func("/ibis/message/ircdocs/simple-with-empty-trailing-param-with-colon",
+	                test_ibis_message_ircdocs_simple_with_empty_trailing_param_with_colon);
+	g_test_add_func("/ibis/message/ircdocs/simple-with-source-and-trailing-param",
+	                test_ibis_message_ircdocs_simple_with_source_and_trailing_param);
+	g_test_add_func("/ibis/message/ircdocs/simple-with-source-and-trailing-param-with-whitespace",
+	                test_ibis_message_ircdocs_simple_with_source_and_trailing_param_with_whitespace);
+	g_test_add_func("/ibis/message/ircdocs/trailing-containing-another",
+	                test_ibis_message_ircdocs_trailing_containing_another);
+	g_test_add_func("/ibis/message/ircdocs/simple-with-source-and-empty-trailing-param",
+	                test_ibis_message_ircdocs_simple_with_source_and_empty_trailing_param);
+	g_test_add_func("/ibis/message/ircdocs/simple-with-source-and-trailing-param-only-whitespace",
+	                test_ibis_message_ircdocs_simple_with_source_and_trailing_param_only_whitespace);
+	g_test_add_func("/ibis/message/ircdocs/param-contains-tab",
+	                test_ibis_message_ircdocs_param_contains_tab);
+	g_test_add_func("/ibis/message/ircdocs/empty_tag_and_space_trailing",
+	                test_ibis_message_ircdocs_empty_tag_and_space_trailing);
+	g_test_add_func("/ibis/message/ircdocs/missing_tag_and_space_trailing",
+	                test_ibis_message_ircdocs_missing_tag_and_space_trailing);
+	g_test_add_func("/ibis/message/ircdocs/tags_with_escaped_values",
+	                test_ibis_message_ircdocs_tags_with_escaped_values);
+	g_test_add_func("/ibis/message/ircdocs/tags_with_escaped_values_and_params",
+	                test_ibis_message_ircdocs_tags_with_escaped_values_and_params);
+	g_test_add_func("/ibis/message/ircdocs/tag_with_long_strange_values",
+	                test_ibis_message_ircdocs_tags_with_long_strange_values);
+
 	return g_test_run();
 }
