DEV Community

Cover image for Souping Up the DEV.to API with Vala
twirp
twirp

Posted on

Souping Up the DEV.to API with Vala

I'm not saying Vala is the language of the future. All I'm saying is some great apps are being built in it.

ThiefMD1 recently added support for publishing to DEV.to. ThiefMD accomplishes this with Vala, and today I hope to show you how.

ThiefMD Connection Manager

The DEV API is well documented. This post is more about Valavangelism (it's a great language). For API calls across the net, Vala has bindings for both cURL and libsoup2. libsoup integrates with Vala's Main Loop and has a nice API. libsoup + json-glib provides a fun and friendly way to interact with REST API's.

Prerequisites

I'm on Ubuntu 22.04. For dependencies I'll run:

sudo apt install build-essential valac libsoup2.4-dev libjson-glib-dev meson ninja-build
Enter fullscreen mode Exit fullscreen mode

Abstracting the WebCall

For performing REST API calls, I created a class to wrap creating a Soup.Session and Soup.Message.

/**
 * WebCall wraps a Soup.Session and handles managing Soup Messages
 */
private class WebCall {
    private Soup.Session session;
    private Soup.Message message;
    private string url;
    private string body;
    private bool is_mime = false;

    public string response_str;
    public uint response_code;

    /**
      * Constructs a Soup.Session for the given endpoint
      * 
      * @param endpoint The URL to connect to
      * @param api The path the API rests at
      */
    public class WebCall (string endpoint, string api) {
        url = endpoint + api;
        session = new Soup.Session ();
        body = "";
    }

    /**
      * Sets the data body for this request
      * 
      * @param data The data to populate the body with
      */
    public void set_body (string data) {
        body = data;
    }

    /**
      * Constructs a GET Soup.Message for this session
      */
    public void set_get () {
        message = new Soup.Message ("GET", url);
    }

    /**
      * Constructs a DELETE Soup.Message for this session
      */
    public void set_delete () {
        message = new Soup.Message ("DELETE", url);
    }

    /**
      * Constructs a POST Soup.Message for this session
      */
    public void set_post () {
        message = new Soup.Message ("POST", url);
    }

    /**
      * Adds an HTTP header to the current Soup.Message
      * 
      * @param key The HTTP Header Key to add
      * @param value The value for the HTTP Header
      */
    public void add_header (string key, string value) {
        message.request_headers.append (key, value);
    }

    /**
      * Performs the Session call for the current Message.
      * Returns true if the call receives a success code.
      * Returns false if the call fails or receives empty data.
      */
    public bool perform_call () {
        MainLoop loop = new MainLoop ();
        bool success = false;
        debug ("Calling %s", url);

        // The DEV API requires a User Agent
        add_header ("User-Agent", "Vala-to-Dev/0.1");
        if (body != "") {
            message.set_request ("application/json", Soup.MemoryUse.COPY, body.data);
        }

        session.queue_message (message, (sess, mess) => {
            response_str = (string) mess.response_body.flatten ().data;
            response_code = mess.status_code;

            // Validate we have a string response and success HTTP code
            if (response_str != null && response_str != "" &&
                response_code >= 200 && response_code <= 250)
            {
                success = true;
            }
            loop.quit ();
        });

        loop.run ();
        return success;
    }
}
Enter fullscreen mode Exit fullscreen mode

new WebCall will construct our Soup.Session. The set_post, set_get and etc construct the proper Soup.Message type. perform_call executes our REST API calls. MainLoop loop = new MainLoop (); with session.queue_message allows us to let the GUI to continue to run while web requests happen in the background.

With this class, we can easily talk to the DEV API. JSON-GLib will help us speak the DEV API language.

Speaking JSON from Vala

For a easy example, we're going to be checking The authenticated user. The sample output looks like:

{
    "type_of": "user",
    "id": 1234,
    "username": "bob",
    "name": "bob",
    "summary": "Hello, world",
    "twitter_username": "bob",
    "github_username": "bob",
    "website_url": null,
    "location": "New York",
    "joined_at": "Jan 1, 2017",
    "profile_image": "https://res.cloudinary.com/...jpeg"
}
Enter fullscreen mode Exit fullscreen mode

Using JSON-GLib, our class representing the object looks like this.

public class UserResponse : GLib.Object, Json.Serializable {
    public string type_of { get; set; }
    public int id { get; set; }
    public string username { get; set; }
    public string name { get; set; }
    public string summary { get; set; }
    public string twitter_username { get; set; }
    public string github_username { get; set; }
    public string website_url { get; set; }
    public string location { get; set; }
    public string joined_at { get; set; }
    public string profile_image { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Using Json.Serializable makes it easy for us to go from a Vala Object to a JSON representation.

Making the API Call

Once we have our DEV API Key, we're ready to start sending messages. Our validation of the API Key can look something like:

/**
 * Retrieves the username corresponding to the API-Key.
 * Returns true if the API Key worked, false otherwise.
 * 
 * @param username Receives the alias associated with the API-Key
 * @param api_key The API KEY to use for authentication
 */
public bool get_authenticated_user (
    out string username,
    string api_key)
{
    // Create a WebCall to handle this session
    WebCall authentication = new WebCall ("https://dev.to", "/api/users/me");
    // The Authentication API expects a GET request
    authentication.set_get ();
    // Add our API Key
    authentication.add_header ("api-key", api_key);

    // Perform our call
    bool res = authentication.perform_call ();
    if (!res) {
        return false;
    }

    // Parse the reponse
    Json.Parser parser = new Json.Parser ();
    parser.load_from_data (authentication.response_str);
    Json.Node data = parser.get_root ();
    UserResponse response = Json.gobject_deserialize (
        typeof (UserResponse),
        data)
        as UserResponse;

    username = response.username;
    return true;
}
Enter fullscreen mode Exit fullscreen mode

Boom! Now we can validate our API key.

We can do something similar for createArticle, and we did! The code is at github.com/ThiefMD/forem-vala.

Thanks

Dear reader, I think your great. With Vala, I think you'll build great things! So go out and build, and don't hesitate to ask for help.

ThiefMD Export View

ThiefMD uses this library to allow exporting documents to DEV. With Spell Checking, Grammar Checking, and Write-Good ThiefMD will have you missing your teacher's red pen in no time. I mean, writing and publishing great DEV articles in no time.


  1. I make ThiefMD 😻️. ↩

  2. If you're not using GLib or GTK, cURL is a more portable solution. ↩

Discussion (2)

Collapse
colinkiama profile image
Colin Kiama

Great article! There is one question I have though: How is the API key stored?

Collapse
twirp profile image
twirp Author

Thanks! For persisting the API Key, we use libsecret to store it encrypted in the OS Keyring.

The API Key is kept as a string in memory πŸ˜…. Vala doesn't appear to have securestring, and the DEV API doesn't do single session tokens after auth.