DEV Community

Bruce Axtens
Bruce Axtens

Posted on

Another Asana library

A while back my Asana tool stopped working. The issue turned out to be that the good folk at Asana had changed the gid values of all the sections in my projects. They probably wrote warning me of that but I didn't see the email.

So I went off on a tangent, writing yet another Asana library (I've already written two) and you can find it on Github. This is the first project I've written with a definite attempt to use TDD, though I must confess that it was not a pure TDD experience as I was pulling in code from the earlier versions. Nevertheless, the tests are there.

Eventually I shifted from testing with the Visual Studio tool to writing scripts in RulesetRunner, the C#-enhanced JScript that we use for all our production scripting. I've raved before about ClearScript and have made a couple of enhanced interpreters using it (Lychen and LychenBASIC). RulesetRunner was the original. A bit of a monster really because I kept adding things as needed, learning how (and how not) to do it on the fly as we used it to get things done.

And now that I have the attach() function figured out, I can see how I can reduce the monstrousness by slicing the project up into chunks and only attaching what I need when I need it. So now the Woo Commerce, WordPress and Facebook modules can be peeled off into separate projects, leaving a tighter, faster, smaller monster.

Seeing as the core of the current Asana project is fairly small I'm going to reproduce it in its entirety here and describe what's going on. Maybe someone will be helped and, who knows, maybe someone will help me improve it.

using RestSharp;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
Enter fullscreen mode Exit fullscreen mode

The web access is managed by RestSharp. A couple of years ago I wrote what I thought was a "Simple REST and HTTP API Client for .NET" but RestSharp does it way better.

I've also waxed lyrical about Linq and there's a bit of that in this project as well.

namespace Asana
{
    public class Asana
    {
        private Dictionary<string, object> Vars = new Dictionary<string, object>();
        internal const string apiUrl = "https://app.asana.com/api/1.0";
        internal string AccessToken { get; set; }
        internal string JsonBody { get; set; }
Enter fullscreen mode Exit fullscreen mode

That's the limit of the data for the object. A string:object Dictionary for variables, the Asana endpoint (could have a better name), and a couple of strings: one for the personal access token (which one manages through Asana's Developer Console) and one to hold the JsonBody for POSTs and PUTs.

        public Asana(bool debug = false)
        {
            if (debug)
                Debugger.Launch();
        }
Enter fullscreen mode Exit fullscreen mode

Part of my testing regime is to have that debug bool in there. You probably wouldn't want to have that there in Production code.

        public Asana SetToken(string token)
        {
            AccessToken = token;
            return this;
        }

        public Asana SetJsonBody(string body)
        {
            JsonBody = body;
            return this;
        }
Enter fullscreen mode Exit fullscreen mode

A couple of public methods to set the personal access token and the json body.

        public Asana SetVar(string name, object value)
        {
            Vars[name] = value;
            return this;
        }

        public Asana SetStringVar(string name, string value) => SetVar(name, value);
        public Asana SetIntVar(string name, int value) => SetVar(name, value);
        public Asana SetDoubleVar(string name, double value) => SetVar(name, value);
        public Asana SetBooleanVar(string name, bool value) => SetVar(name, value);
        public Asana SetListOfStringVar(string name, List<string> value) => SetVar(name, value);
Enter fullscreen mode Exit fullscreen mode

Some methods for setting values in the Vars dictionary.

        public object GetVar(string name) => Vars.ContainsKey(name) ? Vars[name] : null;
        public string GetStringVar(string name) => GetVar(name)?.ToString();
        public int GetIntVar(string name) => (int)GetVar(name);
        public double GetDoubleVar(string name) => (double)GetVar(name);
        public bool GetBooleanVar(string name) => (bool)GetVar(name);
        public List<string> GetListOfStringVar(string name) => (List<string>)GetVar(name);
Enter fullscreen mode Exit fullscreen mode

and some methods for getting them too. Might be overkill. I certainly didn't end up using them for the two scripts that I wrote once out of the TDD process.

        public string Get(string url, bool debug = false) => Do("GET", url, debug);
        public string Put(string url, bool debug = false) => Do("PUT", url, debug);
        public string Post(string url, bool debug = false) => Do("POST", url, debug);
        public string Delete(string url, bool debug = false) => Do("DELETE", url, debug);
Enter fullscreen mode Exit fullscreen mode

Some wrapper methods. Again maybe not required. What's better, var result = asana.Get("...") or var result = asana.Do("GET", "...")?

        public string Do(string method, string url, bool debug = false)
        {
            if (debug) Debugger.Launch();

            foreach (var braced in from var in Vars where var.Key.StartsWith("{") where var.Key.EndsWith("}") select var)
            {
                url = url.Replace(braced.Key, braced.Value.ToString());
            }
Enter fullscreen mode Exit fullscreen mode

Now the meat. The point of the foreach is to replace brace-delimited symbols in the url with the values that are in the Vars dictionary. This makes possible things like this fragment of JScript

asana.SetStringVar("{project_gid}", settings.Projects[project]);
var result = asana.Get("/projects/{project_gid}/sections?opt_pretty=true");
Enter fullscreen mode Exit fullscreen mode

Getting back to the C#

            var client = new RestClient(apiUrl);
            var request = new RestRequest(url);

            request.Method = (Method)Enum.Parse(typeof(Method), method);
Enter fullscreen mode Exit fullscreen mode

The line immediately above is one way to turn the method string into a recognizable enum value to be given to RestSharp's .Method property.

            request.AddHeader("Authorization", "Bearer " + AccessToken);
            request.AddHeader("Accept", "application/json");
            request.AddHeader("Content-Type", "application/json");

            foreach (var unbraced in from var in Vars where !var.Key.StartsWith("{") select var)
            {
                request.AddParameter(unbraced.Key, unbraced.Value, ParameterType.GetOrPost);
            }
Enter fullscreen mode Exit fullscreen mode

Then we do some more RestSharp setup, including the Authorization header with the AccessToken, and then step through the Vars for keys without braces. Any key-value pairs and handed off to RestSharp as GET or POST parameters.

There's a gotcha waiting to bite me there, I'm sure.


            if (JsonBody != null)
            {
                request.AddJsonBody(JsonBody);
            }
Enter fullscreen mode Exit fullscreen mode

If JsonBody has something, add it to RestSharp request

            var response = client.Execute(request);
            return response.Content;
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Finally, execute the request and return the content of the response, which will be a chunk of JSON. Making sense of that JSON is left for the upstream software.

So now to one of the scripts which I wrote to interrogate Asana and through which I discovered the gid renumbering.

var settings = JSON.parse(slurp("c:\\web\\asanasettings.json"));
attach("Asana.DLL", "A");
var asana = new A.Asana.Asana();
asana.SetToken(settings.Authorisation.Token);
Enter fullscreen mode Exit fullscreen mode

Because RulesetRunner talks to Microsoft JScript, I've set it up to pull in a large collection of polyfills which I collected from various MDN pages. I also added a JSON object which I probably picked up from Douglas Crockford's page on github.

So I JSON.parse the contents of an settings JSON file which will give all my workspace and project gids along with our personal access token which I use in the instantiation of the Asana object.

if (!CSSettings.ContainsKey("$ARG1")) {
    CSConsole.WriteLine("needs project");
    CSEnvironment.Exit(1);
}

var project = CSSettings("$ARG1");
if ("undefined" === typeof settings.Projects[project]) {
    CSConsole.WriteLine("needs real project");
    CSEnvironment.Exit(1);
}
Enter fullscreen mode Exit fullscreen mode

RulesetRunner has a CSSettings string:object dictionary and that's loaded up with a pile of symbols when the EXE starts. $ARG1 is the first symbol after the name of the script. In this case we're looking for it to be present and be the name of an existing project

asana.SetStringVar("{project_gid}", settings.Projects[project]);
var result = asana.Get("/projects/{project_gid}/sections?opt_pretty=true");
Enter fullscreen mode Exit fullscreen mode

Next the setting and using of a Var

var json = JSON.parse(result);
for (var d = 0; d < json.data.length; d++) {
    CSConsole.WriteLine('"{0}":"{1}",', project + json.data[d].name, json.data[d].gid);
}
Enter fullscreen mode Exit fullscreen mode

and then the making sense of the result. The code is generating some JSON so as to update the asanasettings.json.

So am I going to use the Asana library? Probably not. Whichever one of the other two that's currently in use is working properly, now that we have the correct gids in play. It's MIT licensed, so someone may find a use for it.

Top comments (0)