michaelheap.com Thoughts on leadership, code and how to fix odd edge cases in tools (not necessarily in that order) 2024-05-13T09:17:51Z https://michaelheap.com Michael Heap m@michaelheap.com 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> Does this live in the docs or on the knowledge base? 2024-01-05T15:21:17Z https://michaelheap.com/docs-or-knowledge-base/ <blockquote> <p>You may also be interested in my thoughts on content living on <a href="https://michaelheap.com/blog-or-docs/">a blog vs docs</a></p> </blockquote> <p>Making customers successful with a product requires three key components:</p> <ol> <li><strong>UX</strong>: If the application is intuitive, customers are successful without thinking about it.</li> <li><strong>Documentation</strong>: Practically speaking, most apps need supporting documentation to explain new concepts or provide concrete implementation instructions.</li> <li><strong>Support</strong>: Heroes that work directly with customers every day to make them successful in a 1:1 setting.</li> </ol> <p>Great UX is the ideal solution, but in most companies, customers need supporting enablement content.</p> <p>As the company grows, both the documentation and support teams build out their own libraries of content to make customers successful. Eventually, they’ll hit a critical mass and someone asks the inevitable question:</p> <blockquote> <p>Does this live in the docs or on the knowledge base?</p> </blockquote> <p>Both teams want their content to live in their own system. Support wants to use the knowledge base as it has deep integration with their ticketing system, with workflows for pulling answers from articles which aren’t always able to integrate with external docs. Docs want to use their docs-as-code platform and all the tooling that they’ve built around the written word.</p> <h2 id="where-does-the-content-live%3F" tabindex="-1">Where does the content live?</h2> <p>Whatever you do, don't store the content in both places. The second copy goes out of sync before you even hit publish.</p> <p>Instead, categorise content in two buckets:</p> <ul> <li>This is generally applicable to the majority of customers using this product</li> <li>This is specific to a small number of customer setups</li> </ul> <p>Imagine that you have a customer issue come in and the support team resolves it. Now what? Where does the solution go?</p> <p>If the answer applies to the majority of customers, then I recommend having a small KB article so that it shows up in their day to day workflow, but the KB article has a small description that then links to docs.</p> <p>If the answer is specific to a small number of customers, the answer lives in the KB. The support team shares these answers on demand in response to tickets. They're also available if people search the KB specifically (or on the docs too if you have multi-site indexing).</p> <p>Splitting content by audience allows you to keep your documentation focused on the 80% of customers that can be successful without human interaction. Providing an exhaustive list of failure modes makes it difficult for customers to find what they're looking for when everything is working as expected.</p> <h2 id="examples" tabindex="-1">Examples</h2> <p>The above is quite abstract, so here are some examples from the docs teams that I’ve been a part of.</p> <p>Vonage provides communication APIs, including SMS. Sending an SMS using the API is something that was applicable to everyone, so it had public docs. Restrictions around the sender ID (<code>from</code>) used are different on a per-country basis (<a href="https://api.support.vonage.com/hc/en-us/articles/204017823-Peru-SMS-Features-and-Restrictions">Peru</a> for example). Most of Vonage’s customers were in the USA where there were no restrictions, so this information lived in the knowledge base. When the USA introduced 10DLC it affected the majority of customers, so the restrictions were added to the public docs <em>and</em> the knowledge base.</p> <p>Kong provides connectivity tools, such as API Gateways and Service Meshes. Older versions of Ubuntu don’t have a required library (<code>zlib</code>) installed by default and every customer using an old version of Ubuntu encounters this issue, so we document it on the installation page. Kong Mesh allows you to proxy requests to Redis, but sometimes you get a cryptic <code>&quot;Error: Protocol error, got &quot;H&quot;</code> error. This means that you configured your <code>MeshGatewayRoute</code> as with a HTTP protocol rather than TCP. A small percentage of customers hit this issue, so it lives in the knowledge base.</p> <h2 id="tl%3Bdr" tabindex="-1">TL;DR</h2> <ol> <li>If it’s applicable to &gt; 50% of customers, put it in the public docs</li> <li>If it applies to a small percentage of customers, put it in the knowledge base</li> <li>Make the support team’s life easier by putting pointers to public docs in the knowledge base</li> </ol> Create an Amazon EKS Cluster 2023-12-20T16:44:58Z https://michaelheap.com/create-eks-cluster/ <p>I've been using <a href="https://github.com/hashicorp/learn-terraform-provision-eks-cluster">this</a> Terraform module to deploy test EKS clusters for the longest time, but I just learned that it's even easier when using <a href="https://eksctl.io/"><code>eksctl</code></a>.</p> <p>Create a <code>cluster.yaml</code> file with the following contents. You can change the <code>instanceType</code> and <code>desiredCapacity</code> (the number of nodes) if needed:</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">apiVersion</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">eksctl.io/v1alpha5</span></div><div class="line"><span style="color: #8FBCBB">kind</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">ClusterConfig</span></div><div class="line"></div><div class="line"><span style="color: #8FBCBB">metadata</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><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">mheap-testing</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">region</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">eu-west-2</span></div><div class="line"></div><div class="line"><span style="color: #8FBCBB">nodeGroups</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">name</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">ng-1</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">instanceType</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">m5.large</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">desiredCapacity</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">1</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">volumeSize</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">80</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">ssh</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">allow</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">false</span></div></code></div></pre> <p>Run <code>eksctl create cluster</code>. It takes around 4 minutes:</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">eksctl create cluster -f cluster.yaml</span></div></code></div></pre> <p>Update your <code>kubeconfig</code> using the <code>aws</code> CLI:</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 eks --region eu-west-2 update-kubeconfig --name mheap-testing</span></div></code></div></pre> <p>Now you can run all the <code>kubectl</code> commands that you want.</p> <p>To delete the cluster when you're done:</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">eksctl delete cluster -f cluster.yaml</span></div></code></div></pre> How I work: Email 2023-09-09T11:06:19Z https://michaelheap.com/email-workflow/ <p>Depending on who you ask, I’m excellent with email, or terrible with email. I don’t let it run my life, and process my (work) inbox every 2-3 days.</p> <p>My review process has four steps: <em>triage</em>, <em>unsubscribe</em>, <em>respond</em> and <em>prioritise</em>.</p> <ol> <li>Triage: Read all unread emails in my inbox with the search <code>is:unread in:inbox</code>.</li> <li>If there are emails that I don’t want to receive (e.g. cold outreach) I create a filter based on the sender so that it doesn’t land in my inbox</li> <li>Things that take 2 minutes to answer are responded to immediately. Otherwise they’re labelled (not starred - more on this later) for action later</li> <li>Finally, I go through my labelled items and import them in to Sunsama to be scheduled alongside my other work.</li> </ol> <h2 id="the-klinger-email-method" tabindex="-1">The Klinger email method</h2> <p>I’ve been using a <a href="https://klinger.io/posts/how-to-use-gmail-more-efficiently">multiple inbox setup</a> from Andreas Klinger for years. Having a main inbox, then separate inboxes for high/medium priority emails allowed me to keep on top of things that I needed to do.</p> <p><strong>Go and read the <a href="https://klinger.io/posts/how-to-use-gmail-more-efficiently">Klinger email method</a> now</strong></p> <p>However, since I <a href="https://michaelheap.com/sunsama/">started using Sunsama</a> my inbox is no longer the source of truth for what I need to do. Instead, it’s an input in to my Sunsama plan.</p> <h2 id="pain-points" tabindex="-1">Pain Points</h2> <p>The Klinger method is great, but it isn’t perfect. I have two big complaints, and they both have to do with the use of custom stars.</p> <ol> <li>The mobile gmail app only supports star/unstar, not custom stars</li> <li>Sunsama only support starred/unstarred, which meant I lost my high/medium priority differentiation</li> </ol> <p>To work around this issue, I created two labels: <code>priority/high</code> and <code>priority/medium</code>. I use these for my custom inboxes instead of the red and yellow bang stars from the Klinger method. This allows me to set priorities on mobile, and filter to specific priorities within Sunsama.</p> <p>The real game changer for me was when I learned that you can customise label colours. Click on the three dots next to the label name in the sidebar and choose a label colour. I chose red for high priority and yellow for medium priority. The colours help these emails stand out when I’m scrolling through my inbox.</p> <h2 id="sunsama-integration" tabindex="-1">Sunsama Integration</h2> <p>Finally, it’s time to look at my Sunsama workflow for emails. I review emails tagged with <code>priority/high</code> or <code>priority/medium</code> on a Monday, Wednesday and Friday.</p> <p>I start with <code>priority/high</code> and schedule work on those emails until the list is empty. Once there are no more remaining, I start working through <code>priority/medium</code> emails. When the email is imported in to Sunsama, the <code>priority</code> label is removed automatically which makes Sunsama the source of truth.</p> <p>Email is one of my lowest priority task sources. I’ll always pull from Slack and GitHub before working through my email task backlog. However, everything that comes in via email <em>does</em> eventually need doing so pulling them in to Sunsama is important.</p>