Slack Status Updater (CLI) - Defaults, and adding CRUD

March 28th 2018

13:30 After using the CLI for a while I think what I'd really like to do now is to be able to unset the status entirely and set the my availability to online and away. But before I do that, I'm going to get this CLI ready to be shared with people and start adding the CRUD operations into it. Some of this might involve moving some code around between the core and the CLI itself. I'm not entirely sure. For this session, I want to :

  1. Make the CLI setup default files with templates and ask for the token value to setup the config file if there is a need.
  2. Add in the ability to add and remove statuses using slackstatus add/remove.
  3. I'm not entirely sure about editing. It should be easy, but I'll leave it to the last since it can technically be achieved by removing a status and readding it with the edited values.

But first, a lunch break.

14:37 I'll start with the CLI setting up default files.

15:47 Had to pause to look into some work that came up. But I did finish the task of getting the code in that:

  1. Sets up the statuses.yaml and tokenconfig file if they don't exist already
  2. Adds in a list of default statuses to statuses.yaml if it didn't exist at the time of checking.

The next thing I need to do is hook up the CLI to actually call this function whenever it is run. Simple enough.

After that, I need to add in is reading in a user input to get the token value into the tokenconfig file.

15:53 Done with hooking up the CLI to call the function to create the files needed. Next!

16:17 Had to correct some code that I had written when creating the default files. I had copy pasted too much and was pasting the default status values into the tokenconfig file too. And I was wondering why my tokenconfig file wasn't being shown as empty but I didn't bother to check inside it. Doh! But with that done, I can't seem to get this code working:

data := ""
fmt.Print("Please enter the slack token value: ")

This just prints and skips along.

16:22 Ah that's covered in this stack overflow which is there in my search I shared. Pfft. Need to pass data in as a pointer variable.

16:26 Aaaaand we are through! Step 1 on the road to a shareable release is done. I need to edit the README now.

16:37 Done!

May 7th 2018

CRUUUUUD! It's been a while since I last worked on this project. I was on the Buffer retreat and we've been in a new cycle and I was super busy researching encryption. A topic which I'm currently trying to break down into layman's terms and add to the engineering blog. Anyhoos. It's time to stretch the golang muscles again and get to adding CRUD to this project. First up, booting up my mind :D . The time is 16:38

17:01 Time for some overengineering. I was never very happy about having the list method inside of the CLI code instead of the Core. Right now, the CLI reads the statuses.yaml file and generates statuses from it. But if that's where that code lives, where does the code for adding and editing a status live? Where should the logic of "cannot add a new status that has a duplicate key" go? Ideally all this should be in the core, but right now it's in the CLI. Which means I have to rewrite a little bit.

  • What I am going to do is create a StatusCRUD interface who has the methods to list, find by key, add, and update a status. This will be in the core. The CLI will implment it using a FileStatusCRUD implementation or something along those lines.
  • This does mean going and reading all about interfaces on the Go documentation site.

17:48 Had some work to look into. Getting back to this after reading the docs. I can't pretend that I really understand what I'm reading. I have a general guage of how I would define and use the interface. So, something like this:

//this would be in the core somewhere
type StatusCRUDInterface interface {
  List() []Status
  GetByKey() Status
  Add() error
  Edit() error

func GetStatuses(crudImplementation StatusCRUDInterface) []Status{
   return crudImplementation.List()

//and this would be from the CLI
func cliFunction() {
   var sci ssucore.StatusCrudInterface
   sci = FileReadingImplementationOfInterface //I have no idea if this line is right
   statuses := ssucore.GetStatuses(sci)

Basically I have an issue where I don't necessarily want my implmentation to have a type. What then? Testing time

18:06 So no matter what I try I have to give the implmentation a type. Feels like a missed opportunity somewhere but in this case, what I'll do is give the implmentation a type of string where the string refers to the folder where the configuration should live.

18:23 While I was at it, I finally gave in and decided to use my current folder as the GOPATH and installed all the tools that VS Code wanted for its golang extension to work well. Turns out I should have done that a long time ago. Being able to click "Run test" from inside the editor is a game changer. Also, I tested the func GetStatuses(crudImplementation StatusCRUDInterface) style function where I pass an implmentation into another function that's expecting an interface type entity. Yey! All the building blocks are in place. Just need to fit them together

18:41 Calling it a night. I have my first test for calling the list method of the StatusCRUDInterface implmentation

May 8th 2018

16:56 Aaaaand we are back for a highly focused one hour of work. Today it's going to be transferring the methods to Core and integrating it back into the CLI if I have time. Go go power rangers! No? K.

17:31 Refined the interface and the tests a bit. Yesterday I assumed that my interface could not have the same public method as another public method inside the same package. i.e.

type MyInterface interface {

//And I thought I couldn't have something like this:

func MyPublicMethod(mi MyInterface) { //thought this would need to be a different name
    return mi.MyPublicMethod()

Turns out I was wrong. So refactored names

17:51 Done with logic to add. This had some validation included. I'm bundling all the CRUD into one commit so I'll add that work at the end.

17:57 Scratch that. Editing has a few more cases that I need to think of and I had a hard stop time set at 18:00 today to make time for reflections and planning for the next day. Going to commit work done so far.

May 10th 2018

17:19 Going straight into trying to finish off the edit functionality today.

17:24 Small change of plans. Going to implement deleting statuses first. Think I can make use of that to edit a status.

  • Done. Now towards editing

17:45 In the interest of shipping quickly, I made it so that the core deletes and re adds a status when it is being edited. That way I can defer thinking about how to handle YAML documents till later.

17:49 Quickly added in verification that the status objects being passed to the edit function are valid status objects indeed. Time to commit, merge, and move back to the CLI.

17:56 Getting better at this dep management stuff. Merged my commit to master, and ran dep ensure -update in the cli repo and boom! Done updating. Quick quick quick!

18:16 Started work on the CRUD implmentation that works with files. For the moment I've stubbed out the methods required by the interface. I'll write tests for each of them and fill them up. This is the meat of the work in the whole migrating to a CRUD logic that comes from the core.

18:29 A golang peculiarity to end the day! Yey!

So now comes the moment of truth where I actually have to use the implementation of the interface. When I was trying to create an interface implementation for the first time I tried to look for a concept of a "type-less" implementation. But golang doesn't allow it. You HAVE to give your implementation a "type".

So I give my implementation the type of string. The idea was that I'd instantiate it with the path to the statuses file. Now, I can do something like this:

type statusCRUDFileImplementation string

func (s statusCRUDFileImplementation) GetStatuses() []ssucore.Status {
	if s != "" {
        //return statuses
    return []ssucore.Status{}

Which is NEAT! You'd think ha! I can use s as a string! Which totally means that if a method/function needs a string as a parameter, I can just pass s in right?

BAM! No you can't. You have to explicitly cast it using string(s) when passing it into a function even though go knows damn well what type of variable the function expects.

The fun part? If I try to write if s != 0 I get a compile error slapped into my face that an int and string cannot be compared. So go is smart enough to do that but not automatically cast this implementation which is obviously a string. I'd love to dig into why this is the case. Later though. I'm going to miss my goal today.

May 14th 2018

09:03 Doing a quick morning session since I left some stuff related to work uncommitted and don't have any way to access it from my laptop. Back to CRUD'ing my way to an implementation of my interface that will allow me to handle statuses via a filesystem.

  • It's interesting to see how efficient it is to just jump into development on a machine that is way behind in sync with the work done so far.
  • Also also interesting is how out of sync my dev environment is with how I've been working so far. On my main machine I had all these fancy golang packages installed that allowed me to test code inside of the editor and make easy refactors. Now I'd have to go back and configure all of those again. Some part of me def feels like I should read the documentation of these editors better instead of spending 30 minutes a day on HN.
    • I also feel like it's a good lesson not to get too dependent on the editor to do work where a simple alt-tab + cli instruction might suffice.

09:29 And we've implemented our second CRUD method. I can now retrieve statuses by key. While writing this I reflected on the fact that I have one file (statuscrud_file.go) that implements the interface for all CRUD methods on statuses. I also have a file called filereader.go that carries the functions to read data from a file INCLUDING a function called "getStatusesFromFile". I don't feel too good about this to be honest. It feels like the work is being spread out in a way that doesn't make sense. Leaving this as a note to myself to probably refactor everything into one.

09:42 Another reflection while writing code to add a new status to the file. The function for reading in a list of statuses as a yaml document are in the core. Now that I've gotten this far it feels like that is purely a concern for whichever part of the codebase handles reading text from a file. This is something else that needs to be refactored out of its existing place.

09:55 To whoever who wrote the "WriteFile" function of golang without any sensible defaults, you are very smart. We get it. You want smart people who understand FileMode's and whatnot to use your code. But really, if I want to write to a file surely you could have transferred some of your smarts to the code and had it figure out what I want to do and figure out what FileModes and permissions to enable?

10:12 And the add status method is finally complete. And I've acquired knowledge on how to edit slices (stuff like arrays but "smarter"). Now onwards to delete.

10:24 Annnd we are done with delete. I'm going to have to pause here and make a commit. But edit is internally running the whole "add and delete" methods together so it shouldn't take long to complete. This whole add and delete business is not good and needs to be refactored asap. The reason it's not good is that the core is the one doing the whole logic of deleting first, then adding the "edited" status back in. Instead the interface should have a method called EditStatus. How EditStatus chooses to implement the delete is entirely its own concern and in the case of the file system implementation it would be to delete first, then re-add.

Posted on March 28 2018 by Adnan Issadeen