← Volver a los artículos

Gnome: Desarrollo de aplicaciones. VAPI

Artículo Desarrollo Gnome

En este artículo vamos a ver qué es VAPI, para qué sirve y cómo se usa. Aprovechamos esta explicación para añadir a nuestro proyecto nueva funcionalidad para leer y parsear los XML de los RSS remotos.

Como he comentado en artículos anteriores para el propósito de esta aplicación no es necesario hacer un VAPI pero es algo que merece la pena ver para entender que es y como funciona.

Un resumen de lo que es VAPI podría ser que son mecanismos que nos permiten utilizar librerías externas en Vala como bindings y  normalmente las de C. En este caso es totalmente prescindible su uso porque Soup (https://valadoc.org/libsoup-3.0/Soup.html) está disponible para Vala por lo que podemos decir que ya existe un VAPI de C de Soup para Vala.

Los ficheros para el código los vamos a poner separados del src de la aplicación en un dir llamado vapi y los ficheros tienen la extensión .vapi. Para la distribución de un vapi se requiere que se ponga un comentario en el código con la licencia pero en nuestro caso podemos omitir esto ya que no es algo que vayamos a distribuir. 

/*
 * Copyright (c) 2016 My Name <my_email@my_address.com>
 * 
 * This library is free software...[or whichever license is used by the library]
 *
 */

Vala va a generar un código siguiendo ciertas normas de nombres y orden de parámetros y para eso usa los atributos de CCode que le permite a Vala tener un control sobre el código C que genera. https://gnome.pages.gitlab.gnome.org/vala/manual/attributes.html. Como regla general las librerías "puenteadas" se usan bajo un namespace por lo que tendremos que declarar nuestro namespace para nuestro vapi.

namespace ExampleVapi
{
...
}

Como último apunte es incluir los ficheros header de la librería C.

[CCode (cheader_filename = "libfoo/foo.h")]

Es importante recalcar que esto que vamos a hacer ahora es TOTALMENTE INNECESARIO para el objetivo de nuestra aplicación porque las librerías que vamos a usar ya tienen vapi para Vala y se pueden usar directamente en el código sin realizar esto. Podéis buscar en Google un ejemplo sencillo que hay con math.h de C para crear un vapi con una librería C y usarlo en Vala.

Lo más sencillo que se me ocurre es implementar algo para nuestro httpClient, que vamos a necesitar si o si para llamar a los rss remotos. Con libsoup podemos realizar una reimplementación de session/message sin complicarnos demasiado... supongo. Libsoup es la librería para cliente/servidor Http que se suele usar en Gnome.

vapi/soup-example.vapi

[CCode (cheader_filename = "libsoup/soup.h", lower_case_cprefix = "soup_")]
namespace SoupExample
{
    [CCode (cname = "SoupSession", cprefix = "soup_session_", type_id = "soup_session_get_type()")]
    public class Session : GLib.Object
    {
        [CCode (cname = "soup_session_new")]
        public Session();
        [CCode (cname = "soup_session_send_and_read")]
        public GLib.Bytes send_and_read(Message msg, GLib.Cancellable? cancellable) throws GLib.Error;
    }

    [CCode (cname = "SoupMessage", cprefix = "soup_message_", type_id = "soup_message_get_type ()")]
    public class Message : GLib.Object
    {
        [CCode (cname = "soup_message_new")]
        public Message(string method, string uri_string);
        public uint status { get; }
    }
}

src/Services/HttpClient.vala

namespace Example
{
    public class HttpClient : GLib.Object
    {
        private SoupExample.Session session;
        public HttpClient ()
        {
            session = new SoupExample.Session();
        }

        public string fetch (string url) throws GLib.Error
        {
            var msg = new SoupExample.Message("GET", url);
            var bytes = session.send_and_read(msg, null);
            if (msg.status != 200) {
                throw new GLib.IOError.FAILED ("HTTP %u fetching %s", msg.status, url);
            }
            return (string) bytes.get_data ();
        }
    }
}

Con esto ya tenemos listo nuestro HttpClient para poder realizar las llamadas a los RSS remotos y obtener los items. El siguiente paso que deberíamos dar es crear un servicio de validación de XML correcto  para descartarlo o no antes de empezar a parsear y lidiar con los posibles errores en los formatos.

src/Services/XmlValidator.vala

  namespace Example
  {
        public class XmlValidator : GLib.Object
        {
                public bool is_valid(string xml, out string? error_message)
                {
                        error_message = null;
                        Xml.Doc* doc = Xml.Parser.parse_memory(xml, xml.length);
                        if (doc == null) {
			Xml.Error* error = Xml.get_last_error ();
error_message = (error != null) ? error->message.strip () : "Invalid XML";
return false; } delete doc; return true; } } }

Comprobado el xml necesitamos una pieza que lo lea, pensando que es un RSS, que obtenga los campos de nuestro modelo y nos genere un listado que podamos manejar para nuestro modelo Item

src/Services/RssFeedParser.vala

using Gee;
namespace Example
{
    public class RssFeedParser : GLib.Object
    {
        public ArrayList<Item> parse (string xml) throws GLib.Error
        {
            var items = new ArrayList<Item>();
            Xml.Doc* doc = Xml.Parser.parse_memory(xml, xml.length);
            if (doc == null) {
                Xml.Error* error = Xml.get_last_error ();
                throw new GLib.IOError.FAILED(error != null ? error->message.strip () : "Malformed XML");
            }
            Xml.Node* root = doc->get_root_element();
            if (root != null) {
                for (Xml.Node* channel = root->children; channel != null; channel = channel->next) 
                {
                    if (channel->name != "channel") continue;
                    for (Xml.Node* node = channel->children; node != null; node = node->next) 
                    {
                        if (node->name != "item") continue;
                        items.add (parse_item (node));
                    }
                }
            }
            delete doc;
            return items;
        }

        private Item parse_item(Xml.Node* node)
        {
            string title = "", link = "", description = "", pubDate = "";
            for (Xml.Node* field = node->children; field != null; field = field->next) 
            {
                string content = field->get_content () ?? "";
                switch (field->name) 
                {
                    case "title": title = content; break;
                    case "link": link = content; break;
                    case "description": description = content; break;
                    case "pubDate": pubDate = content; break;
                }
            }
            return new Item (title, link, description, pubDate);
        }
    }
}

Por último, integrar estas piezas en nuestro repositorio. Vamos a crear un nuevo repo que se llamará RemoteFeedRepository que implemente esto y borraremos la anterior implementación.

src/Repository/RemoteFeedRepository.vala

using Gee;

namespace Example

{
    public class RemoteFeedRepository : Object, Repository
    {
        public string url { get; construct; }
        private HttpClient http_client;
        private XmlValidator xml_validator;
        private RssFeedParser feed_parser;
        public RemoteFeedRepository (string url)
        {
            Object (url: url);
        }
        construct
        {
            http_client = new HttpClient ();
            xml_validator = new XmlValidator ();
            feed_parser = new RssFeedParser ();
        }

        public ArrayList<Item> find ()
        {
            try {
                string xml = http_client.fetch (url);
                string? error_message;
                if (!xml_validator.is_valid (xml, out error_message)) {
                    warning ("Invalid RSS feed XML from %s: %s", url, error_message);
                    return new ArrayList<Item> ();
                }
                return feed_parser.parse (xml);
            } catch (Error e) {
                warning ("Failed to load RSS feed from %s: %s", url, e.message);
                return new ArrayList<Item> ();
            }
        }
    }
}

Cambiamos el repositorio en nuestro ViewModel:

src/ViewModel/ItemListViewModel.vala

...

private Service service = new ImplRemoteFeedService (new RemoteFeedRepository("https://news.google.com/rss?hl=es&gl=ES&ceid=ES:es"));

...

Compilamos la aplicación y comprobamos que todo está funcionando como debe.

rm -r build && meson setup build && ninja -C ./build

./build/src/example

Hemos hecho muchas cosas mas que ver la parte de VAPI y creo que con esto podemos cerrar esta parte. Ya tenemos nuestro cliente http haciendo peticiones, comprobando si los xmls son válidos y generando nuestro modelo para renderizar los items por pantalla. Esto es un avance y estoy pensando que en el próximo artículo podríamos ver como generar un menú de preferencias para configurar los posibles origines de los rss y poder tener varias fuentes de datos y que podamos navegar entre ellas.

Nos vemos en el próximo artículo


app feed ejemplo