# HG changeset patch
# Node ID bd54bf38ea80
# Parent  7f086dfa22c7
diff --git a/xeme/meson.build b/xeme/meson.build
--- a/xeme/meson.build
+++ b/xeme/meson.build
@@ -1,6 +1,7 @@
 XEME_SOURCES = [
   'xemeaddress.c',
   'xemeconnection.c',
+  'xemeextension.c',
   'xemeinputstream.c',
   'xememessage.c',
   'xemeoutputstream.c',
@@ -14,6 +15,7 @@
   'xemeconnection.h',
   'xemeconstants.h',
   'xemecore.h',
+  'xemeextension.h',
   'xemeinputstream.h',
   'xememessage.h',
   'xemeoutputstream.h',
diff --git a/xeme/tests/meson.build b/xeme/tests/meson.build
--- a/xeme/tests/meson.build
+++ b/xeme/tests/meson.build
@@ -1,5 +1,6 @@
 PROGRAMS = [
   'address',
+  'extension',
   'inputstream',
   'message',
   'outputstream',
diff --git a/xeme/tests/testextension.c b/xeme/tests/testextension.c
new file mode 100644
--- /dev/null
+++ b/xeme/tests/testextension.c
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2023 Xeme Developers
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib.h>
+
+#include <xeme.h>
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_xeme_extension_properties(void) {
+	XemeExtension *extension = NULL;
+	char *element = NULL;
+	char *namespace = NULL;
+
+	extension = g_object_new(
+		XEME_TYPE_EXTENSION,
+		"element", "element1",
+		"namespace", "namespace1",
+		NULL);
+
+	g_object_get(
+		G_OBJECT(extension),
+		"element", &element,
+		"namespace", &namespace,
+		NULL);
+
+	g_assert_cmpstr(element, ==, "element1");
+	g_clear_pointer(&element, g_free);
+
+	g_assert_cmpstr(namespace, ==, "namespace1");
+	g_clear_pointer(&namespace, g_free);
+
+	g_assert_finalize_object(extension);
+}
+
+/******************************************************************************
+ * Normal Extension
+ *****************************************************************************/
+G_DECLARE_FINAL_TYPE(TestXemeExtensionNormal, test_xeme_extension_normal,
+                     TEST_XEME, EXTENSION_NORMAL, XemeExtension)
+
+struct _TestXemeExtensionNormal {
+	XemeExtension parent;
+
+	guint parse;
+	guint serialize;
+};
+
+G_DEFINE_TYPE(TestXemeExtensionNormal, test_xeme_extension_normal,
+              XEME_TYPE_EXTENSION)
+
+static void
+test_xeme_extension_normal_parse(XemeExtension *extension,
+                                 GMarkupParseContext *context)
+{
+	TestXemeExtensionNormal *normal = NULL;
+
+	g_assert_true(XEME_IS_EXTENSION(extension));
+	g_assert_true(TEST_XEME_IS_EXTENSION_NORMAL(extension));
+	g_assert_nonnull(context);
+
+	normal = TEST_XEME_EXTENSION_NORMAL(extension);
+
+	normal->parse += 1;
+}
+
+static char *
+test_xeme_extension_normal_serialize(XemeExtension *extension) {
+	TestXemeExtensionNormal *normal = NULL;
+
+	g_assert_true(XEME_IS_EXTENSION(extension));
+	g_assert_true(TEST_XEME_IS_EXTENSION_NORMAL(extension));
+
+	normal = TEST_XEME_EXTENSION_NORMAL(extension);
+
+	normal->serialize += 1;
+
+	return g_strdup("serialized");
+}
+
+static void
+test_xeme_extension_normal_init(TestXemeExtensionNormal *extension) {
+	extension->parse = 0;
+	extension->serialize = 0;
+}
+
+static void
+test_xeme_extension_normal_class_init(TestXemeExtensionNormalClass *klass) {
+	XemeExtensionClass *extension_class = XEME_EXTENSION_CLASS(klass);
+
+	extension_class->parse = test_xeme_extension_normal_parse;
+	extension_class->serialize = test_xeme_extension_normal_serialize;
+}
+
+/******************************************************************************
+ * Normal Tests
+ *****************************************************************************/
+static void
+test_xeme_extension_normal_parse_test(void) {
+	TestXemeExtensionNormal *extension = NULL;
+	GMarkupParseContext *context = NULL;
+	GMarkupParser parser = {
+		.text = NULL,
+	};
+
+	context = g_markup_parse_context_new(&parser, G_MARKUP_DEFAULT_FLAGS,
+	                                     NULL, NULL);
+
+	extension = g_object_new(test_xeme_extension_normal_get_type(), NULL);
+
+	xeme_extension_parse(XEME_EXTENSION(extension), context);
+	g_assert_cmpuint(extension->parse, ==, 1);
+	g_assert_cmpuint(extension->serialize, ==, 0);
+
+	g_markup_parse_context_free(context);
+
+	g_assert_finalize_object(extension);
+}
+
+static void
+test_xeme_extension_normal_serialize_test(void) {
+	TestXemeExtensionNormal *extension = NULL;
+	char *output = NULL;
+
+	extension = g_object_new(test_xeme_extension_normal_get_type(), NULL);
+	output = xeme_extension_serialize(XEME_EXTENSION(extension));
+
+	g_assert_cmpstr(output, ==, "serialized");
+	g_clear_pointer(&output, g_free);
+
+	g_assert_cmpuint(extension->parse, ==, 0);
+	g_assert_cmpuint(extension->serialize, ==, 1);
+
+	g_assert_finalize_object(extension);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+int
+main(int argc, char *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+
+	g_test_set_nonfatal_assertions();
+
+	g_test_add_func("/xeme/extension/properties",
+	                test_xeme_extension_properties);
+
+	g_test_add_func("/xeme/extension/normal/parse",
+	                test_xeme_extension_normal_parse_test);
+	g_test_add_func("/xeme/extension/normal/serialize",
+	                test_xeme_extension_normal_serialize_test);
+
+	return g_test_run();
+}
+
diff --git a/xeme/xemeextension.h b/xeme/xemeextension.h
new file mode 100644
--- /dev/null
+++ b/xeme/xemeextension.h
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2023 Xeme Developers
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef XEME_EXTENSION_H
+#define XEME_EXTENSION_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include <gio/gio.h>
+
+#include "xemeversion.h"
+
+G_BEGIN_DECLS
+
+/**
+ * XemeExtension:
+ *
+ * The base class for representing
+ * [extension elements](https://datatracker.ietf.org/doc/html/rfc6120#section-8.4).
+ *
+ * Since: 0.1.0
+ */
+
+#define XEME_TYPE_EXTENSION (xeme_extension_get_type())
+
+XEME_AVAILABLE_IN_0_1
+G_DECLARE_DERIVABLE_TYPE(XemeExtension, xeme_extension, XEME, EXTENSION,
+                         GObject)
+
+struct _XemeExtensionClass {
+	/*< private >*/
+	GObjectClass parent;
+
+	/*< public >*/
+	void (*parse)(XemeExtension *extension, GMarkupParseContext *context);
+	char *(*serialize)(XemeExtension *extension);
+
+	/*< private >*/
+	gpointer reserved[8];
+};
+
+/**
+ * xeme_extension_get_element:
+ * @extension: The instance.
+ *
+ * Gets the name of the element from @extension.
+ *
+ * Since: 0.1.0
+ */
+XEME_AVAILABLE_IN_0_1
+const char *xeme_extension_get_element(XemeExtension *extension);
+
+/**
+ * xeme_extension_get_namespace:
+ * @extension: The instance.
+ *
+ * Gets the namespace from @extension.
+ *
+ * Since: 0.1.0
+ */
+XEME_AVAILABLE_IN_0_1
+const char *xeme_extension_get_namespace(XemeExtension *extension);
+
+/**
+ * xeme_extension_parse:
+ * @extension: The instance.
+ * @context: The [struct@GLib.MarkupParseContext] of the parser.
+ *
+ * Called by the [class@InputStream] to parse the extension element.
+ *
+ * This calls [vfunc@Extension.parse] which should use @context in a call to
+ * [method@GLib.MarkupParseContext.push] with its own parser to parse the data
+ * and set @extension appropriately.
+ *
+ * Since: 0.1.0
+ */
+XEME_AVAILABLE_IN_0_1
+void xeme_extension_parse(XemeExtension *extension, GMarkupParseContext *context);
+
+/**
+ * xeme_extension_serialize:
+ * @extension: The instance.
+ *
+ * Serializes @extension into XML.
+ *
+ * This calls [vfunc@Extension.serialize] to do the actual serialization.
+ *
+ * Returns: (transfer full): The XML data as a string for @extension.
+ *
+ * Since: 0.1.0
+ */
+XEME_AVAILABLE_IN_0_1
+char *xeme_extension_serialize(XemeExtension *extension);
+
+G_END_DECLS
+
+#endif /* XEME_EXTENSION_H */
+
diff --git a/xeme/xemeextension.c b/xeme/xemeextension.c
new file mode 100644
--- /dev/null
+++ b/xeme/xemeextension.c
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2023 Xeme Developers
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "xemeextension.h"
+
+#include "xemestring.h"
+
+enum {
+	PROP_0,
+	PROP_ELEMENT,
+	PROP_NAMESPACE,
+	N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+
+typedef struct {
+	char *element;
+	char *namespace;
+} XemeExtensionPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE(XemeExtension, xeme_extension, G_TYPE_OBJECT)
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+xeme_extension_set_element(XemeExtension *extension, const char *element) {
+	XemeExtensionPrivate *priv = NULL;
+
+	g_return_if_fail(XEME_IS_EXTENSION(extension));
+
+	priv = xeme_extension_get_instance_private(extension);
+
+	if(!xeme_str_equal(priv->element, element)) {
+		g_free(priv->element);
+		priv->element = g_strdup(element);
+
+		g_object_notify_by_pspec(G_OBJECT(extension),
+		                         properties[PROP_ELEMENT]);
+	}
+}
+
+static void
+xeme_extension_set_namespace(XemeExtension *extension, const char *namespace) {
+	XemeExtensionPrivate *priv = NULL;
+
+	g_return_if_fail(XEME_IS_EXTENSION(extension));
+
+	priv = xeme_extension_get_instance_private(extension);
+
+	if(!xeme_str_equal(priv->namespace, namespace)) {
+		g_free(priv->namespace);
+		priv->namespace = g_strdup(namespace);
+
+		g_object_notify_by_pspec(G_OBJECT(extension),
+		                         properties[PROP_NAMESPACE]);
+	}
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+static void
+xeme_extension_finalize(GObject *obj) {
+	XemeExtension *extension = XEME_EXTENSION(obj);
+	XemeExtensionPrivate *priv = NULL;
+
+	priv = xeme_extension_get_instance_private(extension);
+
+	g_clear_pointer(&priv->element, g_free);
+	g_clear_pointer(&priv->namespace, g_free);
+
+	G_OBJECT_CLASS(xeme_extension_parent_class)->finalize(obj);
+}
+
+static void
+xeme_extension_get_property(GObject *obj, guint param_id, GValue *value,
+                            GParamSpec *pspec)
+{
+	XemeExtension *extension = XEME_EXTENSION(obj);
+
+	switch(param_id) {
+	case PROP_ELEMENT:
+		g_value_set_string(value, xeme_extension_get_element(extension));
+		break;
+	case PROP_NAMESPACE:
+		g_value_set_string(value, xeme_extension_get_namespace(extension));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+xeme_extension_set_property(GObject *obj, guint param_id, const GValue *value,
+                            GParamSpec *pspec)
+{
+	XemeExtension *extension = XEME_EXTENSION(obj);
+
+	switch(param_id) {
+	case PROP_ELEMENT:
+		xeme_extension_set_element(extension, g_value_get_string(value));
+		break;
+	case PROP_NAMESPACE:
+		xeme_extension_set_namespace(extension, g_value_get_string(value));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+xeme_extension_init(G_GNUC_UNUSED XemeExtension *extension) {
+}
+
+static void
+xeme_extension_class_init(XemeExtensionClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->finalize = xeme_extension_finalize;
+	obj_class->get_property = xeme_extension_get_property;
+	obj_class->set_property = xeme_extension_set_property;
+
+	/**
+	 * XemeExtension:element:
+	 *
+	 * The name of the element that's being extended.
+	 *
+	 * Since: 0.1.0
+	 */
+	properties[PROP_ELEMENT] = g_param_spec_string(
+		"element", "element",
+		"The name of the element that's being extended.",
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * XemeExtension:namespace:
+	 *
+	 * The namespace of the extension.
+	 *
+	 * Since: 0.1.0
+	 */
+	properties[PROP_NAMESPACE] = g_param_spec_string(
+		"namespace", "namespace",
+		"The namespace of the extension.",
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+const char *
+xeme_extension_get_element(XemeExtension *extension) {
+	XemeExtensionPrivate *priv = NULL;
+
+	g_return_val_if_fail(XEME_IS_EXTENSION(extension), NULL);
+
+	priv = xeme_extension_get_instance_private(extension);
+
+	return priv->element;
+}
+
+const char *
+xeme_extension_get_namespace(XemeExtension *extension) {
+	XemeExtensionPrivate *priv = NULL;
+
+	g_return_val_if_fail(XEME_IS_EXTENSION(extension), NULL);
+
+	priv = xeme_extension_get_instance_private(extension);
+
+	return priv->namespace;
+}
+
+void
+xeme_extension_parse(XemeExtension *extension, GMarkupParseContext *context) {
+	XemeExtensionClass *klass = NULL;
+
+	g_return_if_fail(XEME_IS_EXTENSION(extension));
+	g_return_if_fail(context != NULL);
+
+	klass = XEME_EXTENSION_GET_CLASS(extension);
+	if(klass != NULL && klass->parse != NULL) {
+		klass->parse(extension, context);
+	}
+}
+
+char *
+xeme_extension_serialize(XemeExtension *extension) {
+	XemeExtensionClass *klass = NULL;
+
+	g_return_val_if_fail(XEME_IS_EXTENSION(extension), NULL);
+
+	klass = XEME_EXTENSION_GET_CLASS(extension);
+	if(klass != NULL && klass->serialize != NULL) {
+		return klass->serialize(extension);
+	}
+
+	return NULL;
+}
+
