michaelheap.com Thoughts on leadership, code and how to fix odd edge cases in tools (not necessarily in that order) 2024-09-12T09:26:22Z https://michaelheap.com Michael Heap m@michaelheap.com Flatten nested foreach loops with Terraform 2024-09-12T09:26:22Z https://michaelheap.com/terraform-flatten-nested-loops/ <p>Given a list of identifiers and an associated list of values, here's how to flatten them all down to a single list of unique values.</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">hcl</div><div class="code-container"><code><div class="line"><span style="color: #616E88"># Define your input data. We'll convert this in to a list of ${name}-${version} values</span></div><div class="line"><span style="color: #81A1C1">variable</span><span style="color: #A3BE8C"> "ai_providers"</span><span style="color: #D8DEE9FF"> {</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">type</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> list(object({</span></div><div class="line"><span style="color: #D8DEE9FF"> name = string</span></div><div class="line"><span style="color: #D8DEE9FF"> versions = list(string)</span></div><div class="line"><span style="color: #D8DEE9FF"> }))</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">default</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> [</span></div><div class="line"><span style="color: #D8DEE9FF"> {</span></div><div class="line"><span style="color: #D8DEE9FF"> name = </span><span style="color: #A3BE8C">"OpenAI"</span></div><div class="line"><span style="color: #D8DEE9FF"> versions = [</span><span style="color: #A3BE8C">"gpt-3.5-turbo"</span><span style="color: #D8DEE9FF">, </span><span style="color: #A3BE8C">"gpt-4o"</span><span style="color: #D8DEE9FF">]</span></div><div class="line"><span style="color: #D8DEE9FF"> },</span></div><div class="line"><span style="color: #D8DEE9FF"> {</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">name</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">"Anthropic"</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">versions</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> [</span><span style="color: #A3BE8C">"Opus"</span><span style="color: #D8DEE9FF">, </span><span style="color: #A3BE8C">"Sonnet"</span><span style="color: #D8DEE9FF">, </span><span style="color: #A3BE8C">"Haiku"</span><span style="color: #D8DEE9FF">]</span></div><div class="line"><span style="color: #D8DEE9FF"> },</span></div><div class="line"><span style="color: #D8DEE9FF"> {</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">name</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">"Mistral"</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">versions</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> [</span><span style="color: #A3BE8C">"7B"</span><span style="color: #D8DEE9FF">, </span><span style="color: #A3BE8C">"8x7B"</span><span style="color: #D8DEE9FF">, </span><span style="color: #A3BE8C">"8x22B"</span><span style="color: #D8DEE9FF">]</span></div><div class="line"><span style="color: #D8DEE9FF"> },</span></div><div class="line"><span style="color: #D8DEE9FF"> {</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">name</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">"llama3"</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">versions</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> [</span><span style="color: #A3BE8C">"8B"</span><span style="color: #D8DEE9FF">, </span><span style="color: #A3BE8C">"70B"</span><span style="color: #D8DEE9FF">]</span></div><div class="line"><span style="color: #D8DEE9FF"> },</span></div><div class="line"><span style="color: #D8DEE9FF"> ]</span></div><div class="line"><span style="color: #D8DEE9FF">}</span></div><div class="line"></div><div class="line"><span style="color: #616E88">## Flatmap using nested loops and flatten()</span></div><div class="line"><span style="color: #81A1C1">locals</span><span style="color: #D8DEE9FF"> {</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">ai_providers_flattened</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> flatten([</span></div><div class="line"><span style="color: #D8DEE9FF"> for provider in var</span><span style="color: #B48EAD">.</span><span style="color: #D8DEE9FF">ai_providers : [</span></div><div class="line"><span style="color: #D8DEE9FF"> for version in provider</span><span style="color: #B48EAD">.</span><span style="color: #D8DEE9FF">versions : {</span></div><div class="line"><span style="color: #D8DEE9FF"> version = version</span></div><div class="line"><span style="color: #D8DEE9FF"> provider = provider</span></div><div class="line"><span style="color: #D8DEE9FF"> key = </span><span style="color: #A3BE8C">"</span><span style="color: #81A1C1">${provider</span><span style="color: #D8DEE9FF">.name</span><span style="color: #81A1C1">}</span><span style="color: #A3BE8C">-</span><span style="color: #81A1C1">${version}</span><span style="color: #A3BE8C">"</span></div><div class="line"><span style="color: #D8DEE9FF"> }</span></div><div class="line"><span style="color: #D8DEE9FF"> ]</span></div><div class="line"><span style="color: #D8DEE9FF"> ])</span></div><div class="line"><span style="color: #D8DEE9FF">}</span></div><div class="line"></div><div class="line"><span style="color: #616E88"># Iterate over the flattened list to create resources</span></div><div class="line"><span style="color: #81A1C1">resource</span><span style="color: #A3BE8C"> "demo_foo" "my_resource"</span><span style="color: #D8DEE9FF"> {</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">for_each</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> { for api in local.ai_providers_flattened : api.key =&gt; api }</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">name</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> each</span><span style="color: #B48EAD">.</span><span style="color: #D8DEE9FF">value</span><span style="color: #B48EAD">.</span><span style="color: #D8DEE9FF">provider</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">version</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> each</span><span style="color: #B48EAD">.</span><span style="color: #D8DEE9FF">value</span><span style="color: #B48EAD">.</span><span style="color: #D8DEE9FF">version</span></div><div class="line"><span style="color: #D8DEE9FF">}</span></div></code></div></pre> <p>The <code>for_each = { for api in local.ai_providers_flattened : api.key =&gt; api }</code> line confused me for a while. This line is used to identify the resource later as <code>my_resource</code> is now a map of objects rather than a single object. Let's walk through it step by step</p> <ul> <li><code>demo_foo.my_resource</code> is a map of objects</li> <li><code>demo_foo.my_resource[api.key]</code> points to a single resource</li> <li><code>api.key</code> is defined as <code>&quot;${provider.name}-${version}&quot;</code></li> <li>Details for the <code>demo_foo</code> resource for OpenAI gpt-4o is available at <code>demo_foo.my_resource[&quot;OpenAI-gpt-4o&quot;]</code></li> </ul> Bind a Hyper/Meh key with Keychron Launcher 2024-09-05T07:55:33Z https://michaelheap.com/keychron-launcher-hyper-meh/ <p>I've been using a <a href="https://www.keychron.com/products/keychron-q1-he-qmk-wireless-custom-keyboard">Keychron Q1 HE</a> this week instead of my trusty Ergodox EZ as I've been travelling.</p> <p>One of the biggest issues I had is that none of my keyboard shortcuts were working as I didn't have a <code>hyper</code> key bound. I could have used something like Karabiner to rebind keys, but I knew I could do it at the keyboard level.</p> <p>It took me far too long to figure out, so I'm writing it down for future me.</p> <h2 id="configuring-hyper%2Fmeh-keys" tabindex="-1">Configuring Hyper/Meh keys</h2> <p>First of all, VIA App doesn't work for the Q1 HE. You have to use the Keychron configuration UI at <a href="https://www.launcher.keychron.com/#/keymap">https://www.launcher.keychron.com/</a>.</p> <p>I learned about the <code>Any</code> key definition from <a href="https://www.reddit.com/r/Keychron/comments/17yv7nk/comment/ks710n9/">this useful Reddit comment</a>. On the keymap screen, select <code>Custom</code> then <code>Any</code>. It will show a text input where you should put the keys to send.</p> <p><em>Be aware - the key that you're binding will change as you type on the keyboard. After entering the keys, click on the key that you want to bind before pressing Enter.</em></p> <p>To create a <code>Hyper</code> (Left Control + Shift + Alt + GUI) key, enter <code>HYPR(KC_NO)</code>. To create a <code>Meh</code> (Left Control + Shift + Alt) key, enter <code>MEH(KC_NO)</code>.</p> <p>The <code>GUI</code> key is <code>cmd</code> on MacOS, and the Windows key on Windows.</p> <p>The <code>KC_NO</code> parameter tells QMK not to send any additional keycodes with the modifiers. This allows you to press other keys on the keyboard.</p> <p>Personally, I like to define a <code>LCAG(KC_NO)</code> (Left Control + Alt + GUI) key. This means I can have a single key as my keyboard trigger, and double the number of available shortcuts by additionally pressing <code>Shift</code>.</p> <h2 id="learn-more-about-qmk" tabindex="-1">Learn more about QMK</h2> <p>The full list of modifier keys is available in the <a href="https://docs.qmk.fm/feature_advanced_keycodes#modifier-keys">QMK docs</a>.</p> <p>I've used prebuilt modifiers such as <code>HYPR</code> and <code>LCAG</code>, but you can also nest them to build composite modifiers. e.g. Send <code>ctrl+alt</code> with <code>LCTL(LALT(KC_NO))</code>. You can change <code>KC_NO</code> for a key to send a full key combination, e.g. <code>ctrl+alt+delete</code> with <code>LCTL(LALT(KC_DEL))</code></p> Downloading webcomics with Dosage 2024-05-20T16:25:59Z https://michaelheap.com/download-webcomics/ <p>I’ve been reading some webcomics for a few years, but never went back to the beginning to learn how each character was introduced. I liked the idea, but with 5000+ posts in some feeds, I constantly lost my position and/or had better things to do.</p> <p>I recently spent some time on holiday and decided to try and copy the whole series on to my iPad to read. I intended to write a small app to download a series and convert to a <code>.cbz</code> file, but it turns out it already exists.</p> <h2 id="dosage" tabindex="-1">Dosage</h2> <p><a href="https://dosage.rocks/">Dosage</a> is a Python CLI designed to download webcomics in to a folder. I always have issues installing Python apps, so I looked to see if Dosage was available as a Docker image. It wasn’t, but it wasn’t too difficult to package up.</p> <p>If you want to do the same, create a <code>Dockerfile</code> with the following contents:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">docker</div><div class="code-container"><code><div class="line"><span style="color: #81A1C1">FROM</span><span style="color: #D8DEE9FF"> python:bullseye</span></div><div class="line"></div><div class="line"><span style="color: #616E88"># Install pipx</span></div><div class="line"><span style="color: #81A1C1">RUN</span><span style="color: #D8DEE9FF"> python3 -m pip install --user pipx \</span></div><div class="line"><span style="color: #D8DEE9FF"> && python3 -m pipx ensurepath</span></div><div class="line"></div><div class="line"><span style="color: #616E88"># Install dosage</span></div><div class="line"><span style="color: #81A1C1">RUN</span><span style="color: #D8DEE9FF"> /root/.local/bin/pipx install </span><span style="color: #A3BE8C">"dosage[css,bash] @ git+https://github.com/webcomics/dosage.git"</span></div><div class="line"></div><div class="line"><span style="color: #81A1C1">ENTRYPOINT</span><span style="color: #D8DEE9FF"> [</span><span style="color: #A3BE8C">"/root/.local/bin/dosage"</span><span style="color: #D8DEE9FF">]</span></div></code></div></pre> <p>Build the image (you don’t need to clone the Dosage repo - the Dockerfile will do it for you):</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">docker build -t dosage </span><span style="color: #88C0D0">.</span></div></code></div></pre> <p>Then finally, add an alias for the a <code>dosage</code> command. This is optional, but encouraged:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #88C0D0">alias</span><span style="color: #D8DEE9FF"> dosage=</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">docker run --rm -v $(pwd):/Comics dosage:latest</span><span style="color: #ECEFF4">'</span></div></code></div></pre> <p>Finally, run <code>dosage</code> to download the comic of your choice:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">dosage --list</span></div><div class="line"><span style="color: #D8DEE9FF">dosage -a TheOrderOfTheStick</span></div></code></div></pre> <h2 id="create-a-cbz" tabindex="-1">Create a CBZ</h2> <p>A <code>.cbz</code> file is a renamed zip file. To create one:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">zip -r oots.cbz TheOrderOfTheStick/</span><span style="color: #81A1C1">*</span></div></code></div></pre> <p>Now you can copy the <code>.cbz</code> in to the comic app of your choice.</p> Presentations on an ultrawide monitor 2024-05-13T09:17:51Z https://michaelheap.com/presenting-ultrawide/ <p>I <em>love</em> having an ultra wide monitor, but it does make presenting tricky at times. All of the available conferencing systems allow you to share either a single window, or all of your screen. For most people, sharing a single window which is the right size works.</p> <p>For me, it doesn’t. Most of the time I’m switching between a code editor, web browser and terminal. I need a way to share a 1080p section of my screen, no matter which window it’s showing.</p> <p>Here are the two options I use:</p> <ol> <li>Use Zoom to share a specific area of the screen + Hammerspoon to position the windows</li> <li>Use <a href="https://github.com/Stengo/DeskPad">DeskPad</a> + any other conferencing tool</li> </ol> <h2 id="zoom-and-hammerspoon" tabindex="-1">Zoom and Hammerspoon</h2> <p>This is the approach I use 99% of the time. It fits my existing workflow with a single display and allows me to drag things in to view as needed (or rather, use the Hammerspoon shortcut below).</p> <p>This approach consists of two sections. The first is to position the window where it needs to be using Hammerspoon. I have a keyboard binding for this:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">lua</div><div class="code-container"><code><div class="line"><span style="color: #81A1C1">function</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">setWindowPosition</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">key</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">w</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9">h</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9">x</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9">y</span><span style="color: #ECEFF4">)</span></div><div class="line"><span style="color: #D8DEE9FF"> hs.</span><span style="color: #D8DEE9">hotkey</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">bind</span><span style="color: #D8DEE9FF">({</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">cmd</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">, </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">alt</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">, </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">ctrl</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">, </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">shift</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">}, key, </span><span style="color: #81A1C1">function</span><span style="color: #ECEFF4">()</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">local</span><span style="color: #D8DEE9FF"> win </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> hs.</span><span style="color: #D8DEE9">window</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">focusedWindow</span><span style="color: #D8DEE9FF">()</span></div><div class="line"></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">local</span><span style="color: #D8DEE9FF"> cursize </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> win:</span><span style="color: #88C0D0">size</span><span style="color: #D8DEE9FF">()</span></div><div class="line"><span style="color: #D8DEE9FF"> cursize.</span><span style="color: #D8DEE9">w</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> w</span></div><div class="line"><span style="color: #D8DEE9FF"> cursize.</span><span style="color: #D8DEE9">h</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> h</span></div><div class="line"></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">local</span><span style="color: #D8DEE9FF"> f </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> win:</span><span style="color: #88C0D0">frame</span><span style="color: #D8DEE9FF">()</span></div><div class="line"><span style="color: #D8DEE9FF"> f.</span><span style="color: #D8DEE9">x</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> x</span></div><div class="line"><span style="color: #D8DEE9FF"> f.</span><span style="color: #D8DEE9">y</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> y</span></div><div class="line"></div><div class="line"><span style="color: #D8DEE9FF"> win:</span><span style="color: #88C0D0">setFrame</span><span style="color: #D8DEE9FF">(f)</span></div><div class="line"><span style="color: #D8DEE9FF"> win:</span><span style="color: #88C0D0">setSize</span><span style="color: #D8DEE9FF">(cursize)</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">end</span><span style="color: #D8DEE9FF">)</span></div><div class="line"><span style="color: #81A1C1">end</span></div><div class="line"></div><div class="line"><span style="color: #88C0D0">setWindowPosition</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">P</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">, </span><span style="color: #B48EAD">1920</span><span style="color: #D8DEE9FF">, </span><span style="color: #B48EAD">1080</span><span style="color: #D8DEE9FF">, </span><span style="color: #B48EAD">2420</span><span style="color: #D8DEE9FF">, </span><span style="color: #B48EAD">285</span><span style="color: #D8DEE9FF">) </span><span style="color: #616E88">-- Right</span></div></code></div></pre> <p><code>2420, 285</code> places the window 2420px from the left, and 285px down from the top of my screen. This is just below where my camera is mounted, allowing me to see the window and also look at the camera.</p> <p>The second trick is to use Zoom’s “Portion of screen” option and select the area of the screen that your window covers.</p> <div class="w-2/3 m-auto"> <div class="image-wrapper "><picture> <source class="" type="image/webp" srcset="https://michaelheap.com/images/presenting-ultrawide/zoom-advanced-share.png/Yxd1gRTGKy-320.webp 320w, https://michaelheap.com/images/presenting-ultrawide/zoom-advanced-share.png/Yxd1gRTGKy-640.webp 640w, https://michaelheap.com/images/presenting-ultrawide/zoom-advanced-share.png/Yxd1gRTGKy-960.webp 960w" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, (max-width: 960px) 960px, 100vw" /> <img class="" alt="Zoom advanced sharing options" src="https://michaelheap.com/images/presenting-ultrawide/zoom-advanced-share.png/Yxd1gRTGKy-960.jpeg" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, (max-width: 960px) 960px, 100vw" srcset="https://michaelheap.com/images/presenting-ultrawide/zoom-advanced-share.png/Yxd1gRTGKy-320.jpeg 320w, https://michaelheap.com/images/presenting-ultrawide/zoom-advanced-share.png/Yxd1gRTGKy-640.jpeg 640w, https://michaelheap.com/images/presenting-ultrawide/zoom-advanced-share.png/Yxd1gRTGKy-960.jpeg 960w" width="960" height="629" /> </picture></div> </div> <p>At this point the core problem is solved, but there’s one remaining thing that frustrated me. The app switcher shows when I <code>cmd+tab</code>, but some icons are cut off as I’m only sharing part of the screen.</p> <p>To solve this, I bind specific windows to a keyboard shortcut with Hammerspoon and switch using the keyboard. This prevents the app switcher from showing on screen.</p> <p>I use <code>cmd+alt+ctrl+shift+y</code> to bind the focused window to <code>y</code>, and <code>cmd+alt+ctrl+y</code> to switch to it instantly. I have six hotkeys bound, which provides plenty of windows if I need them.</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">lua</div><div class="code-container"><code><div class="line"><span style="color: #81A1C1">function</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">windowSwitch</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">binder</span><span style="color: #ECEFF4">)</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">local</span><span style="color: #D8DEE9FF"> windowId </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">0</span><span style="color: #D8DEE9FF">;</span></div><div class="line"><span style="color: #D8DEE9FF"> hs.</span><span style="color: #D8DEE9">hotkey</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">bind</span><span style="color: #D8DEE9FF">({</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">cmd</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">, </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">alt</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">, </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">ctrl</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">, </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">shift</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF"> }, binder, </span><span style="color: #81A1C1">function</span><span style="color: #ECEFF4">()</span></div><div class="line"><span style="color: #D8DEE9FF"> windowId </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> hs.</span><span style="color: #D8DEE9">window</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">focusedWindow</span><span style="color: #D8DEE9FF">():</span><span style="color: #88C0D0">id</span><span style="color: #D8DEE9FF">();</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">end</span><span style="color: #D8DEE9FF">)</span></div><div class="line"></div><div class="line"><span style="color: #D8DEE9FF"> hs.</span><span style="color: #D8DEE9">hotkey</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">bind</span><span style="color: #D8DEE9FF">({</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">cmd</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">, </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">alt</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">, </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">ctrl</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">}, binder, </span><span style="color: #81A1C1">function</span><span style="color: #ECEFF4">()</span></div><div class="line"><span style="color: #D8DEE9FF"> hs.</span><span style="color: #D8DEE9">window</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">find</span><span style="color: #D8DEE9FF">(windowId):</span><span style="color: #88C0D0">focus</span><span style="color: #D8DEE9FF">();</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">end</span><span style="color: #D8DEE9FF">)</span></div><div class="line"><span style="color: #81A1C1">end</span></div><div class="line"></div><div class="line"><span style="color: #88C0D0">windowSwitch</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">y</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">)</span></div><div class="line"><span style="color: #88C0D0">windowSwitch</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">u</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">)</span></div><div class="line"><span style="color: #88C0D0">windowSwitch</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">i</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">)</span></div><div class="line"><span style="color: #88C0D0">windowSwitch</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">h</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">)</span></div><div class="line"><span style="color: #88C0D0">windowSwitch</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">j</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">)</span></div><div class="line"><span style="color: #88C0D0">windowSwitch</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">k</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">)</span></div></code></div></pre> <h2 id="deskpad" tabindex="-1">DeskPad</h2> <p>Not all conferencing systems allow you to share a portion of your screen like Zoom. If I’m presenting through an app that doesn’t allow you to share a specific area <em>and</em> I need to show multiple windows, I use <a href="https://github.com/Stengo/DeskPad">DeskPad</a>.</p> <p>DeskPad adds a virtual screen to your Mac, and an app that shows what’s on that screen. This allows you to share your entire “screen” whilst still keeping the majority of your ultra wide available for other things.</p> <p>I configure DeskPad’s screen to be above my usual screen to make it easy to drag windows on to it.</p> <p>If you’re feeling really fancy, you can combine DeskPad and Hammerspoon to move windows to DeskPad with a keyboard shortcut:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">lua</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">hs.</span><span style="color: #D8DEE9">hotkey</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">bind</span><span style="color: #D8DEE9FF">({</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">cmd</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">, </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">alt</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">, </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">ctrl</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF"> }, </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">.</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">, </span><span style="color: #81A1C1">function</span><span style="color: #ECEFF4">()</span></div><div class="line"><span style="color: #D8DEE9FF"> hs.</span><span style="color: #D8DEE9">window</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">focusedWindow</span><span style="color: #D8DEE9FF">():</span><span style="color: #88C0D0">moveToScreen</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">Deskpad</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">):</span><span style="color: #88C0D0">maximize</span><span style="color: #D8DEE9FF">()</span></div><div class="line"><span style="color: #81A1C1">end</span><span style="color: #D8DEE9FF">)</span></div></code></div></pre> <h2 id="the-end" tabindex="-1">The End</h2> <p>If you're using Zoom, I can highly recommend the first approach. I use it daily and it hasn't failed me yet. If you're using a conferencing platform that doesn't allow you to share a portion of your screen, DeskPad is a great tool that lets you share a virtual screen at the resolution you desire.</p> Kong Gateway Quickstart 2024-05-07T08:34:54Z https://michaelheap.com/kong-quickstart/ <p>A <code>$dayjob</code> related TIL today. Here's how to quickly deploy Kong Gateway locally with the Dev Portal enabled (you'll need an enterprise license).</p> <p>This is useful for testing things locally.</p> <p>Run the Gateway:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">curl -Ls https://get.konghq.com/quickstart </span><span style="color: #81A1C1">|</span><span style="color: #D8DEE9FF"> PROXY_PORT=80 bash -s -- -m \</span></div><div class="line"><span style="color: #D8DEE9FF"> -e KONG_PASSWORD=changeme \</span></div><div class="line"><span style="color: #D8DEE9FF"> -e KONG_ADMIN_GUI_URL=http://manager.example \</span></div><div class="line"><span style="color: #D8DEE9FF"> -e KONG_ADMIN_GUI_API_URL=http://admin.example \</span></div><div class="line"><span style="color: #D8DEE9FF"> -e KONG_PORTAL_GUI_PROTOCOL=http \</span></div><div class="line"><span style="color: #D8DEE9FF"> -e KONG_PORTAL_API_URL=http://portalapi.example \</span></div><div class="line"><span style="color: #D8DEE9FF"> -e KONG_PORTAL_GUI_HOST=portal.example \</span></div><div class="line"><span style="color: #D8DEE9FF"> -e KONG_PORTAL_SESSION_CONF=</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">{"cookie_name": "portal_session", "secret": "PORTAL_SUPER_SECRET", "storage": "kong", "cookie_secure": false, "cookie_domain":".example"}</span><span style="color: #ECEFF4">'</span></div><div class="line"><span style="color: #D8DEE9FF"> -e KONG_PORTAL=</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">on</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF"> \</span></div><div class="line"><span style="color: #D8DEE9FF"> -e KONG_LICENSE_DATA \</span></div><div class="line"><span style="color: #D8DEE9FF"> -t 3.4</span></div></code></div></pre> <p>Create routes that proxy based on host name:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">curl -X POST localhost:8001/services -d name=admin -d url=http://localhost:8001</span></div><div class="line"><span style="color: #D8DEE9FF">curl -X POST localhost:8001/services -d name=manager -d url=http://localhost:8002</span></div><div class="line"><span style="color: #D8DEE9FF">curl -X POST localhost:8001/services -d name=portal -d url=http://localhost:8003</span></div><div class="line"><span style="color: #D8DEE9FF">curl -X POST localhost:8001/services -d name=portalapi -d url=http://localhost:8004</span></div><div class="line"></div><div class="line"><span style="color: #D8DEE9FF">curl -X POST localhost:8001/services/admin/routes -d hosts=admin.example</span></div><div class="line"><span style="color: #D8DEE9FF">curl -X POST localhost:8001/services/manager/routes -d hosts=manager.example</span></div><div class="line"><span style="color: #D8DEE9FF">curl -X POST localhost:8001/services/portal/routes -d hosts=portal.example</span></div><div class="line"><span style="color: #D8DEE9FF">curl -X POST localhost:8001/services/portalapi/routes -d hosts=portalapi.example</span></div></code></div></pre> <p>Add those domains to your <code>/etc/hosts</code> file:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #88C0D0">echo</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">'</span></div><div class="line"><span style="color: #A3BE8C">127.0.0.1 proxy.example</span></div><div class="line"><span style="color: #A3BE8C">127.0.0.1 admin.example</span></div><div class="line"><span style="color: #A3BE8C">127.0.0.1 manager.example</span></div><div class="line"><span style="color: #A3BE8C">127.0.0.1 portal.example</span></div><div class="line"><span style="color: #A3BE8C">127.0.0.1 portalapi.example</span></div><div class="line"><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">|</span><span style="color: #D8DEE9FF"> sudo tee -a /etc/hosts</span></div></code></div></pre> <p>Now you can visit <a href="http://manager.example/">http://manager.example</a> and log in with <code>kong_admin</code> / <code>changeme</code>.</p> Designing OpenAPI Schemas 2024-04-27T19:36:48Z https://michaelheap.com/openapi-schema-design/ <p>I’ve written a <em>lot</em> of OpenAPI schemas over the last couple of years, and have developed a pattern that helps with maintenance. You have a minimum of two logical schemas for every entity in your system, <code>Foo</code> and <code>FooRequest </code>. <code>Foo</code> is a union of <code>FooRequest</code> and any computed fields from the system.</p> <p>Let’s look as a concrete example, a <code>Pet</code> in an adoption shelter. A pet has two user set fields, <code>name</code> and <code>type</code>, and one computed field, <code>created_at</code>. You can’t set the <code>created_at</code> field when creating or updating a pet, which means we have two schemas:</p> <ul> <li><code>PetRequest </code>: <code>name</code>, <code>type</code></li> <li><code>Pet</code>: <code>name</code>, <code>type</code>, <code>created_at</code></li> </ul> <blockquote> <p>The following example isn't the best way to accomplish the GET/POST split! It's shown as it's a common pattern used in many specifications, but keep reading for a better solution.</p> </blockquote> <p>When you model this with JSON schema, it looks like the following:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">yaml</div><div class="code-container"><code><div class="line"><span style="color: #8FBCBB">components</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">schemas</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">PetRequest</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">object</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">properties</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">string</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">string</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Pet</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">allOf</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">$ref</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">#/components/schemas/PetRequest</span><span style="color: #ECEFF4">"</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">object</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">properties</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">created_at</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">string</span></div></code></div></pre> <p>Any updates to the <code>PetRequest</code> object will automatically be reflected in the <code>Pet</code> object. However, this is such a common pattern that OpenAPI has keywords to help built in.</p> <h2 id="simplify-with-readonly%3A-true" tabindex="-1">Simplify with <code>readOnly: true</code></h2> <p>If you can split your schema in to &quot;user provided&quot; and &quot;computed&quot; values cleanly, you only need a single schema thanks to the <code>readOnly</code> keyword.</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">yaml</div><div class="code-container"><code><div class="line"><span style="color: #8FBCBB">components</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">schemas</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Pet</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">object</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">properties</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">string</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">string</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">created_at</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">string</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">readOnly</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">true</span></div></code></div></pre> <p>The <code>readOnly</code> keyword causes the schema to be split in to two virtual schemas - one for <code>GET</code> and one for <code>POST</code>/<code>PATCH</code>/<code>PUT</code>. Any OpenAPI renderer (I tested with Redoc) will remove <code>created_at</code> from the <code>POST</code> schema.</p> <h2 id="complex-apis" tabindex="-1">Complex APIs</h2> <p>In an ideal world, you wouldn’t need more than one single <code>Pet</code> schema. However, we live in the real world and sometimes there are additional requirements. Here are some examples:</p> <ul> <li>Pets have an <code>adopted_at</code> time, which can only be set when updating a pet, not when creating</li> <li>Pets have a <code>total_steps</code> field which is computed from an external source that is not cached and is too expensive to show when listing multiple pets</li> </ul> <p>These requirements mean that we have to split <code>Pet</code> in to two, <code>Pet</code> and <code>CreatePetRequest</code>. We also need a <code>MinimalPet</code> representation for the <code>GET /pets</code> endpoint. This results in three distinct schemas:</p> <ul> <li><code>CreatePetRequest</code>: <code>name</code>, <code>type</code></li> <li><code>Pet</code>: <code>name</code>, <code>type</code>, <code>adopted_at</code> (readOnly), <code>created_at</code> (readOnly)</li> <li><code>PetWithDetails</code>: <code>name</code>, <code>type</code>, <code>adopted_at</code> (readOnly), <code>total_steps</code> (readOnly), <code>created_at</code> (readOnly)</li> </ul> <p>These schemas can be composed to build the entities we need at runtime. <code>CreatePetRequest</code> is the base as it contains the minimum available information. <code>Pet</code> builds on this by adding <code>adopted_at</code> and <code>created_at</code>. Finally, <code>PetWithDetails</code> adds computed fields that are expensive to calculate such as <code>total_steps</code>.</p> <p><code>CreatePetRequest</code> -&gt; <code>Pet</code> -&gt; <code>PetWithDetails</code>.</p> <p>Expressed using JSON schema, it looks like this:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">yaml</div><div class="code-container"><code><div class="line"><span style="color: #8FBCBB">components</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">schemas</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">CreatePetRequest</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">object</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">properties</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">string</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">string</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Pet</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">allOf</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">$ref</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">#/components/schemas/CreatePetRequest</span><span style="color: #ECEFF4">"</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">object</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">properties</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">adopted_at</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">string</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">created_at</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">string</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">readOnly</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">true</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">PetWithDetails</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">allOf</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">$ref</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">#/components/schemas/Pet</span><span style="color: #ECEFF4">"</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">object</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">properties</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">total_steps</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">string</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">readOnly</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">true</span></div></code></div></pre> <p>This gets expanded to the following schemas (courtesy of Swagger UI):</p> <div class="image-wrapper "><picture> <source class="m-auto" type="image/webp" srcset="https://michaelheap.com/images/openapi-schema-design/swagger-ui-schemas.png/5PIDCJlZnV-320.webp 320w, https://michaelheap.com/images/openapi-schema-design/swagger-ui-schemas.png/5PIDCJlZnV-640.webp 640w" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, 100vw" /> <img class="m-auto" alt="Swagger UI rendering of schemas" src="https://michaelheap.com/images/openapi-schema-design/swagger-ui-schemas.png/5PIDCJlZnV-640.jpeg" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, 100vw" srcset="https://michaelheap.com/images/openapi-schema-design/swagger-ui-schemas.png/5PIDCJlZnV-320.jpeg 320w, https://michaelheap.com/images/openapi-schema-design/swagger-ui-schemas.png/5PIDCJlZnV-640.jpeg 640w" width="640" height="880" /> </picture></div> <p>Here's a <a href="https://gist.github.com/mheap/99d32429e2f579cce925d968aa30e6a9">complete OpenAPI specification</a> that uses these schemas.</p> <h2 id="an-ideal-world" tabindex="-1">An ideal world</h2> <p>Although the model works for APIs that have complex requirements, I consider needing separate models for create and update requests a design flaw. In this example API, you could make <code>adopted_at</code> a nullable field and use the same schema for both create and update requests.</p> <p>I also consider needing a minimal representation of an entity a design flaw.There are cases where there is a real requirement that needs these schemas (e.g. if <code>total_steps</code> must <em>always</em> be accurate and can’t be cached) but these cases are rare.</p> <p>If you find yourself using more than one schema, take a moment to reconsider your API design and see how you can simplify it.</p> Workspace layouts with Hammerspoon Grid 2024-01-10T15:10:39Z https://michaelheap.com/hammerspoon-layout/ <p>Since I switched from Arch to MacOS there’s been an i3 shaped hole in my life. I’ve used apps like Divvy to approximate it, but it’s not the same.</p> <p>I tried out some of the MacOS tiling window managers, but couldn’t get to grips with them. Between having to trigger things using external apps (Yabai and <code>skhd</code>) and having to disable system integrity protection, things just didn’t click. One of the things I loved about i3 was its simplicity.</p> <p>After many years of searching, I think I’ve found a solution in Hammerspoon.</p> <h2 id="hammerspoon%3F" tabindex="-1">Hammerspoon?</h2> <p>Hammerspoon is an automation framework for MacOS that allows you to script various things using Lua.</p> <p>I’ve written about <a href="https://michaelheap.com/topic/hammerspoon/">Hammerspoon</a> before, and have been using it for window management since I abandoned <a href="https://apps.apple.com/fi/app/divvy-window-manager/id413857545">Divvy</a> a couple of years ago (when I configured a dedicated <code>hyper</code> key on my keyboard).</p> <p>I was recently browsing through <a href="https://github.com/jakubdyszkiewicz/dotfishy/blob/master/hammerspoon/init.lua">Jakub’s Hammerspoon config</a> and spotted his usage of <code>hs.grid</code> to configure predefined layouts.</p> <p>Predefined layouts were one of my big use cases for i3, and so I went digging.</p> <h2 id="hs.grid" tabindex="-1">hs.grid</h2> <p><code>hs.grid</code> splits your screen into an <code>x</code> by <code>y</code> grid. You can place windows by specifying a width/height along with an offset for the <code>x</code> and <code>y</code> values.</p> <p>I have split my screen in to an 8x2 grid, with zero margin. I also have a shortcut to show the grid overlay on screen in case I want to position a single window in a non-standard layout.</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">lua</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">hs.</span><span style="color: #D8DEE9">grid</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">setMargins</span><span style="color: #D8DEE9FF">(hs.</span><span style="color: #D8DEE9">geometry</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">size</span><span style="color: #D8DEE9FF">(</span><span style="color: #B48EAD">0</span><span style="color: #D8DEE9FF">,</span><span style="color: #B48EAD">0</span><span style="color: #D8DEE9FF">))</span></div><div class="line"><span style="color: #D8DEE9FF">hs.</span><span style="color: #D8DEE9">grid</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">setGrid</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">8x2</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">)</span></div><div class="line"></div><div class="line"><span style="color: #D8DEE9FF">hs.</span><span style="color: #D8DEE9">hotkey</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">bind</span><span style="color: #D8DEE9FF">(hyper,</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">g</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">,</span><span style="color: #81A1C1">function</span><span style="color: #ECEFF4">()</span></div><div class="line"><span style="color: #D8DEE9FF"> hs.</span><span style="color: #D8DEE9">grid</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">show</span><span style="color: #D8DEE9FF">()</span></div><div class="line"><span style="color: #81A1C1">end</span><span style="color: #D8DEE9FF">)</span></div></code></div></pre> <p>To align a window to the grid, you call <code>hs.grid.set</code> and pass an instance of <code>hs.window</code> as the first parameter. The window is being placed on the right hand side of the screen (4 cells offset on the x-axis, 0 on the y-axis, and it covers a 4x2 space on an 8x2 grid).</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">lua</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">hs.</span><span style="color: #D8DEE9">grid</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">set</span><span style="color: #D8DEE9FF">(window, </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">4,0 4x2</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">)</span></div></code></div></pre> <h2 id="automatic-layouts" tabindex="-1">Automatic layouts</h2> <p>One of the things that I loved about Jakub’s Hammerspoon config is that he had <a href="https://github.com/jakubdyszkiewicz/dotfishy/blob/master/hammerspoon/init.lua#L99-L110">a helper function</a> to discover all of the windows for a certain app before reflowing windows in to the grid.</p> <p>I borrowed this idea and built <a href="https://github.com/mheap/dotfiles/blob/main/dot_hammerspoon/Layouts.lua#L2-L12">some convenience functions</a> which allow you to define layouts like this:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">lua</div><div class="code-container"><code><div class="line"><span style="color: #88C0D0">defineLayout</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">Writing</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">, </span><span style="color: #B48EAD">1</span><span style="color: #D8DEE9FF">, {</span></div><div class="line"><span style="color: #D8DEE9FF"> {</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">Bear</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">, </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">0,0 2x2</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">},</span></div><div class="line"><span style="color: #D8DEE9FF"> {</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">Arc</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">, </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">2,0 4x2</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">, </span><span style="color: #81A1C1">true</span><span style="color: #D8DEE9FF">}</span></div><div class="line"><span style="color: #D8DEE9FF">});</span></div><div class="line"></div><div class="line"><span style="color: #88C0D0">defineLayout</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">Code</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">, </span><span style="color: #B48EAD">2</span><span style="color: #D8DEE9FF">, {</span></div><div class="line"><span style="color: #D8DEE9FF"> {</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">Arc</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">, </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">0,0 2x2</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">},</span></div><div class="line"><span style="color: #D8DEE9FF"> {</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">Code - Insiders</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">, </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">2,0 4x2</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">, </span><span style="color: #81A1C1">true</span><span style="color: #D8DEE9FF">},</span></div><div class="line"><span style="color: #D8DEE9FF"> {</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">iTerm</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">, </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">6,0 2x2</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">}</span></div><div class="line"><span style="color: #D8DEE9FF">})</span></div></code></div></pre> <p>Now I can press <code>hyper+1</code> to have Bear on the left 1/4 of the screen and Arc in the middle half. If I’m writing code, I can press <code>hyper+2</code> to have VSCode in the middle taking up half of the screen, with Arc on the left and a terminal on the right.</p> <p>Applications will only be positioned if they are already launched. Hammerspoon provides a <code>hs.application.launchOrFocus()</code> method, but I chose to implement <a href="https://github.com/mheap/dotfiles/blob/main/dot_hammerspoon/Layouts.lua#L63-L68">focusIfLaunched</a> as sometimes I don’t have all the apps open (e.g. Bear is optional when I’m writing).</p> <h2 id="give-it-a-go" tabindex="-1">Give it a go</h2> <p>Hammerspoon is wonderful, and I recommend giving it a go if you haven’t already tried it. <a href="https://github.com/mheap/dotfiles/tree/main/dot_hammerspoon">My Hammerspoon config</a> is publicly available, and whilst I’m not a power user, it saves me a ton of time every single day.</p> <p>If you’ve a Hammerspoon user and have tips and tricks you want to share, I’m <a href="https://hachyderm.io/@mheap/followers">@mheap@hachyderm.io</a>. I’d love to hear them!</p> <h2 id="bonus-content" tabindex="-1">Bonus content</h2> <p>I’ve recently stumbled across <a href="https://github.com/nikitabobko/AeroSpace">aerospace</a>, which bills itself as “an i3-like tiling window manager for macOS”. I haven’t given it a go yet, but it looks promising based on <a href="https://www.youtube.com/watch?v=UOl7ErqWbrk">the YouTube video</a>.</p> Could/Should/Will/Why 2024-01-08T20:27:59Z https://michaelheap.com/could-should-will-why/ <p>I had an interesting conversation with my manager this week. I’ve been working on a lot of different projects, and was a little spread thin. In one of our calls my manager asked me why I’m working on .</p> <p>We sat and chatted about it, and I tried to answer the question: <strong>why</strong> am I doing these things? After hearing me out, they agreed that everything I was doing was important. Then they repeated the question, with a slightly different emphasis:</p> <p>Why am <strong>I</strong> doing these things?</p> <p>I didn't have a good answer. The things needed doing. No-one else was doing them. I could do them, so I took them on. But why was <strong>I</strong> the one taking them on?</p> <p>A day has a finite number of hours. If I’m spending my time doing X, then Y isn’t getting done. If Y is something that only I can (or will) do, then isn’t that a better use of my time than X?</p> <p>Here’s a concrete example. I had a choice to either:</p> <ol> <li>Write some tutorials for one of our products using the API</li> <li>Build a proof of concept Terraform provider to understand if we want to invest in the next financial year</li> </ol> <p>My decision? To do both. Work on the proof of concept first, and write some tutorials between my meetings once it's done.</p> <p>That’s the wrong answer.</p> <p>It turns out that writing tutorials isn’t my responsibility. More than that, by writing them I’m taking away learning opportunities from others. I’m taking it away from the product manager that wants to learn to write documentation. I’m taking it away from the writer that wants to learn more about API development.</p> <p>Anything I take on should meet one of the following criteria:</p> <ul> <li>No-one else can do the work</li> <li>It’s time sensitive, and no-one else has capacity</li> <li>There’s a learning opportunity for me</li> </ul> <h2 id="building-a-system" tabindex="-1">Building a system</h2> <p>When my manager asked why <strong>I</strong> was working on these projects, I realised that I couldn’t answer. We talked about who <em>could</em> do the work, who <em>should</em> do the work, and who <em>will</em> do the work. This gave me a good framework for evaluating projects before starting them.</p> <p>I’ve added a final column - <em>why</em> - to help keep a record of why the person that <em>will</em> take on the project is responsible.</p> <p>Here’s an example with a couple of projects:</p> <table> <thead> <tr> <th>Project</th> <th>Could</th> <th>Should</th> <th>Will</th> <th>Why</th> </tr> </thead> <tbody> <tr> <td>Public Demos</td> <td>DevRel, Sales Engineering</td> <td>Sales Engineering</td> <td>Sales Engineering</td> <td>Demos are a core part of the SE role. If DevRel build demos, there would be duplication.</td> </tr> <tr> <td>Release Demos</td> <td>Product, DevRel, Product Marketing</td> <td>Product Marketing</td> <td>DevRel</td> <td>DevRel has an engineering background to understand the features + how to get started</td> </tr> <tr> <td>Splunk Tutorial</td> <td>Docs, DevRel</td> <td>Docs</td> <td>DevRel</td> <td>Docs team is at capacity with regular release documentation</td> </tr> <tr> <td>VSCode Extension</td> <td>DevRel, Engineering</td> <td>Engineering</td> <td>N/A</td> <td>Project not prioritised.</td> </tr> <tr> <td>Terraform Provider</td> <td>DevRel, Engineering</td> <td>DevRel</td> <td>DevRel</td> <td>This is an initial proof of concept exploration. If the idea shows potential, it's added to the Engineering roadmap.</td> </tr> </tbody> </table> <p>Let’s work through the tutorials example in more detail:</p> <ul> <li>A community member asked how to send logs from Kong on a VM to Splunk</li> <li>DevRel couldn’t find any existing documentation to send them</li> <li>DevRel the docs team if they could write a new tutorial</li> <li>The docs team share that they’re already overcommitted.</li> </ul> <p>At this point DevRel have three choices:</p> <ul> <li>Abandon the project</li> <li>Submit it in to the docs team backlog</li> <li>Do it themselves</li> </ul> <p>This is your classic prioritisation problem. Whichever option you choose, fill out the <em>why</em> column with the rationale then move on.</p> <h2 id="has-it-helped%3F" tabindex="-1">Has it helped?</h2> <p>Kind of. I still take on more projects than I should, but having an explicit <em>will</em> step forces me to think about why we’re taking on this specific project. It forces me to talk to other people about the project. Could their teams take on the work? Do they <strong>want</strong> to take on the work? Are there learning opportunities?</p> <p>Next time you have an idea for some work that needs doing, run it through <em>could/should/will/why</em> to make sure that you’re the best suited person or team to complete the project.</p> Create an AWS RDS database 2024-01-05T15:52:09Z https://michaelheap.com/aws-rds-db/ <p>I've been using RDS to provide throwaway databases for testing and wanted to work from the CLI to speed things up. The <code>aws rds</code> incantations were hard to find at times, so here they are for posterity.</p> <p>Create a publicly accessible Postgres database:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">aws rds create-db-instance --db-instance-identifier demo-db --db-instance-class db.t3.micro --allocated-storage 50 --engine postgres --publicly-accessible --master-username postgres --master-user-password YOUR_PASSWORD</span></div></code></div></pre> <p>Show the database status and connection details:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">aws rds describe-db-instances </span><span style="color: #81A1C1">|</span><span style="color: #D8DEE9FF"> jq </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">.DBInstances[] | select(.DBInstanceIdentifier == "demo-db") | .DBInstanceStatus,.Endpoint</span><span style="color: #ECEFF4">'</span></div></code></div></pre> <p>Delete the database:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">aws rds delete-db-instance --db-instance-identifier demo-db --skip-final-snapshot</span></div></code></div></pre> Create an Azure AKS Cluster 2024-01-05T15:50:50Z https://michaelheap.com/create-aks-cluster/ <p>I needed to test Azure AKS with the Application Gateway Ingress Controller. Thankfully, Azure makes it easy with it's <code>az aks</code> CLI.</p> <p>Create a new Azure resource group:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">az group create --name mheap-test --location uksouth</span></div></code></div></pre> <p>Create a new AKS cluster with the Azure Application Gateway Ingress Controller enabled:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">az aks create --name mheap-aks-test --resource-group mheap-test --node-count 1 --network-plugin azure --enable-managed-identity --enable-addons ingress-appgw --appgw-name mheap-appgw --appgw-subnet-cidr </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">10.225.0.0/16</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF"> --generate-ssh-keys</span></div></code></div></pre> <p>Configure <code>kubectl</code> to connect to this cluster:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">az aks get-credentials --resource-group mheap-test --name mheap-aks-test</span></div></code></div></pre> <p>Once you've finished testing, delete the AKS cluster and resource group:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">az aks delete --name mheap-aks-test --resource-group mheap-test --yes</span></div><div class="line"><span style="color: #D8DEE9FF">az group delete --name mheap-test --yes</span></div></code></div></pre>