At the time I was looking for an excuse to use both F# and Azure Functions for a real project, and found it when I got frustrated navigating the Covid vaccination information to get the info I really wanted: how many people are completely vaccinated?
The bot retrieves this number once a day from official CDC data and tweets it out. It can do so for any US state, but currently I’m doing only Washington and the USA as a whole. No one wants to get 51 tweets a day, and creating a bot per state is painful because it requires different Twitter accounts. I think? Didn’t research this much.
Creating a Twitter developer account was straightforward. There was just one surprisingly hidden bit of information. My developer account is my main Twitter account from which I tweet, which is probably not best practice, but I assume is the case for many hobby projects. But I want the bot to tweet on its own account, how do I do that? - Stackoverflow has the answer.
Honestly, I don’t remember exactly how I created the project and set everything up, but it was fairly automated. Development on the Azure and F# tooling in VS code is very active anyway and things might be different from a year ago.
I use Ionide for F# development and installed the “Azure Functions” extension, basically following Develop Azure Functions by using Visual Studio Code . These instructions are for C# but they worked for F# as well, except I ran into an issue with a missing host.json solved by adding an explicit copy in the fsproj file.
The end result is a really nice and convenient integration into VS Code, where I can deploy the function and stream logs directly from a menu. Not exactly automated or reproducible or properly logged but just fine for a hobby project.
The other thing where the Microsoft docs were not clear to me was how exactly the Key Vault integration worked, which I needed for the Twitter API secrets. Daniel’s How to keep secrets safe using Key Vault cleared that up quickly.
The function runs on a schedule, which is defined directly on its entry point:
[<FunctionName("TweetCdcDataTimer")>] // 8p EST = 1a UTC let runTimer ([<TimerTrigger("0 0 1 * * *")>] myTimer: TimerInfo, log: ILogger) = let msg = sprintf "Time trigger function executed at: %A" DateTime.Now log.LogInformation msg run log
It’s nothing fancy, but I really enjoyed writing my first real F# program. I feel like it could be factored better, but at the same time it’s still easy to read coming back to it after a few months - a testament to F# more than to my beginner code.
Here’s how we create a type that represents the CDC’s vaccination:
type CdcData = JsonProvider<Sample="sampleVaxData.json">
Yep, that’s it. Just give the type provider a sample. That allows us to query it naturally, e.g., for the completed vaccination percentage for a given location:
let getFullyVaccinatedPercentage (vaxData: CdcData.VaccinationData array) location = let locationRow = vaxData |> Array.where (fun row -> row.Location = location) |> Seq.exactlyOne locationRow.SeriesCompletePopPct
Honestly, one of the harder part was getting the edge cases of the progress bar right :) It needs to work for all percentages and always have the same number of characters. The generator function scales the percentage to the number of characters available and use
String.replicate to make the correct number of characters. I ended up writing a bunch of tests using FsUnit.Xunit, which was a nice experience. Defining some helpers, each test is very succinct.
let firstNCharactersShouldBeFilled n bar = bar |> Seq.take n |> Seq.forall (fun c -> c = filled) |> should be True let charactersFromNShouldBeEmpty n bar = bar |> Seq.skip (n - 1) |> Seq.forall (fun c -> c = empty) |> should be True // The progress part should be the first n characters let shouldBeFilledUntil n bar = firstNCharactersShouldBeFilled n bar charactersFromNShouldBeEmpty (n + 1) bar [<Fact>] let ``progress bar with 0%`` () = progressBar 50 0.0m |> shouldBeFilledUntil 0 [<Fact>] let ``progress bar of length 20 1 completed`` () = progressBar 20 5.0m |> shouldBeFilledUntil 1
Alright, the code is straightforward so I won’t repeat more of it here. Browse or check out the repository!