Create the PurpleTags object for handling tags

Fri, 23 Sep 2022 00:13:38 -0500

author
Gary Kramlich <grim@reaperworld.com>
date
Fri, 23 Sep 2022 00:13:38 -0500
changeset 41736
d78c0951ea2c
parent 41735
c9b476c9c9d2
child 41737
657b634379ab

Create the PurpleTags object for handling tags

Testing Done:
Ran the unit tests.

Bugs closed: PIDGIN-17664

Reviewed at https://reviews.imfreedom.org/r/1777/

libpurple/meson.build file | annotate | diff | comparison | revisions
libpurple/purpletags.c file | annotate | diff | comparison | revisions
libpurple/purpletags.h file | annotate | diff | comparison | revisions
libpurple/tests/meson.build file | annotate | diff | comparison | revisions
libpurple/tests/test_tags.c file | annotate | diff | comparison | revisions
--- a/libpurple/meson.build	Thu Sep 22 23:26:05 2022 -0500
+++ b/libpurple/meson.build	Fri Sep 23 00:13:38 2022 -0500
@@ -83,6 +83,7 @@
 	'purpleproxyinfo.c',
 	'purpleroomlistroom.c',
 	'purplesqlitehistoryadapter.c',
+	'purpletags.c',
 	'purpleuiinfo.c',
 	'purplewhiteboard.c',
 	'purplewhiteboardmanager.c',
@@ -185,6 +186,7 @@
 	'purpleproxyinfo.h',
 	'purpleroomlistroom.h',
 	'purplesqlitehistoryadapter.h',
+	'purpletags.h',
 	'purpleuiinfo.h',
 	'purplewhiteboard.h',
 	'purplewhiteboardmanager.h',
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purpletags.c	Fri Sep 23 00:13:38 2022 -0500
@@ -0,0 +1,177 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Purple is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "purpletags.h"
+
+#include "util.h"
+
+struct _PurpleTags {
+	GObject parent;
+
+	GList *tags;
+};
+
+G_DEFINE_TYPE(PurpleTags, purple_tags, G_TYPE_OBJECT)
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+
+static void
+purple_tags_dispose(GObject *obj) {
+	PurpleTags *tags = PURPLE_TAGS(obj);
+
+	if(tags->tags != NULL) {
+		g_list_free_full(tags->tags, g_free);
+		tags->tags = NULL;
+	}
+
+	G_OBJECT_CLASS(purple_tags_parent_class)->dispose(obj);
+}
+
+static void
+purple_tags_init(PurpleTags *tags) {
+}
+
+static void
+purple_tags_class_init(PurpleTagsClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->dispose = purple_tags_dispose;
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+PurpleTags *
+purple_tags_new(void) {
+	return g_object_new(PURPLE_TYPE_TAGS, NULL);
+}
+
+const gchar *
+purple_tags_lookup(PurpleTags *tags, const gchar *name, gboolean *found) {
+	size_t name_len = 0;
+
+	g_return_val_if_fail(PURPLE_IS_TAGS(tags), FALSE);
+	g_return_val_if_fail(name != NULL, FALSE);
+
+	/* Assume we're going to find the tag, if we don't we set found to false
+	 * before we return. This sounds silly, but it saves some additional logic
+	 * below.
+	 */
+	if(found) {
+		*found = TRUE;
+	}
+
+	name_len = strlen(name);
+
+	for(GList *l = tags->tags; l != NULL; l = l->next) {
+		const gchar *tag = l->data;
+
+		if(g_str_has_prefix(tag, name)) {
+			const gchar *value = tag + name_len;
+
+			if(*value == '\0') {
+				return NULL;
+			} else if(*value == ':') {
+				return value+1;
+			}
+		}
+	}
+
+	/* We didn't find the tag, so set found to false if necessary. */
+	if(found) {
+		*found = FALSE;
+	}
+
+	return NULL;
+}
+
+const gchar *
+purple_tags_get(PurpleTags *tags, const gchar *name) {
+	g_return_val_if_fail(PURPLE_IS_TAGS(tags), NULL);
+	g_return_val_if_fail(name != NULL, NULL);
+
+	return purple_tags_lookup(tags, name, NULL);
+}
+
+void
+purple_tags_add(PurpleTags *tags, const gchar *tag) {
+	g_return_if_fail(PURPLE_IS_TAGS(tags));
+	g_return_if_fail(tag != NULL);
+
+	tags->tags = g_list_append(tags->tags, g_strdup(tag));
+}
+
+gboolean
+purple_tags_remove(PurpleTags *tags, const gchar *tag) {
+	g_return_val_if_fail(PURPLE_IS_TAGS(tags), FALSE);
+	g_return_val_if_fail(tag != NULL, FALSE);
+
+	for(GList *l = tags->tags; l != NULL; l = l->next) {
+		gchar *etag = l->data;
+
+		if(purple_strequal(etag, tag)) {
+			g_free(etag);
+			tags->tags = g_list_delete_link(tags->tags, l);
+
+			return TRUE;
+		}
+	}
+
+	return FALSE;
+}
+
+guint
+purple_tags_get_count(PurpleTags *tags) {
+	g_return_val_if_fail(PURPLE_IS_TAGS(tags), 0);
+
+	return g_list_length(tags->tags);
+}
+
+GList *
+purple_tags_get_all(PurpleTags *tags) {
+	g_return_val_if_fail(PURPLE_IS_TAGS(tags), NULL);
+
+	return tags->tags;
+}
+
+gchar *
+purple_tags_to_string(PurpleTags *tags, const gchar *separator) {
+	GString *value = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_TAGS(tags), NULL);
+
+	value = g_string_new("");
+
+	for(GList *l = tags->tags; l != NULL; l = l->next) {
+		const gchar *tag = l->data;
+
+		g_string_append(value, tag);
+
+		if(separator != NULL && l->next != NULL) {
+			g_string_append(value, separator);
+		}
+	}
+
+	return g_string_free(value, FALSE);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purpletags.h	Fri Sep 23 00:13:38 2022 -0500
@@ -0,0 +1,157 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Purple is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION)
+# error "only <purple.h> may be included directly"
+#endif
+
+#ifndef PURPLE_TAGS_H
+#define PURPLE_TAGS_H
+
+#include <glib.h>
+#include <gio/gio.h>
+
+#define PURPLE_TYPE_TAGS (purple_tags_get_type())
+G_DECLARE_FINAL_TYPE(PurpleTags, purple_tags, PURPLE, TAGS, GObject)
+
+/**
+ * PurpleTags:
+ *
+ * Tags is an object that can be used to keep track of arbitrary tags on
+ * objects. Tags are simple strings that use the first ':' to delimit a value.
+ * For example: `foo` is a tag with just a name and no value, but `foo:bar` is
+ * a tag with a name and value. Please note this distinction when the API calls
+ * for a name versus a tag which would be the name and the value.
+ *
+ * Since: 3.0.0
+ */
+
+G_BEGIN_DECLS
+
+/**
+ * purple_tags_new:
+ *
+ * Creates a new tags object.
+ *
+ * Returns: (transfer full): The new tags object.
+ *
+ * Since: 3.0.0
+ */
+PurpleTags *purple_tags_new(void);
+
+/**
+ * purple_tags_lookup:
+ * @tags: The instance.
+ * @name: The name of the tag to check if it exists.
+ * @found: (out) (nullable): A return address for a boolean on whether the tag
+ *         was found or not.
+ *
+ * Gets the value of @name if it exists in @tags.
+ *
+ * If @found is non %NULL, it will be set to %TRUE if @name was found.
+ *
+ * Returns: The value of the first tag matching @name. If there is no value,
+ *          %NULL is returned.
+ *
+ * Since: 3.0.0
+ */
+const gchar *purple_tags_lookup(PurpleTags *tags, const gchar *name, gboolean *found);
+
+/**
+ * purple_tags_get:
+ * @tags: The instance.
+ * @name: The name of the tag to get.
+ *
+ * Gets the first tag that matches @name.
+ *
+ * Returns: The value of the first tag matching @name. If there is no value,
+ *          %NULL is returned.
+ *
+ * Since: 3.0.0
+ */
+const gchar *purple_tags_get(PurpleTags *tags, const gchar *name);
+
+/**
+ * purple_tags_add:
+ * @tags: The instance.
+ * @tag: The tag data.
+ *
+ * Adds @tag to @tags.
+ *
+ * Since: 3.0.0
+ */
+void purple_tags_add(PurpleTags *tags, const gchar *tag);
+
+/**
+ * purple_tags_remove:
+ * @tags: The instance.
+ * @tag: The tag data.
+ *
+ * Removes the first occurrence of @tag from @tags. Note that this is the tag
+ * name and value not just the name.
+ *
+ * Returns: %TRUE if @tag was found and removed, otherwise %FALSE.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_tags_remove(PurpleTags *tags, const gchar *tag);
+
+/**
+ * purple_tags_get_count:
+ * @tags: The instance.
+ *
+ * Gets the number of tags in @tags.
+ *
+ * Returns: The number of tags that @tags is keeping track of.
+ *
+ * Since: 3.0.0
+ */
+guint purple_tags_get_count(PurpleTags *tags);
+
+/**
+ * purple_tags_get_all:
+ * @tags: The instance.
+ *
+ * Gets a list of all the tags.
+ *
+ * Returns: (transfer none) (element-type utf8): The list of all the tags.
+ *
+ * Since: 3.0.0
+ */
+GList *purple_tags_get_all(PurpleTags *tags);
+
+/**
+ * purple_tags_to_string:
+ * @tags: The instance.
+ * @separator: (nullable): A string to separate the items.
+ *
+ * Creates a @separator delimited string containing each item in @tags.
+ *
+ * Returns: (transfer full): The string representation.
+ *
+ * Since: 3.0.0
+ */
+gchar *purple_tags_to_string(PurpleTags *tags, const gchar *separator);
+
+G_END_DECLS
+
+#endif /* PURPLE_TAGS_H */
--- a/libpurple/tests/meson.build	Thu Sep 22 23:26:05 2022 -0500
+++ b/libpurple/tests/meson.build	Fri Sep 23 00:13:38 2022 -0500
@@ -16,6 +16,7 @@
     'protocol_xfer',
     'purplepath',
     'queued_output_stream',
+    'tags',
     'util',
     'whiteboard_manager',
     'xmlnode',
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/tests/test_tags.c	Fri Sep 23 00:13:38 2022 -0500
@@ -0,0 +1,285 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Purple is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib.h>
+
+#include <purple.h>
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_purple_tags_lookup_exists(void) {
+	PurpleTags *tags = purple_tags_new();
+	gboolean found = FALSE;
+	const gchar *value = NULL;
+
+	purple_tags_add(tags, "foo");
+	value = purple_tags_lookup(tags, "foo", &found);
+	g_assert_null(value);
+	g_assert_true(found);
+
+	purple_tags_add(tags, "bar:baz");
+	value = purple_tags_lookup(tags, "bar", &found);
+	g_assert_cmpstr(value, ==, "baz");
+	g_assert_true(found);
+
+	/* make sure that a name of pur doesn't match a tag of purple */
+	purple_tags_add(tags, "purple");
+	value = purple_tags_lookup(tags, "pur", &found);
+	g_assert_null(value);
+	g_assert_false(found);
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_lookup_non_existent(void) {
+	PurpleTags *tags = purple_tags_new();
+	gboolean found = FALSE;
+	const gchar *value;
+
+	value = purple_tags_lookup(tags, "foo", &found);
+	g_assert_null(value);
+	g_assert_false(found);
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_add_remove_bare(void) {
+	PurpleTags *tags = purple_tags_new();
+
+	purple_tags_add(tags, "tag1");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 1);
+
+	purple_tags_remove(tags, "tag1");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 0);
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_add_duplicate_bare(void) {
+	PurpleTags *tags = purple_tags_new();
+
+	purple_tags_add(tags, "tag1");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 1);
+
+	purple_tags_add(tags, "tag1");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 2);
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_remove_non_existent_bare(void) {
+	PurpleTags *tags = purple_tags_new();
+
+	purple_tags_remove(tags, "tag1");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 0);
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_add_remove_with_value(void) {
+	PurpleTags *tags = purple_tags_new();
+
+	purple_tags_add(tags, "tag1:purple");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 1);
+
+	purple_tags_remove(tags, "tag1:purple");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 0);
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_add_duplicate_with_value(void) {
+	PurpleTags *tags = purple_tags_new();
+
+	purple_tags_add(tags, "tag1:purple");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 1);
+
+	purple_tags_add(tags, "tag1:purple");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 2);
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_remove_non_existent_with_value(void) {
+	PurpleTags *tags = purple_tags_new();
+
+	purple_tags_remove(tags, "tag1:purple");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 0);
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_get_single(void) {
+	PurpleTags *tags = purple_tags_new();
+	const gchar *value = NULL;
+
+	purple_tags_add(tags, "tag1:purple");
+	value = purple_tags_get(tags, "tag1");
+
+	g_assert_cmpstr(value, ==, "purple");
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_get_multiple(void) {
+	PurpleTags *tags = purple_tags_new();
+	const gchar *value = NULL;
+
+	purple_tags_add(tags, "tag1:purple");
+	purple_tags_add(tags, "tag1:pink");
+
+	value = purple_tags_get(tags, "tag1");
+
+	g_assert_cmpstr(value, ==, "purple");
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_get_all(void) {
+	PurpleTags *tags = purple_tags_new();
+	GList *all_tags = NULL;
+	const gchar *values[] = {"foo", "bar", "baz", "qux", "quux", NULL};
+	gint i = 0;
+	gint len = 0;
+
+	for(i = 0; values[i] != NULL; i++) {
+		purple_tags_add(tags, values[i]);
+		len++;
+	}
+
+	all_tags = purple_tags_get_all(tags);
+	i = 0;
+
+	for(GList *l = all_tags; l != NULL; l = l->next) {
+		const gchar *value = l->data;
+
+		g_assert_cmpint(i, <, len);
+		g_assert_cmpstr(value, ==, values[i]);
+
+		i++;
+	}
+
+	g_assert_cmpint(i, ==, len);
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_to_string_single(void) {
+	PurpleTags *tags = purple_tags_new();
+	gchar *value = NULL;
+
+	purple_tags_add(tags, "foo");
+	value = purple_tags_to_string(tags, NULL);
+
+	g_assert_cmpstr(value, ==, "foo");
+
+	g_free(value);
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_to_string_multiple_with_separator(void) {
+	PurpleTags *tags = purple_tags_new();
+	gchar *value = NULL;
+
+	purple_tags_add(tags, "foo");
+	purple_tags_add(tags, "bar");
+	purple_tags_add(tags, "baz");
+	value = purple_tags_to_string(tags, ", ");
+
+	g_assert_cmpstr(value, ==, "foo, bar, baz");
+
+	g_free(value);
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_to_string_multiple_with_null_separator(void) {
+	PurpleTags *tags = purple_tags_new();
+	gchar *value = NULL;
+
+	purple_tags_add(tags, "foo");
+	purple_tags_add(tags, "bar");
+	purple_tags_add(tags, "baz");
+	value = purple_tags_to_string(tags, NULL);
+
+	g_assert_cmpstr(value, ==, "foobarbaz");
+
+	g_free(value);
+
+	g_clear_object(&tags);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+gint
+main(gint argc, gchar **argv) {
+	g_test_init(&argc, &argv, NULL);
+
+	g_test_add_func("/tags/lookup-exists", test_purple_tags_lookup_exists);
+	g_test_add_func("/tags/lookup-non-existent",
+	                test_purple_tags_lookup_non_existent);
+
+	g_test_add_func("/tags/add-remove-bare",
+	                test_purple_tags_add_remove_bare);
+	g_test_add_func("/tags/add-duplicate-bare",
+	                test_purple_tags_add_duplicate_bare);
+	g_test_add_func("/tags/remove-non-existent-bare",
+	                test_purple_tags_remove_non_existent_bare);
+
+	g_test_add_func("/tags/add-remove-with-value",
+	                test_purple_tags_add_remove_with_value);
+	g_test_add_func("/tags/add-duplicate-with-value",
+	                test_purple_tags_add_duplicate_with_value);
+	g_test_add_func("/tags/remove-non-existent-with-value",
+	                test_purple_tags_remove_non_existent_with_value);
+
+	g_test_add_func("/tags/get-single", test_purple_tags_get_single);
+	g_test_add_func("/tags/get-multiple", test_purple_tags_get_multiple);
+
+	g_test_add_func("/tags/get-all", test_purple_tags_get_all);
+
+	g_test_add_func("/tags/to-string-single",
+	                test_purple_tags_to_string_single);
+	g_test_add_func("/tags/to-string-multiple-with-separator",
+	                test_purple_tags_to_string_multiple_with_separator);
+	g_test_add_func("/tags/to-string-multiple-with-null-separator",
+	                test_purple_tags_to_string_multiple_with_null_separator);
+
+	return g_test_run();
+}

mercurial