[gnome-contacts/wip/nielsdg/vcard-import: 46/47] Enable importing & exporting VCards
- From: Niels De Graef <nielsdg src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-contacts/wip/nielsdg/vcard-import: 46/47] Enable importing & exporting VCards
- Date: Sun, 24 Jul 2022 17:28:00 +0000 (UTC)
commit 8d285c0b6b9b044a0e3fe548907e179111b8dc74
Author: Niels De Graef <nielsdegraef gmail com>
Date: Mon Jan 11 19:22:17 2021 +0100
Enable importing & exporting VCards
This commit adds the experimental functionality in Contacts to import
VCard (*.vcf) files.
Since importing a contact means we have to take in untrusted/unvalidated
input, let's give a high-level view of what happens (should happen):
* Contacts starts a native file chooser dialog so the user can choose
which file to import
* According to the chosen file, Contacts will launch a subprocess to do
the actual parsing using a `Contacts.Io.Importer`.
* The subprocess serializes the result to a `GLib.Variant`
* Contacts receives the result, and on success deserializes the result
* After that, we can show it in a contact pane
data/ui/contacts-main-window.ui | 18 +-
src/contacts-app.vala | 44 ++-
src/contacts-import-operation.vala | 96 +++++
src/contacts-main-window.vala | 23 +-
src/io/contacts-io-exporter.vala | 31 ++
src/io/contacts-io-import-main.vala | 58 +++
src/io/contacts-io-importer.vala | 54 +++
src/io/contacts-io-vcard-exporter.vala | 264 +++++++++++++
src/io/contacts-io-vcard-importer.vala | 280 +++++++++++++
src/io/contacts-io.vala | 435 +++++++++++++++++++++
src/io/meson.build | 36 ++
src/meson.build | 5 +-
tests/io/internal/meson.build | 29 ++
tests/io/internal/test-serialise-birthday.vala | 54 +++
tests/io/internal/test-serialise-common.vala | 66 ++++
tests/io/internal/test-serialise-emails.vala | 41 ++
tests/io/internal/test-serialise-full-name.vala | 42 ++
tests/io/internal/test-serialise-nickname.vala | 40 ++
.../internal/test-serialise-structured-name.vala | 45 +++
tests/io/internal/test-serialise-urls.vala | 41 ++
tests/io/meson.build | 2 +
tests/io/vcard/meson.build | 21 +
tests/io/vcard/minimal.vcf | 4 +
tests/io/vcard/test-vcard-minimal.vala | 54 +++
tests/meson.build | 6 +-
25 files changed, 1783 insertions(+), 6 deletions(-)
---
diff --git a/data/ui/contacts-main-window.ui b/data/ui/contacts-main-window.ui
index 289c56c8..bbf8e336 100644
--- a/data/ui/contacts-main-window.ui
+++ b/data/ui/contacts-main-window.ui
@@ -34,6 +34,13 @@
</section>
</menu>
+ <menu id="import_menu">
+ <item>
+ <attribute name="label" translatable="yes">Import…</attribute>
+ <attribute name="action">app.import</attribute>
+ </item>
+ </menu>
+
<template class="ContactsMainWindow" parent="AdwApplicationWindow">
<property name="default_width">800</property>
<property name="default_height">600</property>
@@ -71,10 +78,11 @@
<property name="show-end-title-buttons" bind-source="content_box"
bind-property="folded" bind-flags="sync-create"/>
<child type="start">
- <object class="GtkButton" id="add_button">
+ <object class="AdwSplitButton" id="add_button">
<property name="tooltip-text" translatable="yes">Create new contact</property>
<property name="icon-name">list-add-symbolic</property>
<property name="action-name">win.new-contact</property>
+ <property name="menu-model">import_menu</property>
</object>
</child>
@@ -151,10 +159,18 @@
<child>
<object class="GtkActionBar" id="actions_bar">
<property name="revealed">False</property>
+ <child>
+ <object class="GtkButton" id="export_button">
+ <property name="label" translatable="yes" comments="Export refers to the
verb">Export</property>
+ <property name="tooltip-text" translatable="yes">Export Selected
Contacts</property>
+ <property name="action-name">win.export-marked-contacts</property>
+ </object>
+ </child>
<child>
<object class="GtkButton" id="link_button">
<property name="focus_on_click">False</property>
<property name="label" translatable="yes" comments="Link refers to the
verb, from linking contacts together">Link</property>
+ <property name="tooltip-text" translatable="yes">Link Selected Contacts
Together</property>
<property name="action-name">win.link-marked-contacts</property>
</object>
</child>
diff --git a/src/contacts-app.vala b/src/contacts-app.vala
index b127554c..26ffc7f7 100644
--- a/src/contacts-app.vala
+++ b/src/contacts-app.vala
@@ -37,7 +37,8 @@ public class Contacts.App : Adw.Application {
{ "help", show_help },
{ "about", show_about },
{ "show-preferences", show_preferences },
- { "show-contact", on_show_contact, "s"}
+ { "show-contact", on_show_contact, "s" },
+ { "import", on_import }
};
private const OptionEntry[] options = {
@@ -306,5 +307,46 @@ public class Contacts.App : Adw.Application {
}
base.quit ();
});
+
+ private void on_import (SimpleAction action, Variant? param) {
+ var chooser = new Gtk.FileChooserNative ("Select contact file",
+ this.window,
+ Gtk.FileChooserAction.OPEN,
+ _("Import"),
+ _("Cancel"));
+ chooser.modal = true;
+ chooser.select_multiple = false;
+
+ // TODO: somehow get this from the list of importers we have
+ var filter = new Gtk.FileFilter ();
+ filter.set_filter_name ("VCard files");
+ filter.add_pattern ("*.vcf");
+ filter.add_pattern ("*.vcard");
+ chooser.add_filter (filter);
+
+ chooser.response.connect ((response) => {
+ if (response != Gtk.ResponseType.ACCEPT) {
+ chooser.destroy ();
+ return;
+ }
+
+ if (chooser.get_file () == null) {
+ debug ("No file selected, or no path available");
+ chooser.destroy ();
+ }
+
+ var file = chooser.get_file ();
+ var import = new ImportOperation (this.contacts_store, file);
+ import.execute.begin ((obj, res) => {
+ try {
+ import.execute.end (res);
+ } catch (GLib.Error err) {
+ warning ("Couldn't import file: %s", err.message);
+ }
+ });
+
+ chooser.destroy ();
+ });
+ chooser.show ();
}
}
diff --git a/src/contacts-import-operation.vala b/src/contacts-import-operation.vala
new file mode 100644
index 00000000..784c8059
--- /dev/null
+++ b/src/contacts-import-operation.vala
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+using Folks;
+
+/**
+ * Launches a subprocess to deal with the import of a Contact file
+ */
+public class Contacts.ImportOperation : Object, Operation {
+
+ private unowned Store store;
+
+ public bool reversable { get { return false; } }
+
+ private string _description;
+ public string description { owned get { return this._description; } }
+
+ private File input_file;
+
+ public ImportOperation (Store store, File file) {
+ this._description = _("Importing contacts from '%s'").printf (file.get_uri ());
+
+ this.store = store;
+ this.input_file = file;
+ }
+
+ public async void execute () throws GLib.Error {
+ var launcher = new SubprocessLauncher (SubprocessFlags.STDOUT_PIPE);
+ // Make sure we're not accidentally propagating the G_MESSAGES_DEBUG variable
+ launcher.set_environ ({});
+
+ debug ("Spawning import subprocess");
+ var subprocess = launcher.spawnv ({
+ "/home/niels/jhbuild/install/libexec/gnome-contacts/gnome-contacts-import",
+ "vcard",
+ this.input_file.get_path ()
+ });
+
+ // Hook up stdout to a MemoryOutputStream, so we can easily fetch the output
+ var proc_stdout = subprocess.get_stdout_pipe ();
+ var stdout_stream = new MemoryOutputStream.resizable ();
+ try {
+ yield stdout_stream.splice_async (proc_stdout, 0, Priority.DEFAULT, null);
+ } catch (Error err) {
+ warning ("Error fetching stdout of import subprocess: %s", err.message);
+ return;
+ }
+
+ debug ("Waiting for import subprocess to finish");
+ var success = yield subprocess.wait_check_async ();
+ debug ("Import subprocess finished");
+ if (!success) {
+ warning ("Import process exited with error status %d", subprocess.get_exit_status ());
+ return;
+ }
+
+ // Ensure we have a proper string by adding a NULL terminator
+ stdout_stream.write ("\0".data);
+ stdout_stream.close ();
+
+ unowned var serialized_str = (string) stdout_stream.get_data ();
+
+ try {
+ var variant = Variant.parse (VariantType.VARDICT, serialized_str);
+
+ var new_details = Contacts.Io.deserialize_gvariant (variant);
+ if (new_details.size () == 0) {
+ warning ("Imported contact has zero fields");
+ return;
+ }
+
+ yield this.store.aggregator.primary_store.add_persona_from_details (new_details);
+
+ } catch (VariantParseError err) {
+ Variant.parse_error_print_context (err, serialized_str);
+ }
+ }
+
+ public async void _undo () throws GLib.Error {
+ return_if_reached ();
+ }
+}
diff --git a/src/contacts-main-window.vala b/src/contacts-main-window.vala
index e395d7e1..bd38db76 100644
--- a/src/contacts-main-window.vala
+++ b/src/contacts-main-window.vala
@@ -27,6 +27,7 @@ public class Contacts.MainWindow : Adw.ApplicationWindow {
{ "stop-editing-contact", stop_editing_contact, "b" },
{ "link-marked-contacts", link_marked_contacts },
{ "delete-marked-contacts", delete_marked_contacts },
+ { "export-marked-contacts", export_marked_contacts },
// { "share-contact", share_contact },
{ "unlink-contact", unlink_contact },
{ "delete-contact", delete_contact },
@@ -72,7 +73,7 @@ public class Contacts.MainWindow : Adw.ApplicationWindow {
private unowned Gtk.ToggleButton favorite_button;
private bool ignore_favorite_button_toggled;
[GtkChild]
- private unowned Gtk.Button add_button;
+ private unowned Adw.SplitButton add_button;
[GtkChild]
private unowned Gtk.Button cancel_button;
[GtkChild]
@@ -179,6 +180,9 @@ public class Contacts.MainWindow : Adw.ApplicationWindow {
unowned var action = lookup_action ("delete-marked-contacts");
((SimpleAction) action).set_enabled (n_selected > 0);
+ action = lookup_action ("export-marked-contacts");
+ ((SimpleAction) action).set_enabled (n_selected > 0);
+
action = lookup_action ("link-marked-contacts");
((SimpleAction) action).set_enabled (n_selected > 1);
@@ -543,6 +547,23 @@ public class Contacts.MainWindow : Adw.ApplicationWindow {
return toast;
}
+ private void export_marked_contacts (GLib.SimpleAction action, GLib.Variant? parameter) {
+ // Take a copy, since we'll unselect everything later
+ var selection = this.marked_contacts.get_selection ().copy ();
+
+ // Go back to normal state as much as possible
+ this.store.selection.unselect_all ();
+ this.marked_contacts.unselect_all ();
+ this.state = UiState.NORMAL;
+
+ var exporter = new Io.VCardExporter ();
+
+ var individuals = bitset_to_individuals (this.store.filter_model,
+ selection);
+ var ret = exporter.export_to_string (individuals.to_array ());
+ warning ("===============VCARD===============\n%s\n==============", ret);
+ }
+
// Little helper
private Gee.LinkedList<Individual> bitset_to_individuals (GLib.ListModel model,
Gtk.Bitset bitset) {
diff --git a/src/io/contacts-io-exporter.vala b/src/io/contacts-io-exporter.vala
new file mode 100644
index 00000000..18cfc7ab
--- /dev/null
+++ b/src/io/contacts-io-exporter.vala
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+using Folks;
+
+/**
+ * An Io.Exporter is an object that can deal with exporting one or more
+ * contacts into a serialized format (VCard is the most common example, but
+ * there exist also CSV based formats and others).
+ *
+ * Note that unlike a Io.Importer, we can skip the whole {@link GLib.HashTable}
+ * dance, since we aren't dealing with untrusted data anymore.
+ */
+public abstract class Contacts.Io.Exporter {
+
+ public abstract string export_to_string (Individual[] individuals) throws GLib.Error;
+}
diff --git a/src/io/contacts-io-import-main.vala b/src/io/contacts-io-import-main.vala
new file mode 100644
index 00000000..75a65597
--- /dev/null
+++ b/src/io/contacts-io-import-main.vala
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+using Folks;
+
+int main (string[] args) {
+ if (args.length != 3)
+ error ("Expected exactly 2 arguments, but got %d", args.length - 1);
+
+ unowned var import_type = args[1];
+ if (import_type == "")
+ error ("Invalid import type: got empty import type");
+
+ unowned var path = args[2];
+ if (path == "")
+ error ("Invalid path: path is empty");
+
+ Contacts.Io.Importer importer;
+ switch (import_type) {
+ case "vcard":
+ importer = new Contacts.Io.VCardImporter ();
+ break;
+ default:
+ error ("Unknown import type '%s'", import_type);
+ }
+
+ HashTable<string, Value?> details;
+ try {
+ var file = File.new_for_path (path);
+ details = importer.import_file (file);
+ } catch (Error err) {
+ error ("Error while importing file '%s': %s", path, err.message);
+ }
+
+ // Serialize
+ var serialized = Contacts.Io.serialize_to_gvariant (details);
+
+ // TODO: raw bytes (performance) or variant.print/parse?
+ // var bytes = serialized.get_data_as_bytes ();
+ // stdout.write (bytes.get_data (), bytes.get_size ());
+ stdout.write (serialized.print (false).data);
+
+ return 0;
+}
diff --git a/src/io/contacts-io-importer.vala b/src/io/contacts-io-importer.vala
new file mode 100644
index 00000000..34f31cdc
--- /dev/null
+++ b/src/io/contacts-io-importer.vala
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+using Folks;
+
+/**
+ * An Io.Importer is an object that can deal with importing a specific format
+ * of describing a Contact (VCard is the most common example, but there exist
+ * also CSV based formats and others).
+ *
+ * The main purpose of an Io.Importer is to import whatever input into a
+ * {@link GLib.HashTable} with string keys and {@link Value} as values. After
+ * that, we can choose to either serialize (using the serializing methods in
+ * Contacts.Io), or to immediately import it in folks using
+ * {@link Folks.PersonaStore.add_from_details}.
+ */
+public abstract class Contacts.Io.Importer {
+
+ /**
+ * Takes the given {@link GLib.File} containing a VCard string and tries to
+ * parse it into a {@link GLib.HashTable}, which can then be used for methods
+ * like {@link Folks.PersonaStore.add_persona_from_details}.
+ */
+ public HashTable<string, Value?> import_file (GLib.File file) throws GLib.Error {
+ string? path = file.get_path ();
+ if (path == null)
+ throw new GLib.IOError.INVALID_FILENAME ("Couldn't import file: file doesn't have a path");
+
+ string vcard_str;
+ FileUtils.get_contents (path, out vcard_str);
+ return import_string (vcard_str);
+ }
+
+ /**
+ * Takes the given input string and tries to parse it into a
+ * {@link GLib.HashTable}, which can then be used for methods like
+ * {@link Folks.PersonaStore.add_persona_from_details}.
+ */
+ public abstract GLib.HashTable<string, Value?> import_string (string vcard_str);
+}
diff --git a/src/io/contacts-io-vcard-exporter.vala b/src/io/contacts-io-vcard-exporter.vala
new file mode 100644
index 00000000..278c9fb8
--- /dev/null
+++ b/src/io/contacts-io-vcard-exporter.vala
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2022 Niels De Graef <nielsdegraef gmail com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+using Folks;
+
+/**
+ * An implementation of {@link Contacts.Io.Exporter} that serializes a contact
+ * to the VCard format.
+ *
+ * Internally, it uses the E.VCard class to implement most of the logic.
+ */
+public class Contacts.Io.VCardExporter : Contacts.Io.Exporter {
+
+ // We _could_ parameterize this with our own enum, but there's no need for
+ // that at the moment.
+ private E.VCardFormat vcard_format = E.VCardFormat.@30;
+
+ // This should always be on false, except for debugging/troubleshooting
+ // purposes. It forces E-D-S personas to use our manual serialization instead
+ // of just returning their own internal E.VCard representation
+ private bool consistent = false;
+
+ public VCardExporter () {
+ }
+
+ public override string export_to_string (Individual[] individuals) throws GLib.Error {
+ StringBuilder result = new StringBuilder ();
+
+ foreach (unowned var individual in individuals) {
+ // XXX at a certain point, we should aggregate personas, but that
+ // requires more thinking through
+
+ foreach (var persona in individual.personas) {
+ string vcard_str = persona_to_vcard (persona);
+ result.append (vcard_str);
+ result.append_c ('\n');
+ }
+ }
+
+ return result.str;
+ }
+
+ private string persona_to_vcard (Persona persona) {
+ // Take a shortcut in case we have an Edsf.Persona, since
+ // that's an E.VCard already
+ if (persona is Edsf.Persona && !consistent) {
+ unowned var contact = ((Edsf.Persona) persona).contact;
+ return contact.to_string (this.vcard_format);
+ }
+
+ var vcard = new E.VCard ();
+
+ if (persona is AvatarDetails)
+ vcard_set_avatar_details (vcard, (AvatarDetails) persona);
+ if (persona is BirthdayDetails)
+ vcard_set_birthday_details (vcard, (BirthdayDetails) persona);
+ if (persona is EmailDetails)
+ vcard_set_email_details (vcard, (EmailDetails) persona);
+ if (persona is FavouriteDetails)
+ vcard_set_favourite_details (vcard, (FavouriteDetails) persona);
+ if (persona is NameDetails)
+ vcard_set_name_details (vcard, (NameDetails) persona);
+ if (persona is NoteDetails)
+ vcard_set_note_details (vcard, (NoteDetails) persona);
+ if (persona is PhoneDetails)
+ vcard_set_phone_details (vcard, (PhoneDetails) persona);
+ if (persona is PostalAddressDetails)
+ vcard_set_postal_address_details (vcard, (PostalAddressDetails) persona);
+ if (persona is RoleDetails)
+ vcard_set_role_details (vcard, (RoleDetails) persona);
+ if (persona is UrlDetails)
+ vcard_set_url_details (vcard, (UrlDetails) persona);
+
+ // The following don't really map properly atm, or are just not worth it.
+ // If we still want/need them later, we can add them still of course
+/*
+ if (persona is AliasDetails)
+ vcard_set_alias_details (vcard, (AliasDetails) persona);
+ if (persona is ExtendedInfo)
+ vcard_set_extended_info (vcard, (ExtendedInfo) persona);
+ if (persona is GenderDetails)
+ vcard_set_gender_details (vcard, (GenderDetails) persona);
+ if (persona is GroupDetails)
+ vcard_set_group_details (vcard, (GroupDetails) persona);
+ if (persona is ImDetails)
+ vcard_set_im_details (vcard, (ImDetails) persona);
+ if (persona is InteractionDetails)
+ vcard_set_interaction_details (vcard, (InteractionDetails) persona);
+ if (persona is LocalIdDetails)
+ vcard_set_localid_details (vcard, (LocalIdDetails) persona);
+ if (persona is LocationDetails)
+ vcard_set_location_details (vcard, (LocationDetails) persona);
+ if (persona is PresenceDetails)
+ vcard_set_presence_details (vcard, (PresenceDetails) persona);
+ if (persona is WebServiceDetails)
+ vcard_set_webservice_details (vcard, (WebServiceDetails) persona);
+*/
+
+ return vcard.to_string (this.vcard_format);
+ }
+
+ private void vcard_set_avatar_details (E.VCard vcard,
+ AvatarDetails details) {
+ // TODO: not sure how we want to do this in such as way that doesn't break
+ // inside a sandbox or without embedding the data directly (which will blow
+ // up the file size)
+ }
+
+ private void vcard_set_birthday_details (E.VCard vcard,
+ BirthdayDetails details) {
+ if (details.birthday == null)
+ return;
+
+ var attr = new E.VCardAttribute (null, E.EVC_BDAY);
+ attr.add_param_with_value (new E.VCardAttributeParam (E.EVC_VALUE), "DATE");
+ vcard.add_attribute_with_value ((owned) attr, details.birthday.format ("%F"));
+ }
+
+ private void vcard_set_email_details (E.VCard vcard,
+ EmailDetails details) {
+ foreach (var email_field in details.email_addresses) {
+ if (email_field.value == "")
+ continue;
+
+ var attr = new E.VCardAttribute (null, E.EVC_EMAIL);
+ vcard.add_attribute_with_value (attr, email_field.value);
+ add_parameters_for_field_details (attr, email_field);
+ }
+ }
+
+ private void vcard_set_favourite_details (E.VCard vcard,
+ FavouriteDetails details) {
+ if (details.is_favourite) {
+ // See Edsf.Persona
+ var attr = new E.VCardAttribute (null, "X-FOLKS-FAVOURITE");
+ vcard.add_attribute_with_value ((owned) attr, "true");
+ }
+ }
+
+ private void vcard_set_name_details (E.VCard vcard,
+ NameDetails details) {
+ if (details.full_name != "") {
+ vcard.add_attribute_with_value (new E.VCardAttribute (null, E.EVC_FN),
+ details.full_name);
+ }
+
+ if (details.structured_name != null) {
+ var attr = new E.VCardAttribute (null, E.EVC_N);
+
+ attr.add_value (details.structured_name.family_name);
+ attr.add_value (details.structured_name.given_name);
+ attr.add_value (details.structured_name.additional_names);
+ attr.add_value (details.structured_name.prefixes);
+ attr.add_value (details.structured_name.suffixes);
+
+ vcard.add_attribute ((owned) attr);
+ }
+
+ if (details.nickname != "") {
+ vcard.add_attribute_with_value (new E.VCardAttribute (null, E.EVC_NICKNAME),
+ details.nickname);
+ }
+ }
+
+ private void vcard_set_note_details (E.VCard vcard,
+ NoteDetails details) {
+ foreach (var note_field in details.notes) {
+ if (note_field.value == "")
+ continue;
+
+ var attr = new E.VCardAttribute (null, E.EVC_NOTE);
+ add_parameters_for_field_details (attr, note_field);
+ vcard.add_attribute_with_value ((owned) attr, note_field.value);
+ }
+ }
+
+ private void vcard_set_phone_details (E.VCard vcard,
+ PhoneDetails details) {
+ foreach (var phone_field in details.phone_numbers) {
+ if (phone_field.value == "")
+ continue;
+
+ var attr = new E.VCardAttribute (null, E.EVC_TEL);
+ add_parameters_for_field_details (attr, phone_field);
+ vcard.add_attribute_with_value ((owned) attr, phone_field.value);
+ }
+ }
+
+ private void vcard_set_postal_address_details (E.VCard vcard,
+ PostalAddressDetails details) {
+ foreach (var postal_field in details.postal_addresses) {
+ unowned var addr = postal_field.value;
+ if (addr.is_empty ())
+ continue;
+
+ var attr = new E.VCardAttribute (null, E.EVC_ADR);
+ add_parameters_for_field_details (attr, postal_field);
+
+ attr.add_value (addr.po_box);
+ attr.add_value (addr.extension);
+ attr.add_value (addr.street);
+ attr.add_value (addr.locality);
+ attr.add_value (addr.region);
+ attr.add_value (addr.postal_code);
+ attr.add_value (addr.country);
+
+ vcard.add_attribute ((owned) attr);
+ }
+ }
+
+ private void vcard_set_role_details (E.VCard vcard,
+ RoleDetails details) {
+ foreach (var role_field in details.roles) {
+ if (role_field.value.title != "") {
+ vcard.add_attribute_with_value (new E.VCardAttribute (null, E.EVC_TITLE),
+ role_field.value.title);
+ }
+ if (role_field.value.organisation_name != "") {
+ vcard.add_attribute_with_value (new E.VCardAttribute (null, E.EVC_ORG),
+ role_field.value.organisation_name);
+ }
+ }
+ }
+
+ private void vcard_set_url_details (E.VCard vcard,
+ UrlDetails details) {
+ foreach (var url_field in details.urls) {
+ if (url_field.value == "")
+ continue;
+
+ var attr = new E.VCardAttribute (null, E.EVC_URL);
+ add_parameters_for_field_details (attr, url_field);
+ vcard.add_attribute_with_value ((owned) attr, url_field.value);
+ }
+ }
+
+ // Helper to get common parameters (e.g. type)
+ private void add_parameters_for_field_details (E.VCardAttribute attr,
+ AbstractFieldDetails field) {
+ Gee.Collection<string>? param_values = null;
+
+ param_values = field.get_parameter_values (AbstractFieldDetails.PARAM_TYPE);
+ if (param_values != null && !param_values.is_empty) {
+ var param = new E.VCardAttributeParam (E.EVC_TYPE);
+ foreach (var typestr in param_values)
+ param.add_value (typestr.up ());
+ attr.add_param ((owned) param);
+ }
+ }
+}
diff --git a/src/io/contacts-io-vcard-importer.vala b/src/io/contacts-io-vcard-importer.vala
new file mode 100644
index 00000000..165c29f6
--- /dev/null
+++ b/src/io/contacts-io-vcard-importer.vala
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+using Folks;
+
+/**
+ * a {@link Contacts.Io.Importer} that specifically deals with importing
+ * VCard files/strings.
+ */
+public class Contacts.Io.VCardImporter : Contacts.Io.Importer {
+
+ public VCardImporter () {
+ }
+
+ /**
+ * Takes the given VCard string and tries to parse it into a
+ * {@link GLib.HashTable}, which can then be used for methods like
+ * {@link Folks.PersonaStore.add_persona_from_details}.
+ */
+ public override HashTable<string, Value?> import_string (string vcard_str) {
+ var details = new HashTable<string, Value?> (GLib.str_hash, GLib.str_equal);
+ var vcard = new E.VCard.from_string (vcard_str);
+
+ unowned var vcard_attrs = vcard.get_attributes ();
+ message ("Got %u attributes in this vcard", vcard_attrs.length ());
+
+ foreach (unowned E.VCardAttribute attr in vcard_attrs) {
+ switch (attr.get_name ()) {
+ // Identification Properties
+ case E.EVC_FN:
+ handle_fn (details, attr);
+ break;
+ case E.EVC_N:
+ handle_n (details, attr);
+ break;
+ case E.EVC_NICKNAME:
+ handle_nickname (details, attr);
+ break;
+/*
+ case E.EVC_PHOTO:
+ handle_photo (details, attr);
+ break;
+*/
+ case E.EVC_BDAY:
+ handle_bday (details, attr);
+ break;
+ // Delivery Addressing Properties
+ case E.EVC_ADR:
+ handle_adr (details, attr);
+ break;
+ // Communications Properties
+ case E.EVC_TEL:
+ handle_tel (details, attr);
+ break;
+ case E.EVC_EMAIL:
+ handle_email (details, attr);
+ break;
+ // Explanatory Properties
+ case E.EVC_NOTE:
+ handle_note (details, attr);
+ break;
+ case E.EVC_URL:
+ handle_url (details, attr);
+ break;
+
+ default:
+ debug ("Unknown property name '%s'", attr.get_name ());
+ break;
+ }
+ }
+
+ return details;
+ }
+
+ // Handles the "FN" (Full Name) attribute
+ private void handle_fn (HashTable<string, Value?> details,
+ E.VCardAttribute attr) {
+ var full_name = attr.get_value ();
+ message ("Got FN '%s'", full_name);
+
+ Value? fn_v = Value (typeof (string));
+ fn_v.set_string (full_name);
+ details.insert (Folks.PersonaStore.detail_key (PersonaDetail.FULL_NAME),
+ (owned) fn_v);
+ }
+
+ // Handles the "N" (structured Name) attribute
+ private void handle_n (HashTable<string, Value?> details,
+ E.VCardAttribute attr) {
+ unowned var values = attr.get_values ();
+
+ // From the VCard spec:
+ // The structured property value corresponds, in sequence, to the Family
+ // Names (also known as surnames), Given Names, Additional Names, Honorific
+ // Prefixes, and Honorific Suffixes.
+ unowned var family_name = values.nth_data (0) ?? "";
+ unowned var given_name = values.nth_data (1) ?? "";
+ unowned var additional_names = values.nth_data (2) ?? "";
+ unowned var prefixes = values.nth_data (3) ?? "";
+ unowned var suffixes = values.nth_data (4) ?? "";
+
+ var structured_name = new StructuredName (family_name, given_name,
+ additional_names,
+ prefixes, suffixes);
+ Value? n_v = Value (typeof (StructuredName));
+ n_v.take_object ((owned) structured_name);
+ details.insert (Folks.PersonaStore.detail_key (PersonaDetail.STRUCTURED_NAME),
+ (owned) n_v);
+ }
+
+ private void handle_nickname (HashTable<string, Value?> details,
+ E.VCardAttribute attr) {
+ var nickname = attr.get_value ();
+ message ("Got nickname '%s'", nickname);
+
+ Value? nick_v = Value (typeof (string));
+ nick_v.set_string (nickname);
+ details.insert (Folks.PersonaStore.detail_key (PersonaDetail.NICKNAME),
+ (owned) nick_v);
+ }
+
+ // Handles the "BDAY" (birthday) attribute
+ private void handle_bday (HashTable<string, Value?> details,
+ E.VCardAttribute attr) {
+ // Get the attribute valuec
+ var bday = attr.get_value ();
+
+ // Parse it using the logic in E.ContactDate
+ var e_date = E.ContactDate.from_string (bday);
+
+ // Turn it into a GLib.DateTime
+ var datetime = new DateTime.utc ((int) e_date.year,
+ (int) e_date.month,
+ (int) e_date.day,
+ 0, 0, 0.0);
+
+ // Insert it into the hashtable as a GLib.Value
+ Value? bday_val = Value (typeof (DateTime));
+ bday_val.take_boxed ((owned) datetime);
+ details.insert (Folks.PersonaStore.detail_key (PersonaDetail.BIRTHDAY),
+ (owned) bday_val);
+ }
+
+ private void handle_email (HashTable<string, Value?> details,
+ E.VCardAttribute attr) {
+ var email = attr.get_value ();
+ if (email == null || email == "")
+ return;
+
+ var email_fd = new EmailFieldDetails (email);
+ add_params (email_fd, attr);
+ insert_field_details<EmailFieldDetails> (details, PersonaDetail.EMAIL_ADDRESSES,
+ email_fd,
+ AbstractFieldDetails<string>.hash_static,
+ AbstractFieldDetails<string>.equal_static);
+ }
+
+ private void handle_tel (HashTable<string, Value?> details,
+ E.VCardAttribute attr) {
+ var phone_nr = attr.get_value ();
+ if (phone_nr == null || phone_nr == "")
+ return;
+
+ var phone_fd = new PhoneFieldDetails (phone_nr);
+ add_params (phone_fd, attr);
+ insert_field_details<PhoneFieldDetails> (details, PersonaDetail.PHONE_NUMBERS,
+ phone_fd,
+ AbstractFieldDetails<string>.hash_static,
+ AbstractFieldDetails<string>.equal_static);
+ }
+
+ // Handles the ADR (postal address) attributes
+ private void handle_adr (HashTable<string, Value?> details,
+ E.VCardAttribute attr) {
+ unowned var values = attr.get_values ();
+
+ // From the VCard spec:
+ // ADR-value = ADR-component-pobox ";" ADR-component-ext ";"
+ // ADR-component-street ";" ADR-component-locality ";"
+ // ADR-component-region ";" ADR-component-code ";"
+ // ADR-component-country
+ unowned var po_box = values.nth_data (0) ?? "";
+ unowned var extension = values.nth_data (1) ?? "";
+ unowned var street = values.nth_data (2) ?? "";
+ unowned var locality = values.nth_data (3) ?? "";
+ unowned var region = values.nth_data (4) ?? "";
+ unowned var postal_code = values.nth_data (5) ?? "";
+ unowned var country = values.nth_data (6) ?? "";
+
+ var addr = new PostalAddress (po_box, extension, street, locality, region,
+ postal_code, country, "", null);
+ var addr_fd = new PostalAddressFieldDetails ((owned) addr);
+ add_params (addr_fd, attr);
+
+ insert_field_details<PostalAddressFieldDetails> (details,
+ PersonaDetail.POSTAL_ADDRESSES,
+ addr_fd,
+ AbstractFieldDetails<PostalAddress>.hash_static,
+ AbstractFieldDetails<PostalAddress>.equal_static);
+ }
+
+ private void handle_url (HashTable<string, Value?> details,
+ E.VCardAttribute attr) {
+ var url = attr.get_value ();
+ if (url == null || url == "")
+ return;
+
+ var url_fd = new UrlFieldDetails (url);
+ add_params (url_fd, attr);
+ insert_field_details<UrlFieldDetails> (details, PersonaDetail.URLS,
+ url_fd,
+ AbstractFieldDetails<string>.hash_static,
+ AbstractFieldDetails<string>.equal_static);
+ }
+
+ private void handle_note (HashTable<string, Value?> details,
+ E.VCardAttribute attr) {
+ var note = attr.get_value ();
+ if (note == null || note == "")
+ return;
+
+ var note_fd = new NoteFieldDetails (note);
+ add_params (note_fd, attr);
+ insert_field_details<NoteFieldDetails> (details, PersonaDetail.NOTES,
+ note_fd,
+ AbstractFieldDetails<string>.hash_static,
+ AbstractFieldDetails<string>.equal_static);
+
+ }
+
+ // Helper method for inserting aggregated properties
+ private bool insert_field_details<T> (HashTable<string, Value?> details,
+ PersonaDetail key,
+ T field_details,
+ owned Gee.HashDataFunc<T>? hash_func,
+ owned Gee.EqualDataFunc<T>? equal_func) {
+
+ // Get the existing set, or create a new one and add it
+ unowned var old_val = details.lookup (Folks.PersonaStore.detail_key (key));
+ if (old_val != null) {
+ unowned var values = old_val as Gee.HashSet<T>;
+ return values.add (field_details);
+ }
+
+ var values = new Gee.HashSet<T> ((owned) hash_func, (owned) equal_func);
+ Value? new_val = Value (typeof (Gee.Set));
+ new_val.set_object (values);
+ details.insert (Folks.PersonaStore.detail_key (key), (owned) new_val);
+
+ return values.add (field_details);
+ }
+
+ // Helper method to get VCard parameters into an AbstractFieldDetails object.
+ // Will take care of setting the correct "type"
+ private void add_params (AbstractFieldDetails details, E.VCardAttribute attr) {
+ foreach (unowned E.VCardAttributeParam param in attr.get_params ()) {
+ string param_name = param.get_name ().down ();
+ foreach (unowned string param_value in param.get_values ()) {
+ if (param_name == AbstractFieldDetails.PARAM_TYPE)
+ details.add_parameter (param_name, param_value.down ());
+ else
+ details.add_parameter (param_name, param_value);
+ }
+ }
+ }
+}
diff --git a/src/io/contacts-io.vala b/src/io/contacts-io.vala
new file mode 100644
index 00000000..9b8bb5fd
--- /dev/null
+++ b/src/io/contacts-io.vala
@@ -0,0 +1,435 @@
+/*
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+using Folks;
+
+/**
+ * Everything in the Io namespace deals with importing and exporting contacts,
+ * both internally (between Contacts and a subprocess, using {@link GLib.Variant}
+ * serialization) and externally (VCard, CSV, ...).
+ */
+namespace Contacts.Io {
+
+ /**
+ * Serializes the {@link GLib.HashTable} as returned by a
+ * {@link Contacts.Io.Importer} into a {@link GLib.Variant} so it can be sent
+ * from one process to another.
+ */
+ public GLib.Variant serialize_to_gvariant (HashTable<string, Value?> details) {
+ var dict = new GLib.VariantDict ();
+
+ var iter = HashTableIter<string, Value?> (details);
+ unowned string prop;
+ unowned Value? val;
+ while (iter.next (out prop, out val)) {
+
+ if (prop == Folks.PersonaStore.detail_key (PersonaDetail.FULL_NAME)) {
+ serialize_full_name (dict, prop, val);
+ } else if (prop == Folks.PersonaStore.detail_key (PersonaDetail.STRUCTURED_NAME)) {
+ serialize_structured_name (dict, prop, val);
+ } else if (prop == Folks.PersonaStore.detail_key (PersonaDetail.NICKNAME)) {
+ serialize_nickname (dict, prop, val);
+ } else if (prop == Folks.PersonaStore.detail_key (PersonaDetail.BIRTHDAY)) {
+ serialize_birthday (dict, prop, val);
+ } else if (prop == Folks.PersonaStore.detail_key (PersonaDetail.POSTAL_ADDRESSES)) {
+ serialize_addresses (dict, prop, val);
+ } else if (prop == Folks.PersonaStore.detail_key (PersonaDetail.PHONE_NUMBERS)) {
+ serialize_phone_nrs (dict, prop, val);
+ } else if (prop == Folks.PersonaStore.detail_key (PersonaDetail.EMAIL_ADDRESSES)) {
+ serialize_emails (dict, prop, val);
+ } else if (prop == Folks.PersonaStore.detail_key (PersonaDetail.NOTES)) {
+ serialize_notes (dict, prop, val);
+ } else if (prop == Folks.PersonaStore.detail_key (PersonaDetail.URLS)) {
+ serialize_urls (dict, prop, val);
+ } else {
+ warning ("Couldn't serialize unknown property '%s'", prop);
+ }
+ }
+
+ return dict.end ();
+ }
+
+ /**
+ * Deserializes the {@link GLib.Variant} back into a {@link GLib.HashTable}.
+ */
+ public HashTable<string, Value?> deserialize_gvariant (GLib.Variant variant) {
+ return_val_if_fail (variant.get_type ().equal (VariantType.VARDICT), null);
+
+ var details = new HashTable<string, Value?> (GLib.str_hash, GLib.str_equal);
+
+ var iter = variant.iterator ();
+ string prop;
+ GLib.Variant val;
+ while (iter.next ("{sv}", out prop, out val)) {
+
+ if (prop == Folks.PersonaStore.detail_key (PersonaDetail.FULL_NAME)) {
+ deserialize_full_name (details, prop, val);
+ } else if (prop == Folks.PersonaStore.detail_key (PersonaDetail.STRUCTURED_NAME)) {
+ deserialize_structured_name (details, prop, val);
+ } else if (prop == Folks.PersonaStore.detail_key (PersonaDetail.NICKNAME)) {
+ deserialize_nickname (details, prop, val);
+ } else if (prop == Folks.PersonaStore.detail_key (PersonaDetail.BIRTHDAY)) {
+ deserialize_birthday (details, prop, val);
+ } else if (prop == Folks.PersonaStore.detail_key (PersonaDetail.POSTAL_ADDRESSES)) {
+ deserialize_addresses (details, prop, val);
+ } else if (prop == Folks.PersonaStore.detail_key (PersonaDetail.PHONE_NUMBERS)) {
+ deserialize_phone_nrs (details, prop, val);
+ } else if (prop == Folks.PersonaStore.detail_key (PersonaDetail.EMAIL_ADDRESSES)) {
+ deserialize_emails (details, prop, val);
+ } else if (prop == Folks.PersonaStore.detail_key (PersonaDetail.NOTES)) {
+ deserialize_notes (details, prop, val);
+ } else if (prop == Folks.PersonaStore.detail_key (PersonaDetail.URLS)) {
+ deserialize_urls (details, prop, val);
+ } else {
+ warning ("Couldn't serialize unknown property '%s'", prop);
+ }
+ }
+
+ return details;
+ }
+
+ //
+ // FULL NAME
+ // -----------------------------------
+ private const string FULL_NAME_TYPE = "s";
+
+ private bool serialize_full_name (GLib.VariantDict dict, string prop, Value? val) {
+ return_val_if_fail (val.type () == typeof (string), false);
+
+ unowned string full_name = val as string;
+ return_val_if_fail (full_name != null, false);
+
+ dict.insert (prop, FULL_NAME_TYPE, full_name);
+
+ return true;
+ }
+
+ private bool deserialize_full_name (HashTable<string, Value?> details, string prop, Variant variant) {
+ return_val_if_fail (variant.get_type ().equal (VariantType.STRING), false);
+
+ unowned string full_name = variant.get_string ();
+ return_val_if_fail (full_name != null, false);
+
+ details.insert (prop, full_name);
+
+ return true;
+ }
+
+ //
+ // NICKNAME
+ // -----------------------------------
+ private const string STRUCTURED_NAME_TYPE = "(sssss)";
+
+ private bool serialize_structured_name (GLib.VariantDict dict, string prop, Value? val) {
+ return_val_if_fail (val.type () == typeof (StructuredName), false);
+
+ unowned var name = val as StructuredName;
+ return_val_if_fail (name != null, false);
+
+ dict.insert (prop, STRUCTURED_NAME_TYPE,
+ name.family_name, name.given_name, name.additional_names,
+ name.prefixes, name.suffixes);
+
+ return true;
+ }
+
+ private bool deserialize_structured_name (HashTable<string, Value?> details, string prop, Variant variant)
{
+ return_val_if_fail (variant.get_type ().equal (new VariantType (STRUCTURED_NAME_TYPE)), false);
+
+ string family_name, given_name, additional_names, prefixes, suffixes;
+ variant.get (STRUCTURED_NAME_TYPE,
+ out family_name,
+ out given_name,
+ out additional_names,
+ out prefixes,
+ out suffixes);
+
+ var structured_name = new StructuredName (family_name, given_name, additional_names,
+ prefixes, suffixes);
+ details.insert (prop, structured_name);
+
+ return true;
+ }
+
+ //
+ // NICKNAME
+ // -----------------------------------
+ private const string NICKNAME_TYPE = "s";
+
+ private bool serialize_nickname (GLib.VariantDict dict, string prop, Value? val) {
+ return_val_if_fail (val.type () == typeof (string), false);
+
+ unowned string nickname = val as string;
+ return_val_if_fail (nickname != null, false);
+
+ dict.insert (prop, NICKNAME_TYPE, nickname);
+
+ return true;
+ }
+
+ private bool deserialize_nickname (HashTable<string, Value?> details, string prop, Variant variant) {
+ return_val_if_fail (variant.get_type ().equal (VariantType.STRING), false);
+
+ unowned string nickname = variant.get_string ();
+ return_val_if_fail (nickname != null, false);
+
+ details.insert (prop, nickname);
+
+ return true;
+ }
+
+ //
+ // BIRTHDAY
+ // -----------------------------------
+ private const string BIRTHDAY_TYPE = "(iii)"; // Year-Month-Day
+
+ private bool serialize_birthday (GLib.VariantDict dict, string prop, Value? val) {
+ return_val_if_fail (val.type () == typeof (DateTime), false);
+
+ unowned var bd = val as DateTime;
+ return_val_if_fail (bd != null, false);
+
+ int year, month, day;
+ bd.get_ymd (out year, out month, out day);
+ dict.insert (prop, BIRTHDAY_TYPE, year, month, day);
+
+ return true;
+ }
+
+ private bool deserialize_birthday (HashTable<string, Value?> details, string prop, Variant variant) {
+ return_val_if_fail (variant.get_type ().equal (new VariantType (BIRTHDAY_TYPE)), false);
+
+ int year, month, day;
+ variant.get (BIRTHDAY_TYPE, out year, out month, out day);
+
+ var bd = new DateTime.utc (year, month, day, 0, 0, 0.0);
+
+ details.insert (prop, bd);
+
+ return true;
+ }
+
+ //
+ // POSTAL ADDRESSES
+ // -----------------------------------
+ private const string ADDRESS_TYPE = "(sssssssv)";
+ private const string ADDRESSES_TYPE = "a" + ADDRESS_TYPE;
+
+ private bool serialize_addresses (GLib.VariantDict dict, string prop, Value? val) {
+ return_val_if_fail (val.type () == typeof (Gee.Set), false);
+
+ // Get the list of field details
+ unowned var afds = val as Gee.Set<PostalAddressFieldDetails>;
+ return_val_if_fail (afds != null, false);
+
+ // Turn the set of field details into an array Variant
+ var builder = new GLib.VariantBuilder (GLib.VariantType.ARRAY);
+ foreach (var afd in afds) {
+ unowned PostalAddress addr = afd.value;
+
+ builder.add (ADDRESS_TYPE,
+ addr.po_box,
+ addr.extension,
+ addr.street,
+ addr.locality,
+ addr.region,
+ addr.postal_code,
+ addr.country,
+ serialize_parameters (afd));
+ }
+
+ dict.insert_value (prop, builder.end ());
+
+ return true;
+ }
+
+ private bool deserialize_addresses (HashTable<string, Value?> details, string prop, Variant variant) {
+ return_val_if_fail (variant.get_type ().equal (new VariantType ("a" + ADDRESS_TYPE)), false);
+
+ var afds = new Gee.HashSet<PostalAddressFieldDetails> ();
+
+ // Turn the array variant into a set of field details
+ var iter = variant.iterator ();
+
+ string po_box, extension, street, locality, region, postal_code, country;
+ GLib.Variant parameters;
+ while (iter.next (ADDRESS_TYPE,
+ out po_box,
+ out extension,
+ out street,
+ out locality,
+ out region,
+ out postal_code,
+ out country,
+ out parameters)) {
+ if (po_box == "" && extension == "" && street == "" && locality == ""
+ && region == "" && postal_code == "" && country == "") {
+ warning ("Got empty postal address");
+ continue;
+ }
+
+ var addr = new PostalAddress (po_box, extension, street, locality, region,
+ postal_code, country, "", null);
+
+ var afd = new PostalAddressFieldDetails (addr);
+ deserialize_parameters (parameters, afd);
+
+ afds.add (afd);
+ }
+
+ details.insert (prop, afds);
+
+ return true;
+ }
+
+ //
+ // PHONE NUMBERS
+ // -----------------------------------
+ private bool serialize_phone_nrs (GLib.VariantDict dict, string prop, Value? val) {
+ return serialize_afd_strings (dict, prop, val);
+ }
+
+ private bool deserialize_phone_nrs (HashTable<string, Value?> details, string prop, Variant variant) {
+ return deserialize_afd_str (details, prop, variant,
+ (str) => { return new PhoneFieldDetails (str); });
+ }
+
+ //
+ // EMAILS
+ // -----------------------------------
+ private bool serialize_emails (GLib.VariantDict dict, string prop, Value? val) {
+ return serialize_afd_strings (dict, prop, val);
+ }
+
+ private bool deserialize_emails (HashTable<string, Value?> details, string prop, Variant variant) {
+ return deserialize_afd_str (details, prop, variant,
+ (str) => { return new EmailFieldDetails (str); });
+ }
+
+ //
+ // NOTES
+ // -----------------------------------
+ private bool serialize_notes (GLib.VariantDict dict, string prop, Value? val) {
+ return serialize_afd_strings (dict, prop, val);
+ }
+
+ private bool deserialize_notes (HashTable<string, Value?> details, string prop, Variant variant) {
+ return deserialize_afd_str (details, prop, variant,
+ (str) => { return new NoteFieldDetails (str); });
+ }
+
+ //
+ // URLS
+ // -----------------------------------
+ private bool serialize_urls (GLib.VariantDict dict, string prop, Value? val) {
+ return serialize_afd_strings (dict, prop, val);
+ }
+
+ private bool deserialize_urls (HashTable<string, Value?> details, string prop, Variant variant) {
+ return deserialize_afd_str (details, prop, variant,
+ (str) => { return new UrlFieldDetails (str); });
+ }
+
+ //
+ // HELPER: AbstractFielDdetail<string>
+ // -----------------------------------
+ private const string AFD_STRING_TYPE = "(sv)";
+
+ private bool serialize_afd_strings (GLib.VariantDict dict, string prop, Value? val) {
+ return_val_if_fail (val.type () == typeof (Gee.Set), false);
+
+ // Get the list of field details
+ unowned var afds = val as Gee.Set<AbstractFieldDetails<string>>;
+ return_val_if_fail (afds != null, false);
+
+ // Turn the set of field details into an array Variant
+ var builder = new GLib.VariantBuilder (GLib.VariantType.ARRAY);
+ foreach (var afd in afds) {
+ builder.add (AFD_STRING_TYPE, afd.value, serialize_parameters (afd));
+ }
+
+ dict.insert_value (prop, builder.end ());
+
+ return true;
+ }
+
+ // In an ideal world, we wouldn't need this delegate and we could just use
+ // GLib.Object.new(), but this is Vala and generics, so we find ourselves in
+ // a big mess here
+ delegate AbstractFieldDetails<string> CreateAbstractFieldStrFunc(string value);
+
+ private bool deserialize_afd_str (HashTable<string, Value?> details,
+ string prop,
+ Variant variant,
+ CreateAbstractFieldStrFunc create_afd_func) {
+ return_val_if_fail (variant.get_type ().equal (new VariantType ("a" + AFD_STRING_TYPE)), false);
+
+ var afds = new Gee.HashSet<AbstractFieldDetails> ();
+
+ // Turn the array variant into a set of field details
+ var iter = variant.iterator ();
+ string str;
+ GLib.Variant parameters;
+ while (iter.next (AFD_STRING_TYPE, out str, out parameters)) {
+ AbstractFieldDetails afd = create_afd_func (str);
+ deserialize_parameters (parameters, afd);
+
+ afds.add (afd);
+ }
+
+ details.insert (prop, afds);
+
+ return true;
+ }
+
+ //
+ // HELPER: Parameters
+ // -----------------------------------
+ // We can't use a vardict here, since one key can map to multiple values.
+ private const string PARAMS_TYPE = "a(ss)";
+
+ private Variant serialize_parameters (AbstractFieldDetails details) {
+
+ if (details.parameters == null || details.parameters.size == 0) {
+ return new GLib.Variant (PARAMS_TYPE, null); // Empty array
+ }
+
+ var builder = new GLib.VariantBuilder (GLib.VariantType.ARRAY);
+ var iter = details.parameters.map_iterator ();
+ while (iter.next ()) {
+ string param_name = iter.get_key ();
+ string param_value = iter.get_value ();
+
+ builder.add ("(ss)", param_name, param_value);
+ }
+
+ return builder.end ();
+ }
+
+ private void deserialize_parameters (Variant parameters, AbstractFieldDetails details) {
+ return_if_fail (parameters.get_type ().is_array ());
+
+ var iter = parameters.iterator ();
+ string param_name, param_value;
+ while (iter.next ("(ss)", out param_name, out param_value)) {
+ if (param_name == AbstractFieldDetails.PARAM_TYPE)
+ details.add_parameter (param_name, param_value.down ());
+ else
+ details.add_parameter (param_name, param_value);
+ }
+ }
+}
diff --git a/src/io/meson.build b/src/io/meson.build
new file mode 100644
index 00000000..08b21182
--- /dev/null
+++ b/src/io/meson.build
@@ -0,0 +1,36 @@
+# Common library
+contacts_io_sources = files(
+ 'contacts-io.vala',
+ 'contacts-io-exporter.vala',
+ 'contacts-io-importer.vala',
+ 'contacts-io-vcard-exporter.vala',
+ 'contacts-io-vcard-importer.vala',
+)
+
+contacts_vala_args = [
+ '--target-glib=@0@'.format(min_glib_version),
+ '--pkg', 'config',
+ '--pkg', 'custom',
+]
+
+contacts_c_args = [
+ '-include', 'config.h',
+ '-DGNOME_DESKTOP_USE_UNSTABLE_API',
+ '-DLOCALEDIR="@0@"'.format(locale_dir),
+]
+
+contacts_io_deps = [
+ folks,
+ folks_eds,
+ gee,
+ gio_unix,
+ glib,
+ libebook,
+]
+
+executable('gnome-contacts-import',
+ [ contacts_io_sources, files('contacts-io-import-main.vala') ],
+ dependencies: contacts_io_deps,
+ install: true,
+ install_dir: get_option('libexecdir') / 'gnome-contacts',
+)
diff --git a/src/meson.build b/src/meson.build
index c246b612..7f01b789 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -1,3 +1,5 @@
+subdir ('io')
+
# GSettings
compiled = gnome.compile_schemas()
install_data('org.gnome.Contacts.gschema.xml',
@@ -10,6 +12,7 @@ libcontacts_sources = files(
'contacts-esd-setup.vala',
'contacts-fake-persona-store.vala',
'contacts-im-service.vala',
+ 'contacts-import-operation.vala',
'contacts-individual-sorter.vala',
'contacts-link-operation.vala',
'contacts-operation.vala',
@@ -56,7 +59,7 @@ if get_option('telepathy')
endif
libcontacts = static_library('contacts',
- libcontacts_sources,
+ [ libcontacts_sources, contacts_io_sources ],
include_directories: config_h_dir,
vala_args: contacts_vala_args,
c_args: contacts_c_args,
diff --git a/tests/io/internal/meson.build b/tests/io/internal/meson.build
new file mode 100644
index 00000000..82590eff
--- /dev/null
+++ b/tests/io/internal/meson.build
@@ -0,0 +1,29 @@
+io_internal_testlib = library('io-internal-testlib',
+ files('test-serialise-common.vala'),
+ dependencies: libcontacts_dep,
+)
+
+io_internal_testlib_dep = declare_dependency(
+ link_with: io_internal_testlib,
+ include_directories: include_directories('.'),
+)
+
+io_internal_test_names = [
+ 'serialise-full-name',
+ 'serialise-structured-name',
+ 'serialise-nickname',
+ 'serialise-birthday',
+ 'serialise-emails',
+ 'serialise-urls',
+]
+
+foreach _test : io_internal_test_names
+ test_bin = executable(_test,
+ files('test-'+_test+'.vala'),
+ dependencies: [ libcontacts_dep, io_internal_testlib_dep ],
+ )
+
+ test(_test, test_bin,
+ suite: 'io-internal',
+ )
+endforeach
diff --git a/tests/io/internal/test-serialise-birthday.vala b/tests/io/internal/test-serialise-birthday.vala
new file mode 100644
index 00000000..46beef2e
--- /dev/null
+++ b/tests/io/internal/test-serialise-birthday.vala
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+using Folks;
+
+void main (string[] args) {
+ Test.init (ref args);
+ Test.add_func ("/io/serialize_birthday",
+ Contacts.Tests.Io.test_serialize_birthday);
+ Test.add_func ("/io/serialize_birthday_pre_epoch",
+ Contacts.Tests.Io.test_serialize_birthday_pre_epoch);
+ Test.run ();
+}
+
+namespace Contacts.Tests.Io {
+
+ private void test_serialize_birthday () {
+ unowned var bd_key = PersonaStore.detail_key (PersonaDetail.BIRTHDAY);
+
+ DateTime old_bd = new GLib.DateTime.utc (1992, 8, 1, 0, 0, 0);
+ var old_bd_val = Value (typeof (DateTime));
+ old_bd_val.set_boxed (old_bd);
+
+ var new_bd_val = _transform_single_value (bd_key, old_bd_val);
+ assert_true (new_bd_val.type () == typeof (DateTime));
+ assert_true (old_bd.equal ((DateTime) new_bd_val.get_boxed ()));
+ }
+
+ private void test_serialize_birthday_pre_epoch () {
+ unowned var bd_key = PersonaStore.detail_key (PersonaDetail.BIRTHDAY);
+
+ DateTime old_bd = new GLib.DateTime.utc (1961, 7, 3, 0, 0, 0);
+ var old_bd_val = Value (typeof (DateTime));
+ old_bd_val.set_boxed (old_bd);
+
+ var new_bd_val = _transform_single_value (bd_key, old_bd_val);
+ assert_true (new_bd_val.type () == typeof (DateTime));
+ assert_true (old_bd.equal ((DateTime) new_bd_val.get_boxed ()));
+ }
+}
diff --git a/tests/io/internal/test-serialise-common.vala b/tests/io/internal/test-serialise-common.vala
new file mode 100644
index 00000000..5ab7a562
--- /dev/null
+++ b/tests/io/internal/test-serialise-common.vala
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+using Folks;
+
+namespace Contacts.Tests.Io {
+
+ // Helper to serialize and deserialize an AbstractFieldDetails
+ public T _transform_single_afd<T> (string prop_key, T afd) {
+ Gee.Set<T> afd_set = new Gee.HashSet<T> ();
+ afd_set.add (afd);
+
+ Value val = Value (typeof (Gee.Set));
+ val.set_object (afd_set);
+
+ Value emails_value = _transform_single_value (prop_key, val);
+ var emails_set = emails_value.get_object () as Gee.Set<T>;
+ if (emails_set == null)
+ error ("GValue has null value");
+ if (emails_set.size != 1)
+ error ("Expected %d elements but got %d", 1, emails_set.size);
+
+ var deserialized_fd = Utils.get_first<T> (emails_set);
+ assert_nonnull (deserialized_fd);
+
+ return deserialized_fd;
+ }
+
+ // Helper to serialize and deserialize a single property with a GLib.Value
+ public GLib.Value _transform_single_value (string prop_key, GLib.Value val) {
+ var details = new HashTable<string, Value?> (GLib.str_hash, GLib.str_equal);
+ details.insert (prop_key, val);
+
+ // Serialize
+ Variant serialized = Contacts.Io.serialize_to_gvariant (details);
+ if (serialized == null)
+ error ("Couldn't serialize single-value table for property %s", prop_key);
+
+ // Deserialize
+ var details_deserialized = Contacts.Io.deserialize_gvariant (serialized);
+ if (details_deserialized == null)
+ error ("Couldn't deserialize details for property %s", prop_key);
+
+ if (!details_deserialized.contains (prop_key))
+ error ("Deserialized details doesn't contain value for property %s", prop_key);
+ Value? val_deserialized = details_deserialized.lookup (prop_key);
+ if (val_deserialized.type() == GLib.Type.NONE)
+ error ("Deserialized Value is unset");
+
+ return val_deserialized;
+ }
+}
diff --git a/tests/io/internal/test-serialise-emails.vala b/tests/io/internal/test-serialise-emails.vala
new file mode 100644
index 00000000..27b15ac0
--- /dev/null
+++ b/tests/io/internal/test-serialise-emails.vala
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+using Folks;
+
+void main (string[] args) {
+ Test.init (ref args);
+ Test.add_func ("/io/serialize_emails",
+ Contacts.Tests.Io.test_serialize_emails);
+ Test.run ();
+}
+
+namespace Contacts.Tests.Io {
+
+ private void test_serialize_emails () {
+ unowned var emails_key = PersonaStore.detail_key (PersonaDetail.EMAIL_ADDRESSES);
+
+ var old_fd = new EmailFieldDetails ("nielsdegraef gmail com");
+ var new_fd = _transform_single_afd<EmailFieldDetails> (emails_key, old_fd);
+
+ if (!(new_fd is EmailFieldDetails))
+ error ("Expected EmailFieldDetails but got %s", new_fd.get_type ().name ());
+
+ if (old_fd.value != new_fd.value)
+ error ("Expected '%s' but got '%s'", old_fd.value, new_fd.value);
+ }
+}
diff --git a/tests/io/internal/test-serialise-full-name.vala b/tests/io/internal/test-serialise-full-name.vala
new file mode 100644
index 00000000..9da8319f
--- /dev/null
+++ b/tests/io/internal/test-serialise-full-name.vala
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+using Folks;
+
+void main (string[] args) {
+ Test.init (ref args);
+ Test.add_func ("/io/serialize_full_name_simple",
+ Contacts.Tests.Io.test_serialize_full_name_simple);
+ Test.run ();
+}
+
+namespace Contacts.Tests.Io {
+
+ private void test_serialize_full_name_simple () {
+ unowned var fn_key = PersonaStore.detail_key (PersonaDetail.FULL_NAME);
+
+ string old_fn = "Niels De Graef";
+ Value old_fn_val = Value (typeof (string));
+ old_fn_val.set_string (old_fn);
+
+ var new_fn_val = _transform_single_value (fn_key, old_fn_val);
+ if (new_fn_val.type () != typeof (string))
+ error ("Expected G_TYPE_STRING but got %s", new_fn_val.type ().name ());
+ if (old_fn != new_fn_val.get_string ())
+ error ("Expected '%s' but got '%s'", old_fn, new_fn_val.get_string ());
+ }
+}
diff --git a/tests/io/internal/test-serialise-nickname.vala b/tests/io/internal/test-serialise-nickname.vala
new file mode 100644
index 00000000..649b6382
--- /dev/null
+++ b/tests/io/internal/test-serialise-nickname.vala
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+using Folks;
+
+void main (string[] args) {
+ Test.init (ref args);
+ Test.add_func ("/io/serialize_nickame",
+ Contacts.Tests.Io.test_serialize_nickname);
+ Test.run ();
+}
+
+namespace Contacts.Tests.Io {
+
+ private void test_serialize_nickname () {
+ unowned var nick_key = PersonaStore.detail_key (PersonaDetail.NICKNAME);
+
+ string old_nick = "nielsdg";
+ var old_nick_val = Value (typeof (string));
+ old_nick_val.set_string (old_nick);
+
+ var new_nick_val = _transform_single_value (nick_key, old_nick_val);
+ assert_true (new_nick_val.type () == typeof (string));
+ assert_true (old_nick == new_nick_val.get_string ());
+ }
+}
diff --git a/tests/io/internal/test-serialise-structured-name.vala
b/tests/io/internal/test-serialise-structured-name.vala
new file mode 100644
index 00000000..45f2093e
--- /dev/null
+++ b/tests/io/internal/test-serialise-structured-name.vala
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+using Folks;
+
+void main (string[] args) {
+ Test.init (ref args);
+ Test.add_func ("/io/serialize_structured_name_simple",
+ Contacts.Tests.Io.test_serialize_structured_name_simple);
+ Test.run ();
+}
+
+namespace Contacts.Tests.Io {
+
+ private void test_serialize_structured_name_simple () {
+ unowned var sn_key = PersonaStore.detail_key (PersonaDetail.STRUCTURED_NAME);
+
+ var old_sn = new StructuredName.simple ("Niels", "De Graef");
+ Value old_sn_val = Value (typeof (StructuredName));
+ old_sn_val.set_object (old_sn);
+
+ var new_sn_val = _transform_single_value (sn_key, old_sn_val);
+
+ if (new_sn_val.type () != typeof (StructuredName))
+ error ("Expected FOLKS_TYPE_STRUCTURED_NAME but got %s", new_sn_val.type ().name ());
+
+ var new_sn = new_sn_val.get_object () as StructuredName;
+ if (!old_sn.equal (new_sn))
+ error ("Expected '%s' but got '%s'", old_sn.to_string (), new_sn.to_string ());
+ }
+}
diff --git a/tests/io/internal/test-serialise-urls.vala b/tests/io/internal/test-serialise-urls.vala
new file mode 100644
index 00000000..cf4cdf90
--- /dev/null
+++ b/tests/io/internal/test-serialise-urls.vala
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+using Folks;
+
+void main (string[] args) {
+ Test.init (ref args);
+ Test.add_func ("/io/serialize_urls_single",
+ Contacts.Tests.Io.test_serialize_urls_single);
+ Test.run ();
+}
+
+namespace Contacts.Tests.Io {
+
+ private void test_serialize_urls_single () {
+ unowned var urls_key = PersonaStore.detail_key (PersonaDetail.URLS);
+
+ var old_fd = new UrlFieldDetails ("http://www.islinuxaboutchoice.com/");
+ var new_fd = _transform_single_afd<UrlFieldDetails> (urls_key, old_fd);
+
+ if (!(new_fd is UrlFieldDetails))
+ error ("Expected UrlFieldDetails but got %s", new_fd.get_type ().name ());
+
+ if (old_fd.value != new_fd.value)
+ error ("Expected '%s' but got '%s'", old_fd.value, new_fd.value);
+ }
+}
diff --git a/tests/io/meson.build b/tests/io/meson.build
new file mode 100644
index 00000000..2f349605
--- /dev/null
+++ b/tests/io/meson.build
@@ -0,0 +1,2 @@
+subdir('internal')
+subdir('vcard')
diff --git a/tests/io/vcard/meson.build b/tests/io/vcard/meson.build
new file mode 100644
index 00000000..e3946e3f
--- /dev/null
+++ b/tests/io/vcard/meson.build
@@ -0,0 +1,21 @@
+io_vcard_files = [
+ 'minimal',
+]
+
+foreach vcard_name : io_vcard_files
+ vcf_file = meson.current_source_dir() / vcard_name + '.vcf'
+
+ # Ideally we'd do this using a preprocessor symbol or something
+ vcf_test_env = environment()
+ vcf_test_env.append('_VCF_FILE', vcf_file)
+
+ test_bin = executable(vcard_name,
+ files('test-vcard-'+vcard_name+'.vala'),
+ dependencies: libcontacts_dep,
+ )
+
+ test(vcard_name, test_bin,
+ suite: 'io-vcard',
+ env: vcf_test_env,
+ )
+endforeach
diff --git a/tests/io/vcard/minimal.vcf b/tests/io/vcard/minimal.vcf
new file mode 100644
index 00000000..8ac83c91
--- /dev/null
+++ b/tests/io/vcard/minimal.vcf
@@ -0,0 +1,4 @@
+BEGIN:'''VCARD'''
+VERSION:3.0
+FN:Niels De Graef
+END:VCARD
diff --git a/tests/io/vcard/test-vcard-minimal.vala b/tests/io/vcard/test-vcard-minimal.vala
new file mode 100644
index 00000000..28da64ad
--- /dev/null
+++ b/tests/io/vcard/test-vcard-minimal.vala
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+using Folks;
+
+void main (string[] args) {
+ Test.init (ref args);
+ Test.add_func ("/io/test_vcard_minimal",
+ Contacts.Tests.Io.test_vcard_minimal);
+ Test.run ();
+}
+
+namespace Contacts.Tests.Io {
+ private void test_vcard_minimal () {
+ unowned var vcf_path = Environment.get_variable ("_VCF_FILE");
+ if (vcf_path == null || vcf_path == "")
+ error ("No .vcf file set as envvar. Please use the meson test suite");
+
+ var file = GLib.File.new_for_path (vcf_path);
+
+ var importer = new Contacts.Io.VCardImporter ();
+ HashTable<string, Value?> details;
+ try {
+ details = importer.import_file (file);
+ } catch (Error err) {
+ error ("Error while importing: %s", err.message);
+ }
+ if (details == null)
+ error ("VCardImporter returned null");
+
+ unowned var fn_key = PersonaStore.detail_key (PersonaDetail.FULL_NAME);
+ if (!details.contains (fn_key))
+ error ("No FN value");
+
+ var fn_value = details.lookup (fn_key);
+ unowned var fn = fn_value as string;
+ if (fn != "Niels De Graef")
+ error ("Expected '%s' but got '%s'", "Niels De Graef", fn);
+ }
+}
diff --git a/tests/meson.build b/tests/meson.build
index 92c35863..6dcfcf12 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -1,14 +1,16 @@
+subdir('io')
+
test_names = [
'basic-test',
]
foreach _test : test_names
test_bin = executable(_test,
- '@0@.vala'.format(_test),
+ files('@0@.vala'.format(_test)),
dependencies: libcontacts_dep,
)
test(_test, test_bin,
- suite: 'gnome-contacts',
+ suite: 'src',
)
endforeach
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]