# HG changeset patch
# Node ID 9934f909cb92
# Parent  02886594438d
diff --git a/ibis/ibisclient.c b/ibis/ibisclient.c
--- a/ibis/ibisclient.c
+++ b/ibis/ibisclient.c
@@ -39,6 +39,12 @@
 };
 static GParamSpec *properties[N_PROPERTIES] = {NULL, };
 
+enum {
+	SIG_MESSAGE,
+	N_SIGNALS,
+};
+static guint signals[N_SIGNALS] = {0, };
+
 struct _IbisClient {
 	GObject parent;
 
@@ -118,11 +124,11 @@
 static void
 ibis_client_read_cb(GObject *source, GAsyncResult *result, gpointer data) {
 	IbisClient *client = data;
+	IbisMessage *message = NULL;
 	GDataInputStream *input = G_DATA_INPUT_STREAM(source);
 	GError *error = NULL;
 	gchar *line = NULL;
 	gsize length;
-	gboolean parsed = FALSE;
 
 	line = g_data_input_stream_read_line_finish(input, result, &length,
 	                                            &error);
@@ -144,14 +150,36 @@
 		return;
 	}
 
-	parsed = ibis_parser_parse(client->parser, line, &error, client);
-	if(!parsed) {
+	message = ibis_parser_parse(client->parser, line, &error);
+	if(error != NULL) {
 		g_warning("failed to handle '%s': %s", line,
 		          error != NULL ? error->message : "unknown error");
+
+		g_clear_error(&error);
+	} else if(IBIS_IS_MESSAGE(message)) {
+		char *upper_command = NULL;
+		const char *command = NULL;
+		gboolean handled = FALSE;
+
+		command = ibis_message_get_command(message);
+		upper_command = g_utf8_strup(command, -1);
+
+		g_signal_emit(client, signals[SIG_MESSAGE],
+		              g_quark_from_string(upper_command), upper_command,
+		              message, &handled);
+
+		g_clear_pointer(&upper_command, g_free);
+
+		if(!handled) {
+			g_warning("unhandled message: '%s'", line);
+		}
+	} else {
+		g_warning("parsing failed to generate a message or error for '%s'",
+		          line);
 	}
-	g_clear_error(&error);
 
-	g_free(line);
+	g_clear_pointer(&line, g_free);
+	g_clear_object(&message);
 
 	/* Call read_line_async again to continue reading lines. */
 	g_data_input_stream_read_line_async(client->input,
@@ -289,6 +317,35 @@
 		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
 
 	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+
+	/**
+	 * IbisClient::message:
+	 * @client: The instance.
+	 * @command: The command.
+	 * @message: The message.
+	 *
+	 * Emitted when the client has received a new message.
+	 *
+	 * This signal supports *details* based on the upper case version of the
+	 * command from the message. For example, `PRIVMSG`, `NOTICE`, etc.
+	 *
+	 * Returns: %TRUE to stop other handlers from being invoked, otherwise
+	 *          %FALSE to propagate further.
+	 *
+	 * Since: 0.1
+	 */
+	signals[SIG_MESSAGE] = g_signal_new_class_handler(
+		"message",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_DETAILED | G_SIGNAL_RUN_LAST,
+		NULL,
+		g_signal_accumulator_true_handled,
+		NULL,
+		NULL,
+		G_TYPE_BOOLEAN,
+		2,
+		G_TYPE_STRING,
+		IBIS_TYPE_MESSAGE);
 }
 
 /******************************************************************************
diff --git a/ibis/ibisparser.h b/ibis/ibisparser.h
--- a/ibis/ibisparser.h
+++ b/ibis/ibisparser.h
@@ -32,19 +32,15 @@
 G_BEGIN_DECLS
 
 /**
- * IbisMessageHandler:
- * @message: The [class@Message] to handle.
- * @error: (nullable): A return address for a [type@GLib.Error].
- * @data: The user data that was passed to [method@Parser.parse].
+ * IBIS_PARSER_ERROR:
  *
- * Defines the type of a function that will be called to handle @message.
- *
- * Returns: %TRUE if the command was handled properly, otherwise %FALSE and
- *          @error may be set.
+ * A GError domain for the Ibis parser.
  *
  * Since: 0.1
  */
-typedef gboolean (*IbisMessageHandler)(IbisMessage *message, GError **error, gpointer data);
+#define IBIS_PARSER_ERROR \
+	(g_quark_from_static_string("ibis-parser")) \
+	IBIS_AVAILABLE_MACRO_IN_0_1
 
 #define IBIS_TYPE_PARSER (ibis_parser_get_type())
 
@@ -72,60 +68,25 @@
 IbisParser *ibis_parser_new(void);
 
 /**
- * ibis_parser_set_fallback_handler: (skip):
- * @parser: The instance.
- * @handler: (scope forever): A [callback@Ibis.MessageHandler].
- *
- * Sets @handler to be called for any messages that @parser doesn't know how to
- * handle.
- *
- * Since: 0.1
- */
-IBIS_AVAILABLE_IN_0_1
-void ibis_parser_set_fallback_handler(IbisParser *parser, IbisMessageHandler handler);
-
-/**
  * ibis_parser_parse:
  * @parser: The instance.
  * @buffer: The buffer to parse.
  * @error: Return address for a #GError, or %NULL.
- * @data: (nullable): Optional data to pass to the handler.
  *
  * Parses @buffer with @parser.
  *
- * Returns: %TRUE if the buffer was parsed correctly or %FALSE with @error set.
+ * @buffer should just be a single %NULL terminated string with the `\r\n`
+ * already removed.
+ *
+ * If an error is encountered @error will be set and %NULL will be returned.
+ *
+ * Returns: (transfer full) (nullable): The parsed message or %NULL if an error
+ *          occurred.
  *
  * Since: 0.1
  */
 IBIS_AVAILABLE_IN_0_1
-gboolean ibis_parser_parse(IbisParser *parser, const gchar *buffer, GError **error, gpointer data);
-
-/**
- * ibis_parser_add_handler:
- * @parser: The instance.
- * @command: The command string.
- * @handler: (scope forever): The handler to call.
- *
- * Calls @handler every time @parser finds the command named @command.
- *
- * Since: 0.1
- */
-IBIS_AVAILABLE_IN_0_1
-void ibis_parser_add_handler(IbisParser *parser, const char *command, IbisMessageHandler handler);
-
-/**
- * ibis_parser_add_handlers:
- * @parser: The instance.
- * @handler: (scope forever): The handler to call when the command is received.
- * @...: A %NULL terminated list of string command names.
- *
- * Like [method@Parser.add_handler] but allows you to add multiple commands at
- * once that share a handler.
- *
- * Since: 0.1
- */
-IBIS_AVAILABLE_IN_0_1
-void ibis_parser_add_handlers(IbisParser *parser, IbisMessageHandler handler, ...) G_GNUC_NULL_TERMINATED;
+IbisMessage *ibis_parser_parse(IbisParser *parser, const gchar *buffer, GError **error);
 
 G_END_DECLS
 
diff --git a/ibis/ibisparser.c b/ibis/ibisparser.c
--- a/ibis/ibisparser.c
+++ b/ibis/ibisparser.c
@@ -31,9 +31,6 @@
 	GObject parent;
 
 	GRegex *regex_message;
-
-	IbisMessageHandler fallback_handler;
-	GHashTable *handlers;
 };
 
 G_DEFINE_FINAL_TYPE(IbisParser, ibis_parser, G_TYPE_OBJECT)
@@ -108,19 +105,6 @@
 }
 
 /******************************************************************************
- * Handlers
- *****************************************************************************/
-static gboolean
-ibis_fallback_handler(IbisMessage *message, GError **error,
-                      G_GNUC_UNUSED gpointer data)
-{
-	g_set_error(error, IBIS_DOMAIN, 0, "no handler for command %s",
-	            ibis_message_get_command(message));
-
-	return FALSE;
-}
-
-/******************************************************************************
  * GObject Implementation
  *****************************************************************************/
 static void
@@ -129,8 +113,6 @@
 
 	g_clear_pointer(&parser->regex_message, g_regex_unref);
 
-	g_hash_table_destroy(parser->handlers);
-
 	G_OBJECT_CLASS(ibis_parser_parent_class)->finalize(obj);
 }
 
@@ -143,10 +125,6 @@
 	                                     "(?<coda> +:(?<trailing>.*)?)?",
 	                                     0, 0, NULL);
 	g_assert(parser->regex_message != NULL);
-
-	parser->fallback_handler = ibis_fallback_handler;
-	parser->handlers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
-	                                         NULL);
 }
 
 static void
@@ -164,26 +142,13 @@
 	return g_object_new(IBIS_TYPE_PARSER, NULL);
 }
 
-void
-ibis_parser_set_fallback_handler(IbisParser *parser,
-                                 IbisMessageHandler handler)
-{
-	g_return_if_fail(IBIS_IS_PARSER(parser));
-
-	parser->fallback_handler = handler;
-}
-
-gboolean
-ibis_parser_parse(IbisParser *parser, const char *buffer, GError **error,
-                  gpointer data)
-{
+IbisMessage *
+ibis_parser_parse(IbisParser *parser, const char *buffer, GError **error) {
 	IbisMessage *message = NULL;
-	IbisMessageHandler handler = NULL;
 	IbisTags *tags = NULL;
 	GError *local_error = NULL;
 	GMatchInfo *info = NULL;
 	GStrv params = NULL;
-	gpointer func = NULL;
 	char *coda = NULL;
 	char *command = NULL;
 	char *middle = NULL;
@@ -191,7 +156,6 @@
 	char *tags_string = NULL;
 	char *trailing = NULL;
 	gboolean matches = FALSE;
-	gboolean result = FALSE;
 
 	g_return_val_if_fail(IBIS_IS_PARSER(parser), FALSE);
 	g_return_val_if_fail(buffer != NULL, FALSE);
@@ -199,33 +163,16 @@
 	/* Check if the buffer matches our regex for messages. */
 	matches = g_regex_match(parser->regex_message, buffer, 0, &info);
 	if(!matches) {
-		g_set_error(error, IBIS_DOMAIN, 0,
+		g_match_info_unref(info);
+
+		g_set_error(error, IBIS_PARSER_ERROR, 0,
 		            "failed to parser buffer '%s'", buffer);
 
-		g_match_info_unref(info);
-
-		return FALSE;
+		return NULL;
 	}
 
 	/* Extract the command from the buffer, so we can find the handler. */
 	command = g_match_info_fetch_named(info, "command");
-	func = g_hash_table_lookup(parser->handlers, command);
-	if(func == NULL) {
-		if(parser->fallback_handler == NULL) {
-			g_set_error(error, IBIS_DOMAIN, 0,
-			            "no handler found for command %s and no default "
-			            "handler set.", command);
-
-			g_free(command);
-			g_match_info_unref(info);
-
-			return FALSE;
-		}
-
-		handler = parser->fallback_handler;
-	} else {
-		handler = (IbisMessageHandler)func;
-	}
 
 	/* If we made it this far, we have our handler, so lets get the rest of the
 	 * parameters and call the handler.
@@ -240,11 +187,10 @@
 
 			g_free(tags_string);
 			g_free(command);
-			g_clear_object(&message);
 			g_clear_object(&tags);
 			g_match_info_unref(info);
 
-			return FALSE;
+			return NULL;
 		}
 	}
 	g_free(tags_string);
@@ -269,47 +215,15 @@
 		ibis_message_set_paramsv(message, params);
 	}
 
+	/* Clean up everything */
 	g_free(command);
 	g_free(middle);
 	g_free(coda);
 	g_free(trailing);
 	g_strfreev(params);
 
-	/* Call the handler. */
-	result = handler(message, error, data);
-
 	/* Cleanup the left overs. */
 	g_match_info_unref(info);
-	g_clear_object(&message);
 
-	return result;
-}
-
-void
-ibis_parser_add_handler(IbisParser *parser, const char *command,
-                        IbisMessageHandler handler)
-{
-	g_return_if_fail(IBIS_IS_PARSER(parser));
-	g_return_if_fail(command != NULL);
-	g_return_if_fail(handler != NULL);
-
-	g_hash_table_insert(parser->handlers, g_strdup(command), handler);
+	return message;
 }
-
-void
-ibis_parser_add_handlers(IbisParser *parser, IbisMessageHandler handler, ...) {
-	va_list vargs;
-	const char *command = NULL;
-
-	g_return_if_fail(IBIS_IS_PARSER(parser));
-	g_return_if_fail(handler != NULL);
-
-	va_start(vargs, handler);
-
-	while((command = va_arg(vargs, const char *)) != NULL) {
-		ibis_parser_add_handler(parser, command, handler);
-	}
-
-	va_end(vargs);
-}
-
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
@@ -20,6 +20,8 @@
 
 #include <ibis.h>
 
+#define TEST_IBIS_MESSAGE_ERROR (g_quark_from_static_string("test-ibis-message"))
+
 /******************************************************************************
  * Tests
  *****************************************************************************/
@@ -56,37 +58,37 @@
 
 	message = g_object_new(
 		IBIS_TYPE_MESSAGE,
-		"tags", tags1,
-		"source", "source",
 		"command", "command",
 		"params", params1,
+		"source", "source",
+		"tags", tags1,
 		NULL);
 
 	g_assert_true(IBIS_IS_MESSAGE(message));
 
 	g_object_get(
 		message,
-		"tags", &tags2,
-		"source", &source,
 		"command", &command,
 		"params", &params2,
+		"source", &source,
+		"tags", &tags2,
 		NULL);
 
-	g_assert_nonnull(tags2);
-	g_assert_true(ibis_tags_equal(tags1, tags2));
-	g_clear_object(&tags2);
-
-	g_assert_cmpstr(source, ==, "source");
-	g_clear_pointer(&source, g_free);
-
 	g_assert_cmpstr(command, ==, "command");
 	g_clear_pointer(&command, g_free);
 
 	g_assert_cmpstrv(params2, params1);
 	g_clear_pointer(&params2, g_strfreev);
 
+	g_assert_cmpstr(source, ==, "source");
+	g_clear_pointer(&source, g_free);
+
+	g_assert_nonnull(tags2);
+	g_assert_true(ibis_tags_equal(tags1, tags2));
+	g_clear_object(&tags2);
+
+	g_clear_pointer(&params1, g_strfreev);
 	g_clear_object(&tags1);
-	g_clear_pointer(&params1, g_strfreev);
 	g_assert_finalize_object(message);
 }
 
diff --git a/ibis/tests/test_parser.c b/ibis/tests/test_parser.c
--- a/ibis/tests/test_parser.c
+++ b/ibis/tests/test_parser.c
@@ -31,26 +31,29 @@
 } TestIbisParserData;
 
 /******************************************************************************
- * Handlers
+ * Helpers
  *****************************************************************************/
-static gboolean
-test_ibis_test_handler(IbisMessage *message,
-                       G_GNUC_UNUSED GError **error,
-                       gpointer data)
-{
-	TestIbisParserData *d = data;
+static void
+test_ibis_parser(const gchar *raw, TestIbisParserData *d) {
+	IbisMessage *message = NULL;
+	IbisParser *parser = NULL;
+	GError *error = NULL;
 	GStrv params = NULL;
 	const char *command = NULL;
 	const char *source = NULL;
 	guint n_params = 0;
 
-	command = ibis_message_get_command(message);
-	params = ibis_message_get_params(message);
-	source = ibis_message_get_source(message);
+	parser = ibis_parser_new();
+
+	message = ibis_parser_parse(parser, raw, &error);
+	g_assert_no_error(error);
+	g_assert_true(IBIS_IS_MESSAGE(message));
 
-	if(params != NULL) {
-		n_params = g_strv_length(params);
-	}
+	command = ibis_message_get_command(message);
+	g_assert_cmpstr(command, ==, d->command);
+
+	source = ibis_message_get_source(message);
+	g_assert_cmpstr(source, ==, d->source);
 
 	/* Make sure we have an expected tags hash table before checking them. */
 	if(d->tags != NULL) {
@@ -61,7 +64,12 @@
 		g_assert_true(ibis_tags_equal(d->tags, tags));
 	}
 
-	/* Walk through the params checking against the expected values. */
+	/* Check the parameters to make sure they match. */
+	params = ibis_message_get_params(message);
+	if(params != NULL) {
+		n_params = g_strv_length(params);
+	}
+
 	if(d->n_params > 0) {
 		g_assert_cmpuint(n_params, ==, d->n_params);
 
@@ -70,64 +78,31 @@
 		}
 	}
 
-	/* Validate all the string parameters. */
-	g_assert_cmpstr(source, ==, d->source);
-	g_assert_cmpstr(command, ==, d->command);
-
 	/* Cleanup everything the caller allocated. */
 	g_clear_object(&d->tags);
 
-	/* Return the return value the caller asked for. */
-	return TRUE;
-}
-
-static gboolean
-test_ibis_test_handler_error(G_GNUC_UNUSED IbisMessage *message,
-                             GError **error,
-                             G_GNUC_UNUSED gpointer data)
-{
-	g_set_error(error, TEST_IBIS_PARSER_DOMAIN, 0, "test error");
-
-	return FALSE;
-}
-
-/******************************************************************************
- * Helpers
- *****************************************************************************/
-static void
-test_ibis_parser(const gchar *source, TestIbisParserData *d) {
-	IbisParser *parser = ibis_parser_new();
-	GError *error = NULL;
-	gboolean result = FALSE;
-
-	ibis_parser_set_fallback_handler(parser, test_ibis_test_handler);
-
-	result = ibis_parser_parse(parser, source, &error, d);
-
-	g_assert_no_error(error);
-	g_assert_true(result);
-
-	g_clear_object(&parser);
+	g_assert_finalize_object(message);
+	g_assert_finalize_object(parser);
 }
 
 /******************************************************************************
  * Tests
  *****************************************************************************/
 static void
-test_ibis_parser_propagates_errors(void) {
-	IbisParser *parser = ibis_parser_new();
+test_ibis_parser_error(void) {
+	IbisMessage *message = NULL;
+	IbisParser *parser = NULL;
 	GError *error = NULL;
-	gboolean result = FALSE;
 
-	ibis_parser_set_fallback_handler(parser, test_ibis_test_handler_error);
+	parser = ibis_parser_new();
 
-	result = ibis_parser_parse(parser, "COMMAND", &error, NULL);
-	g_assert_error(error, TEST_IBIS_PARSER_DOMAIN, 0);
+	message = ibis_parser_parse(parser, "", &error);
+	g_assert_error(error, IBIS_PARSER_ERROR, 0);
 	g_clear_error(&error);
 
-	g_assert_false(result);
+	g_assert_null(message);
 
-	g_clear_object(&parser);
+	g_assert_finalize_object(parser);
 }
 
 static void
@@ -688,11 +663,7 @@
 main(gint argc, gchar *argv[]) {
 	g_test_init(&argc, &argv, NULL);
 
-	/* Make sure an error in the handler is propagated back up to the calling
-	 * function.
-	 */
-	g_test_add_func("/ibis/parser/propagates-errors",
-	                test_ibis_parser_propagates_errors);
+	g_test_add_func("/ibis/parser/error", test_ibis_parser_error);
 
 	/* These tests are based on the msg-split tests from
 	 * https://github.com/ircdocs/parser-tests/blob/master/tests/msg-split.yaml
