Slack Status Updater (CLI) - Setting up Models

Or in the case of Go. Structs

January 23rd 2018

Before I start I should probably clarify that I still have no idea what is the true "Golang" way of writing things. And there are a few resources that give some best practices. I probably don't know my way around just yet. So instead of diving in from there, I'm going to break down my core features that I decided on and try and figure out the logic I need to implement for this. Then I can try and apply whatever language features I need.

IMG_20180123_085544

I'll need to add a step to check if the status information is valid before posting the information to the Slack API

09:15 Breaking for a bit. Wrote first test to verify the struct is setup correctly.

10:40 Synced work across from laptop to desktop. Here's the commit that added in the first test and "model"

10:45 Writing the tests for reading in a list of yaml formatted entries. The format I've decided on is going to be:

- statusName: lunch
  emoji: chompy
  statusText: Having lunch

- statusName: resting
  emoji: bath
  statusText: Resting

- statusName: awesome
  emoji: awesome
  statusText: ''

- statusName: deep-work
  emoji: ''
  statusText: In Focus mode

11:11 String formatting troubles. Just learnt that there's a difference between t.Log and t.Logf. Would be good to read into this later and understand why the two different methods exist. The first one formats the string with variables in it. The second one doesn't.

11:22 Riddle me this. According to the go documentation, if I want to make a field of a struct public (or anything public for that matter) I have to capitalize the name I'm giving the identifier. So technically:

type Status struct{
  statusName string
}

would mean that statusName is unreadable when using status.statusName. But in my case, it is readable.

  • Ah. It becomes inaccesible only to packages outside of its own (i.e. the ones that import it). I'm cool with that.

11:56 Related to the above. It turns out that if you pass the variable off anywhere else it's internals won't be readable. Which makes a lot of sense. Here's what happened:

  • So I was struggling with getting the package that takes text that contains yaml and converts it into an object to actually work. All I kept getting was an empty object. The way the method works is:
  • `yaml.Unmarshal([]byte(textToConvert), &variableToReturnItInto)
    • This by the way highlights one of golang's differences. Unmarshaling is taking a string and converting it into an object. Marshaling is the other way around. Except when you unmarshal, you don't actually get anything returned except for an error. When you marshal, you get both the error and the final string. Which makes sense.
      When unmarshaling, you pass a reference to the object into the method. So whatever happens inside is directly editing your object in place. So there's no need to copy anything. That said, for a first time reader, it might be a little hard to see what's happening in the code. When code follows a assignment = function_call(values) format, it's intuitive what's going on. You know what is being mutated. In the format of passing in something by reference, it vanishes into this black box and in a multi argument function call, there's no easy way to know which one changed unless you assume that only the values that need to be changed are passed in by reference (with the & sign). But then, people make mistakes. Some will say "oh let's just pass it in by reference IN CASE we need to change it". And then your code reading session goes to the toilet for the rest of the day.

12:12 Completed tests for converting a yaml document into an array of statuses. I still need to add conditions for checking if:

  • the array is formatted poorly And
  • if there are duplicate names.

Also, I need to check how to create an array/named dictionary/whatever you call it in go where I can access an element using some kind of hashtable format like statusList["status-name"] format.

One last thing for the day is a really interesting learning element on another feature of golang. Struct tags. I'll probably write up on these a bit more once I understand them more but this talk is pretty interesting and this blog post is pretty great :). My personal preference around this is that I don't quite like it. On one hand it seems pretty cool, and on the other hand it seems really arbitrary. Watch the talk. You'll see how a non standardised way of creating tags is actually inside of the official library. A different way to go about this kind of thing would be to use an interface that has a decode and encode method. Of course, having these tags allow you to do much more than just decode and encode values but every example I've seen of it so far has this feeling of "there's this feature in the language which we can use so let's use it because it's there".

Janaury 25th 2018

08:29 Starting work on the laptop. Don't have dependencies installed here. Figured this would be a good time to setup using go's dep tool.

  • Installed using the binaries available on github
  • According to the docs, to get started all I need to do is cd into the directory and run dep init.
    • This actually takes longer than I expected :D. Looks like it scans the existing code to identify dependencies and put it into the lock files which is SUPER neat for projects already in place. Adoption would be nearly effortless.

Screen-Shot-2018-01-25-at-8.32.42-AM

  • This is great! After running the above, I then ran dep ensure and it pulled in the package I needed after which I could run go test ... on my library.
  • Commit pushed

08:38 Adding in tests for poorl formatted yaml data and duplicate names. To clarify, I'm checking for both conditions of:

  • does the yaml meet the specifications of the yaml language. AKA, does the code throw back the right error after the yaml library errors out (I assumed the yaml.v2 library errors out)
  • And does the yaml meet the specification as expected by the program. Are all the fields present? Are they the correct fields? Since the yaml library handles this part of assigning to an object I have no idea what it does if a field is missing or misnamed. Do I have to handle that myself? Let's find out.

08:44 I should modify my method now to return both an error (as err) and the StatusArray and capture both. I don't know how to throw errors though so let's find out.

  • I'm going to start by reading some package code from the go library itself.
  • Based on the code from the strings library I have to modify the function to look something like this:

struct myError {
  ValueA Type,
  ValueB Type
}

func (e *myError) Error() string{
  return fmt.SprintF("Some error message that uses %s and %s", ValueA, ValueB)
}

func functionName(inputA InputType, inputB InputType) (returnValue Type, err error){
    //do whatever
    //check some condition
    if conditionNotMet {
        return [ ], myError(someValue, someOtherValue)
    }
}

And then I need to check the error message I guess in the test.

09:10 Done! After creating a non yaml compliant string, I passed it into the function to read and tested the output. Relevant commit

January 26th 2018

Didn't get to do anymore work on this yesterday. Hoping to get a bit more in today.

08:18 Picking up where I left off, I need to add tests to make sure that the yaml provided matches what I need even if it is correct according to the yaml spec.

  • For the first test, I'm going to check if when statusName is given without the correct capitalization, if the error is thrown as required. At some point I think I may have to write my custom error struct which I'm actually looking forward to :).

08:23 So as I suspected, unmarshalling the yaml doesn't throw an error if an unexpected field doesn't exist (or if an expected field doesn't exist at all). I'm wondering if there's something in the yaml.v2 library that can allow me to specify a field as strictly required.

08:26 Oh COME ON!!! The doc site is down!

08:30 Looks like it was my VPN causing this. I really ought to switch away from Hotspot Shield Elite.

08:35 Sweet! No extra work required on my part. Just need to add some better print statements.

08:48 Took a break to chat to a co-working space member who I'm meeting for the first time. Back to work :)

  • Read some documentation to make sure I'm using printf and println correctly. Println seems to be used if you want to dump some variables out into a line with a newline attached at the end. I could be wrong here.

08:56 On to the last step. Checking if the array contains duplicate keys. This is going to be an interesting challenge. The way unmarshal works is by dumping directly into the array. Meaning that I have to dump into the array and THEN read the unmarshalled values to see if any duplicates exist.

  • There's probably a better way to do this using map or something. But for now, let's get this working. We aren't going to bring down the world with our poor CLI optimization :D

09:15 Breaking for the moment. Couple of interesting concepts covered. Maps. Default error types. Will elaborate more when back

10:31 Discoveries made from the last update.

  • In order to check for duplicate keys, after loading the values into an array, we need to check the array to see if any duplicate values exist. To do this, it turns out that there is NO in command or .exists or find() methods in the language. I need to actually check for the value myself. These quirks of golang are fascinating to me.
  • In order to find the value, I can either write my own function to use throughout the program. The function would accept a "search string/value" and an array to search through. And then you iterate through the array and see if you can find the value.
  • Alternatively (and this is what I am doing), you can start placing values into a map and before putting values in, you check if the value of a key is empty or not. How to do that? I don't know really. That's what I'm about to find out. But if it isn't empty then you know a value already exists.
  • Related to the above is what error to throw in the event that I do find a duplicate. Previously I assumed I would need to create my own error type Struct but it turns out that for the most common type of error which is one that just has a string, the errors package something called a errorString. We can create it by calling error.New("the error string that I want to throw"). The value returned will match the error interface by implmenting the Error() method.

At this point, go's little oddities are growing on me. I'll probably always prefer Python or Ruby's opinionated and more obvious styles. But I can learn to have fun with this wild west do-it-whatever-you-want-to way that go seems to be open to.

11:08 After a quick break to try and put my son back to sleep, I'm looking into how to find out if a value exists in a map. A zero value will be returned if there is not matching key inserted into the map. So if the map expects int values it'll return 0. If it expects a struct, it'll return according to the rules mentioned in struct literals. I can't tell though if this:

if mapOfStatuses["nonexistentkey"] == Status { } 

is good or bad. I'm not sure if it's going to be busy creating lots of empty objects. While I'm not interested in optimization, at the same time, I'm not interested in creating a lot of empty objects for no reason.

11:16 This just reminded me. I'm not checking if the values returned are correct. Like a status can have an empty emoji OR and empty status text but not both. It also cannot have an empty status name.

  • I'm going to write the tests for this, and then use the following to check for a non existen key:
if mapOfStatuses["nonexistentkey"].StatusName == "" {
    // this tells me that the key doesn't exist.
}

11:50 Including a couple of interruptions, finished :) . The model code to check if models are read in and constructed correctly is done. Onwards to actually posting updates. Final commit is here

This post is part of a series of logs exploring how I'm building a CLI for updating my slack status. You can follow the full project here

Posted on January 23 2018 by Adnan Issadeen