# HG changeset patch
# Node ID c56fe5f9f0b4ae3e1859e0b8fff35adc1069e9d8
# Parent  3f59da5156297400ec64977e5568dc6f435b699a
diff --git a/hiya/hiyaservice.c b/hiya/hiyaservice.c
new file mode 100644
--- /dev/null
+++ b/hiya/hiyaservice.c
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2025 Hiya Developers
+ *
+ * Hiya is the legal property of its developers, whose names are too
+ * numerous to list here. Please refer to the AUTHORS file distributed
+ * with this source distribution
+ *
+ * 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 "hiyaservice.h"
+
+struct _HiyaService {
+	GObject parent;
+
+	char *name;
+	GPtrArray *endpoints;
+};
+
+enum {
+	PROP_0,
+	PROP_NAME,
+	N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+hiya_service_set_name(HiyaService *service, const char *name) {
+	g_return_if_fail(HIYA_IS_SERVICE(service));
+
+	if(g_set_str(&service->name, name)) {
+		g_object_notify_by_pspec(G_OBJECT(service), properties[PROP_NAME]);
+	}
+}
+
+/******************************************************************************
+ * GListModel Implementation
+ *****************************************************************************/
+static GType
+hiya_service_get_item_type(G_GNUC_UNUSED GListModel *model) {
+	return HIYA_TYPE_ENDPOINT;
+}
+
+static guint
+hiya_service_get_n_items(GListModel *model) {
+	HiyaService *service = HIYA_SERVICE(model);
+
+	return service->endpoints->len;
+}
+
+static gpointer
+hiya_service_get_item(GListModel *model, guint position) {
+	HiyaEndpoint *endpoint = NULL;
+	HiyaService *service = HIYA_SERVICE(model);
+
+	if(position < service->endpoints->len) {
+		endpoint = g_ptr_array_index(service->endpoints, position);
+
+		if(HIYA_IS_ENDPOINT(endpoint)) {
+			g_object_ref(endpoint);
+		}
+	}
+
+	return endpoint;
+}
+
+static void
+hiya_service_list_model_iface_init(GListModelInterface *iface) {
+	iface->get_item_type = hiya_service_get_item_type;
+	iface->get_n_items = hiya_service_get_n_items;
+	iface->get_item = hiya_service_get_item;
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_FINAL_TYPE_WITH_CODE(HiyaService, hiya_service, G_TYPE_OBJECT,
+                              G_IMPLEMENT_INTERFACE(G_TYPE_LIST_MODEL,
+                                                    hiya_service_list_model_iface_init))
+
+static void
+hiya_service_finalize(GObject *obj) {
+	HiyaService *service = HIYA_SERVICE(obj);
+
+	g_clear_pointer(&service->name, g_free);
+	g_clear_pointer(&service->endpoints, g_ptr_array_unref);
+
+	G_OBJECT_CLASS(hiya_service_parent_class)->finalize(obj);
+}
+
+static void
+hiya_service_get_property(GObject *obj, guint param_id, GValue *value,
+                          GParamSpec *pspec)
+{
+	HiyaService *service = HIYA_SERVICE(obj);
+
+	switch(param_id) {
+	case PROP_NAME:
+		g_value_set_string(value, hiya_service_get_name(service));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+hiya_service_set_property(GObject *obj, guint param_id, const GValue *value,
+                          GParamSpec *pspec)
+{
+	HiyaService *service = HIYA_SERVICE(obj);
+
+	switch(param_id) {
+	case PROP_NAME:
+		hiya_service_set_name(service, g_value_get_string(value));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+hiya_service_init(HiyaService *service) {
+	service->endpoints = g_ptr_array_new_full(1, g_object_unref);
+}
+
+static void
+hiya_service_class_init(HiyaServiceClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->finalize = hiya_service_finalize;
+	obj_class->get_property = hiya_service_get_property;
+	obj_class->set_property = hiya_service_set_property;
+
+	/**
+	 * HiyaService:name:
+	 *
+	 * The name of the service.
+	 *
+	 * This could be something like `_http._tcp` or `_presence._tcp`.
+	 *
+	 * Since: 0.1
+	 */
+	properties[PROP_NAME] = g_param_spec_string(
+		"name", NULL, NULL,
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+void
+hiya_service_add_endpoint(HiyaService *service, HiyaEndpoint *endpoint) {
+	guint n_items = 0;
+
+	g_return_if_fail(HIYA_IS_SERVICE(service));
+	g_return_if_fail(HIYA_IS_ENDPOINT(endpoint));
+
+	/* Store the len as it will be the position of the new item. */
+	n_items = service->endpoints->len;
+
+	g_ptr_array_add(service->endpoints, g_object_ref(endpoint));
+
+	g_list_model_items_changed(G_LIST_MODEL(service), n_items, 0, 1);
+}
+
+void
+hiya_service_clear_endpoints(HiyaService *service) {
+	guint n_items = 0;
+
+	g_return_if_fail(HIYA_IS_SERVICE(service));
+
+	n_items = service->endpoints->len;
+	if(n_items > 0) {
+		g_ptr_array_remove_range(service->endpoints, 0, n_items);
+
+		g_list_model_items_changed(G_LIST_MODEL(service), 0, n_items, 0);
+	}
+}
+
+HiyaService *
+hiya_service_new(const char *name) {
+	g_return_val_if_fail(name != NULL, NULL);
+
+	return g_object_new(
+		HIYA_TYPE_SERVICE,
+		"name", name,
+		NULL);
+}
+
+const char *
+hiya_service_get_name(HiyaService *service) {
+	g_return_val_if_fail(HIYA_IS_SERVICE(service), NULL);
+
+	return service->name;
+}
diff --git a/hiya/hiyaservice.h b/hiya/hiyaservice.h
new file mode 100644
--- /dev/null
+++ b/hiya/hiyaservice.h
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2025 Hiya Developers
+ *
+ * Hiya is the legal property of its developers, whose names are too
+ * numerous to list here. Please refer to the AUTHORS file distributed
+ * with this source distribution
+ *
+ * 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 HIYA_SERVICE_H
+#define HIYA_SERVICE_H
+
+#include <glib.h>
+
+#include <glib-object.h>
+
+#include "hiyaendpoint.h"
+#include "hiyaversion.h"
+
+G_BEGIN_DECLS
+
+#define HIYA_TYPE_SERVICE (hiya_service_get_type())
+
+/**
+ * HiyaService:
+ *
+ * A representation of an mDNS service.
+ *
+ * Since: 0.1
+ */
+HIYA_AVAILABLE_IN_0_1
+G_DECLARE_FINAL_TYPE(HiyaService, hiya_service, HIYA, SERVICE, GObject)
+
+/**
+ * hiya_service_add_endpoint:
+ * @endpoint: (transfer none): the endpoint
+ *
+ * Adds an endpoint to the service.
+ *
+ * Since: 0.1
+ */
+HIYA_AVAILABLE_IN_0_1
+void hiya_service_add_endpoint(HiyaService *service, HiyaEndpoint *endpoint);
+
+/**
+ * hiya_service_clear_endpoints:
+ *
+ * Clears all endpoints from a service.
+ *
+ * Since: 0.1
+ */
+HIYA_AVAILABLE_IN_0_1
+void hiya_service_clear_endpoints(HiyaService *service);
+
+/**
+ * hiya_service_get_name:
+ *
+ * Gets the name of the service.
+ *
+ * The service name will be something like `_http._tcp` or `_presence._tcp`.
+ *
+ * Returns: The name of the service.
+ *
+ * Since: 0.1
+ */
+HIYA_AVAILABLE_IN_0_1
+const char *hiya_service_get_name(HiyaService *service);
+
+/**
+ * hiya_service_new:
+ * @name: the name of the service
+ *
+ * Creates a new service.
+ *
+ * Returns: (transfer full): The new service.
+ *
+ * Since: 0.1
+ */
+HIYA_AVAILABLE_IN_0_1
+HiyaService *hiya_service_new(const char *name);
+
+G_END_DECLS
+
+#endif /* HIYA_SERVICE_H */
diff --git a/hiya/meson.build b/hiya/meson.build
--- a/hiya/meson.build
+++ b/hiya/meson.build
@@ -2,12 +2,14 @@
 
 HIYA_SOURCES = [
   'hiyaendpoint.c',
+  'hiyaservice.c',
   'hiyatxt.c',
   'hiyaversion.c',
 ]
 
 HIYA_HEADERS = [
   'hiyaendpoint.h',
+  'hiyaservice.h',
   'hiyatxt.h',
   'hiyaversion.h',
 ]
diff --git a/hiya/tests/meson.build b/hiya/tests/meson.build
--- a/hiya/tests/meson.build
+++ b/hiya/tests/meson.build
@@ -1,5 +1,6 @@
 PROGRAMS = [
   'endpoint',
+  'service',
   'txt',
 ]
 
diff --git a/hiya/tests/test_service.c b/hiya/tests/test_service.c
new file mode 100644
--- /dev/null
+++ b/hiya/tests/test_service.c
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2025 Hiya Developers
+ *
+ * Hiya is the legal property of its developers, whose names are too
+ * numerous to list here. Please refer to the AUTHORS file distributed
+ * with this source distribution
+ *
+ * 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 <hiya.h>
+
+/******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+test_hiya_service_items_changed_counter(GListModel *model,
+                                        G_GNUC_UNUSED guint position,
+                                        G_GNUC_UNUSED guint removed,
+                                        G_GNUC_UNUSED guint added,
+                                        gpointer data)
+{
+	guint *counter = data;
+
+	g_assert_true(HIYA_IS_SERVICE(model));
+
+	*counter = *counter + 1;
+}
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_hiya_service_new(void) {
+	HiyaService *service = NULL;
+
+	service = hiya_service_new("_http._tcp");
+
+	g_assert_true(HIYA_IS_SERVICE(service));
+	g_assert_true(G_IS_LIST_MODEL(service));
+
+	g_assert_finalize_object(service);
+}
+
+static void
+test_hiya_service_properties(void) {
+	HiyaService *service = NULL;
+	char *name = NULL;
+
+	service = g_object_new(
+		HIYA_TYPE_SERVICE,
+		"name", "_http._tcp",
+		NULL);
+
+	g_assert_true(HIYA_IS_SERVICE(service));
+
+	g_object_get(
+		G_OBJECT(service),
+		"name", &name,
+		NULL);
+
+	g_assert_cmpstr(name, ==, "_http._tcp");
+	g_clear_pointer(&name, g_free);
+
+	g_assert_finalize_object(service);
+}
+
+static void
+test_hiya_service_add_clear(void) {
+	HiyaService *service = NULL;
+	HiyaTxt *txt = NULL;
+	GInetAddress *address = NULL;
+	GListModel *model = NULL;
+	GSocketAddress *endpoint = NULL;
+	guint counter = 0;
+
+	address = g_inet_address_new_from_string("127.0.0.1");
+	txt = hiya_txt_new();
+
+	service = hiya_service_new("_http._tcp");
+	model = G_LIST_MODEL(service);
+	g_signal_connect(service, "items-changed",
+	                 G_CALLBACK(test_hiya_service_items_changed_counter),
+	                 &counter);
+	g_assert_cmpuint(g_list_model_get_n_items(model), ==, 0);
+
+	counter = 0;
+	hiya_service_clear_endpoints(service);
+	g_assert_cmpuint(counter, ==, 0);
+	g_assert_cmpuint(g_list_model_get_n_items(model), ==, 0);
+
+	counter = 0;
+	endpoint = hiya_endpoint_new(address, 1337, txt);
+	hiya_service_add_endpoint(service, HIYA_ENDPOINT(endpoint));
+	g_assert_cmpuint(counter, ==, 1);
+	g_assert_cmpuint(g_list_model_get_n_items(model), ==, 1);
+	g_clear_object(&endpoint);
+
+	counter = 0;
+	endpoint = hiya_endpoint_new(address, 1338, txt);
+	hiya_service_add_endpoint(service, HIYA_ENDPOINT(endpoint));
+	g_assert_cmpuint(counter, ==, 1);
+	g_assert_cmpuint(g_list_model_get_n_items(model), ==, 2);
+	g_clear_object(&endpoint);
+
+	counter = 0;
+	hiya_service_clear_endpoints(service);
+	g_assert_cmpuint(counter, ==, 1);
+	g_assert_cmpuint(g_list_model_get_n_items(model), ==, 0);
+
+	g_assert_finalize_object(service);
+	g_assert_finalize_object(txt);
+	g_assert_finalize_object(address);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+int
+main(int argc, char *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+
+	g_test_add_func("/hiya/service/new", test_hiya_service_new);
+	g_test_add_func("/hiya/service/properties", test_hiya_service_properties);
+
+	g_test_add_func("/hiya/service/add-clear", test_hiya_service_add_clear);
+
+	return g_test_run();
+}
