<?xml version="1.0" encoding="utf-8"?> <feed xmlns="http://www.w3.org/2005/Atom"> <title>michaelheap.com</title> <subtitle>Thoughts on leadership, code and how to fix odd edge cases in tools (not necessarily in that order)</subtitle> <link href="https://michaelheap.com/rss" rel="self"/> <link href="https://michaelheap.com"/> <updated>2025-02-18T17:15:37Z</updated> <id>https://michaelheap.com</id> <author> <name>Michael Heap</name> <email>[email protected]</email> </author> <entry> <title>Using AWS credential_process and 1Password</title> <link href="https://michaelheap.com/aws-credential-helper-1password/"/> <updated>2025-02-18T17:15:37Z</updated> <id>https://michaelheap.com/aws-credential-helper-1password/</id> <content type="html"><p>A while back I read an excellent post from Paul Galow on <a href="https://paulgalow.com/securing-aws-credentials-macos-lastpass">securing AWS credentials with LastPass</a>. I wanted exactly this, but with 1Password instead. Here's how to do it.</p> <h2 id="configure-1password" tabindex="-1">Configure 1Password</h2> <p>If you don’t already have <code>op</code> installed, you’ll need to install the <a href="https://developer.1password.com/docs/cli/">op CLI</a> from the 1Password website.</p> <p>Once that’s done, create a new vault for storing secrets that are accessible from the 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">op vault create CLI</span></div></code></div></pre> <p>Then create a new item in that vault, making sure to replace <code>XXX</code> with your actual access key and secret:</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"> op item create --category Password --vault CLI --title AWSCredentials ACCESS_KEY=XXX SECRET_KEY=XXX</span></div></code></div></pre> <blockquote> <p>The above command is prefixed with a space so that it is not written to your shell history if <code>HIST_IGNORE_SPACE</code> is enabled</p> </blockquote> <h2 id="create-the-credential_process" tabindex="-1">Create the credential_process</h2> <p>The AWS CLI has the ability to read credentials from a process rather than a static configuration file. It expects that the credential process will return a JSON document containing <code>AccessKeyid</code> and <code>SecretAccessKey</code>.</p> <p>Copy and paste the following in to a terminal to create the credentials script:</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: #81A1C1">export</span><span style="color: #D8DEE9FF"> AWS_HELPER=</span><span style="color: #ECEFF4">"</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">HOME</span><span style="color: #A3BE8C">/bin/aws-1password</span><span style="color: #ECEFF4">"</span></div><div class="line"><span style="color: #D8DEE9FF">mkdir -p </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">HOME</span><span style="color: #D8DEE9FF">/bin</span></div><div class="line"></div><div class="line"><span style="color: #88C0D0">echo</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">#!/usr/bin/env bash</span></div><div class="line"><span style="color: #A3BE8C">readonly opVault="CLI"</span></div><div class="line"><span style="color: #A3BE8C">readonly opEntry="AWSCredentials"</span></div><div class="line"><span style="color: #A3BE8C">readonly accessKeyId=$(op read "op://$opVault/$opEntry/ACCESS_KEY")</span></div><div class="line"><span style="color: #A3BE8C">readonly secretAccessKey=$(op read "op://$opVault/$opEntry/SECRET_KEY")</span></div><div class="line"></div><div class="line"><span style="color: #A3BE8C"># Create JSON object that AWS CLI expects</span></div><div class="line"><span style="color: #A3BE8C">jq -n \</span></div><div class="line"><span style="color: #A3BE8C"> --arg accessKeyId "$accessKeyId" \</span></div><div class="line"><span style="color: #A3BE8C"> --arg secretAccessKey "$secretAccessKey" \</span></div><div class="line"><span style="color: #A3BE8C"> ".Version = 1</span></div><div class="line"><span style="color: #A3BE8C"> | .AccessKeyId = \$accessKeyId</span></div><div class="line"><span style="color: #A3BE8C"> | .SecretAccessKey = \$secretAccessKey"</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">AWS_HELPER</span><span style="color: #81A1C1">;</span></div><div class="line"></div><div class="line"><span style="color: #D8DEE9FF">chmod +x </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">AWS_HELPER</span></div><div class="line"></div></code></div></pre> <h2 id="configure-aws" tabindex="-1">Configure AWS</h2> <p>The final thing to do is to create an AWS config file that uses that script to fetch authentication credentials:</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">mkdir -p </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">HOME</span><span style="color: #D8DEE9FF">/.aws</span></div><div class="line"><span style="color: #88C0D0">echo</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">[default]</span></div><div class="line"><span style="color: #A3BE8C">credential_process = </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">AWS_HELPER</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">HOME</span><span style="color: #D8DEE9FF">/.aws/config</span></div></code></div></pre> <p>To check if it worked, run <code>aws configure list</code>. You should be prompted for your 1Password credentials to unlock the vault.</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 configure list</span></div><div class="line"><span style="color: #D8DEE9FF"> Name Value Type Location</span></div><div class="line"><span style="color: #D8DEE9FF"> ---- ----- ---- --------</span></div><div class="line"><span style="color: #D8DEE9FF"> profile </span><span style="color: #81A1C1">&lt;</span><span style="color: #D8DEE9FF">not set</span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF"> None None</span></div><div class="line"><span style="color: #D8DEE9FF">access_key </span><span style="color: #81A1C1">****************</span><span style="color: #D8DEE9FF">XYZ custom-process</span></div><div class="line"><span style="color: #D8DEE9FF">secret_key </span><span style="color: #81A1C1">****************</span><span style="color: #D8DEE9FF">XYZ custom-process</span></div><div class="line"><span style="color: #D8DEE9FF"> region </span><span style="color: #81A1C1">&lt;</span><span style="color: #D8DEE9FF">not set</span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF"> None None</span></div></code></div></pre> </content> </entry> <entry> <title>Slack channels are free</title> <link href="https://michaelheap.com/slack-channels-are-free/"/> <updated>2025-02-12T20:51:45Z</updated> <id>https://michaelheap.com/slack-channels-are-free/</id> <content type="html"><p>“Should we create a new Slack channel for this?”</p> <p>I’ve not seen a topic as divisive as this since the days of tabs vs spaces (tl;dr: <a href="https://adamtuttle.codes/blog/2021/tabs-vs-spaces-its-an-accessibility-issue/">use tabs</a>). Whenever there’s a suggestion to create a new channel, there are fervent discussions about why it’s a good/bad idea, with strong feelings on both sides.</p> <p>I’m firmly in camp “as many channels as needed, sometimes more channels than employees”. Once you hit a certain size, having a few firehose channels for everything means that 80%+ of the messages in a channel are irrelevant to the majority of the audience.</p> <p>Instead, spin up a new channel for each area of focus. This allows you to keep your working groups small (12 rather than 1200 people) and lets people focus on the channels that are important day to day.</p> <p><strong>Extra credit:</strong> If you really want to help people focus, ensure that you use threads to prevent unread notifications. If you see a topic that you want to follow, click the “get notified about new replies” option in the context menu for the top level message.</p> <h2 id="the-power-of-focused-channels" tabindex="-1">The Power of Focused Channels</h2> <p>Using specific channels for each topic has immediate benefits. Scoping conversations down to a topic rather than it being a free for all provides consumers with:</p> <ol> <li>Notification control</li> <li>Easier searching</li> <li>Single purpose channels (e.g. discussion, alerts etc)</li> <li>The correct audience</li> </ol> <p>Even Slack recommends using more, topic specific channels:</p> <blockquote> <p>When you set up multiple topic- and project-specific channels, groups can focus their discussions among smaller numbers of people, helping them to align and move faster. And having lots of specific channels means that each person can participate in fewer channels, because only a handful of them will be necessary for their daily work.<br /> <br /> via <a href="https://slack.com/intl/en-gb/resources/using-slack/how-to-organize-your-slack-channels">https://slack.com/intl/en-gb/resources/using-slack/how-to-organize-your-slack-channels</a></p> </blockquote> <h3 id="fewer-unnecessary-notifications" tabindex="-1">Fewer Unnecessary Notifications</h3> <p>Being able to manage notifications is my favourite reason to use more Slack channels. Not all messages are created equally, but when they land in the same channel I have to manually read and filter the messages in my head.</p> <p>For concrete example, imagine a company that has a single <code>#general</code> channel. It’s a place where company updates are shared, but also a place where people chat about the weekend. As a consumer I have to follow the channel in case I miss anything but the signal to noise ratio is low.</p> <p>Now imagine that this channel is split in to <code>#announcements</code> and <code>#watercooler</code>. I can safely mute or leave the water cooler channel while paying close attention to <code>#announcements</code>.</p> <h3 id="find-what-you-need%E2%80%94fast" tabindex="-1">Find What You Need—Fast</h3> <p>Once you have separate channels, searching becomes much easier. All you need is a keyword or two and the channel that you recall seeing the conversation in.</p> <p>When working on some new documentation, I searched in the <code>#support</code> channel for “flubjam”, the name of the product I was working on (no, thats not the real product name). This surfaced all the issues customers were having with the product that I can now weave in to the updated documentation.</p> <h3 id="dedicated-channels%2C-better-workflows" tabindex="-1">Dedicated Channels, Better Workflows</h3> <p>If we take the notification control benefit and supercharge it, we get to “single use channels”. Slack can do so much more than be a chat room for your team.</p> <p>A <em>large</em> portion of my Slack usage is monitoring events:</p> <ul> <li>A new post on Stack Overflow with a specific tag goes to <code>#stack-overflow</code></li> <li>New GitHub issues and releases for the flubjam project are piped in to <code>#notify-flubjam</code></li> <li>All GitHub activity for the flubjam project is piped in to <code>#firehose-flubjam</code></li> <li>Posts elsewhere on Slack that get a floppy disk icon are cross posted to <code>#zmeta-saved</code></li> </ul> <p>Creating single use channels gives people the information that they need, when they need it.</p> <h3 id="reach-the-right-people%2C-every-time" tabindex="-1">Reach the Right People, Every Time</h3> <p>Finally, using single purpose channels means that your audience is probably the one you’re looking for. Instead of blasting your question to 1200 people in a general channel, you can ask a targeted group your specific question.</p> <p>I find this much easier than trying to figure out who’s involved in a project to set up a five way DM (which will inevitably miss out a key person who then feels slighted that I forgot them).</p> <h2 id="finding-the-right-channels" tabindex="-1">Finding the Right Channels</h2> <p>So if focused channels are so great, why don’t we all do it? There’s an easy answer:</p> <p><strong>Finding new channels is hard</strong>.</p> <p>There are a couple of ways to solve this problem. The most effective ones I’ve seen are:</p> <ul> <li>Have a channel that announces new channels (very meta!)</li> <li>Set a reminder to check for new channels weekly using the Channels-&gt;Browse Channels option. Usually there haven’t been that many created in the last 7 days.</li> <li>You can rely on people to invite you. Usually they won’t invite you directly. Instead, they’ll mention you by your Slack username, realise that you’re not there and then click “invite”. This means you only join channels when there is something for you to actively participate in</li> <li>Finally, update your onboarding documentation with links to relevant channels (you do have onboarding documentation, right? 😁)</li> </ul> <h2 id="where-should-you-post%3F" tabindex="-1">Where Should You Post?</h2> <p>Ok, so you have lots of tightly scoped channels. You’ve joined the channels relevant to your day to day work, and now you have a question about the “flubjam” project.</p> <p>Do you post in <code>#team-product</code>, <code>#flubjam-is-cool</code> or <code>#general</code>?</p> <p>Channel naming schemes can help with this. One of the patterns I’ve seen at a few different places now is to have an “<code>#ask-team-name</code>” channel that is public. Have a question for finance? <code>#ask-finance</code>! Curious about the product roadmap? <code>#ask-product</code>!</p> <p>If you’re small enough that you have 1-3 products, keeping a single <code>#ask-product</code> channel makes sense.</p> <p>As you expand your product offering, you may want to expand to “<code>#ask-product-name</code>” style channels, such as “<code>#ask-dev-portal</code>”. This allows the <a href="https://medium.com/design-bootcamp/the-power-of-the-product-triad-0e76801a384d">product triad</a> to answer the questions as a team rather than the questions going to a specific department all the time.</p> <p>Finally, each product is made up of various initiatives and deliverables. You likely have a “<code>#private-team-a</code>” channel that you use to discuss ongoing projects, but I encourage you to bring that conversation in to “<code>#project-name</code>” channels. Not only does it give you a single place to read for any given project, it invites collaboration with other teams who may have suggestions based on their experience. These channels are short lived and should be archived as soon as the project is completed.</p> <p>To recap:</p> <ul> <li><code>#ask-team-name</code> to get started</li> <li><code>#ask-product-name</code> as the team grows</li> <li><code>#project-name</code> to stop conversation sprawl and build a culture of collaboration</li> </ul> <p>With concrete examples:</p> <ul> <li><code>#ask-product</code> for general product questions</li> <li><code>#ask-flubjam</code> for all flubjam related questions</li> <li><code>#project-flubjam-capacitors</code> is a short term channel to talk about implementing capacitors in flubjam</li> </ul> <h2 id="create-that-slack-channel" tabindex="-1">Create That Slack Channel</h2> <p>Focused channels improve notification control, make searching easier, ensure the right audience sees your messages, and unlock powerful workflows beyond just chat. While discoverability can be a challenge, a few simple strategies—like clear naming conventions—can make it easy for people to find the right spaces.</p> <p>At the end of the day, good Slack hygiene isn’t about having fewer channels—it’s about having the <strong>right</strong> channels.</p> <h2 id="update%3A-super-bonus-content" tabindex="-1">Update: Super Bonus Content</h2> <p>I shared a preview of this post with some colleagues and <a href="https://www.linkedin.com/in/jasonhnaustin">Jason</a> had some great feedback on how to cope with channel sprawl:</p> <ul> <li><strong>Housecleaning built-in:</strong> If you have a topic that is not meant to be long-lived, a convention that flags the channel (usually <code>#temp-</code>) signifies that when the topic is resolved, the channel dies. This helps address the channel sprawl problem, and intentionally redirects folks back to long-lived comms channels, vs tactical problem solving.</li> <li><strong>Join many, star few:</strong> the likelihood that you keep up with the conversation in every channel you join, in a &quot;Slack channels are free&quot; environment, is pretty much zero. Missing critical notifications on people you are working with regularly is a big risk to productivity, and job security in some cases. Star key channels, and mark your closest collaborators and reporting chain as &quot;VIP&quot;, so they are on a short-list to focus your attention.</li> <li><strong>Ruthless prioritization on &quot;Leave Channel&quot;:</strong> if you're not contributing or engaging in a channel, leave it immediately. If you're mentioned, you'll be notified, and you can rejoin.</li> </ul> <p>As someone that left <em>175</em> Slack channels in his January cleanup, that last point on ruthless priorization is key. I'm already back in some of them, but I know it's because I'm actively engaged rather than through inertia.</p> </content> </entry> <entry> <title>Excellent Pull Request Reviews</title> <link href="https://michaelheap.com/pull-request-reviews/"/> <updated>2025-02-05T03:55:49Z</updated> <id>https://michaelheap.com/pull-request-reviews/</id> <content type="html"><p>Pull request reviews are a critical part of building high-quality products, but too often, they become a rubber-stamping exercise—skim, “LGTM,” approve. This kind of review can lead to broken code, unclear documentation, and missed opportunities for improvement.</p> <p>A great PR review isn’t just about glancing at the code; it’s about <strong>running the changes</strong>, <strong>thinking critically about what’s missing</strong>, and <strong>providing actionable feedback</strong>.</p> <h2 id="run-the-thing" tabindex="-1">Run the thing</h2> <p>If I could only have one rule for reviewing pull requests, it would be “did you run it?”. Far too often I see people skimming over the content, giving it a “LGTM” and pressing approve.</p> <p>This happens <em>everywhere</em>. My most recent example is a project that is migrating documentation from one platform to another. All of the content already exists, so it’s just about reformatting and publishing it elsewhere, right?</p> <p>No. No, no, <em>no</em>. You see, a lot of the documentation doesn’t work. Maybe it didn’t work when it was originally published (“LGTM!”). Maybe it did, and it’s rotted over time and no longer works.</p> <p>So now we have a new definition of done for the migration work. Before you submit a pull request for review, you need to work through the content from top to bottom and ensure it works. Then your reviewer has to do the same.</p> <p>Having two people test the same thing ensures that the documentation is clear, isn’t missing implicit knowledge and most of all, that it works.</p> <p>If you’re building software rather than writing docs, good news, the same process works! Having two people <em>use</em> the software makes sure that it meets the requirements, uncovers edge cases and generally helps build better products.</p> <h2 id="what%E2%80%99s-missing%3F" tabindex="-1">What’s missing?</h2> <p>Looking at a pull request and saying “LGTM” is only half of the review. You also need to think about what you’re <strong>not</strong> looking at. What should be there, but isn’t?</p> <p>Are there any missing tests? Missing docs? How about example configuration or usage examples for the feature that you just added?</p> <p>Spotting what isn’t there is one of the hardest things to learn when reviewing PRs, but it’s also one of the most impactful. Being able to say “this looks great, but I think we should add a test for when X” helps build better products.</p> <h2 id="is-this-blocking%3F" tabindex="-1">Is this blocking?</h2> <p>As you add more comments to a pull request (and you <em>will</em> add more comments if you’re actually running what’s there and thinking about what’s missing), it can be hard to understand which pieces of feedback are opinions, and which require changes.</p> <p>To make it obvious for the original author, I recommend prefixing feedback if it doesn’t need to be actioned immediately.</p> <ul> <li><em>No prefix</em>: This is important feedback for the current PR</li> <li><em>future</em>: Could we refactor this into something that’s reusable?</li> <li><em>nit</em>: I find it easier to parse when using early returns rather than else blocks</li> </ul> <p>By following this pattern of feedback you can ensure that you provide all the context you have in the PR without worrying about preventing the original author from shipping.</p> <p>I’ve been on the receiving end of this type of feedback, and it’s been great to learn what <em>could</em> be done given more time. I don’t always go back and fix the <em>nit</em> comments, but it always makes me think about what I can do differently next time.</p> <h2 id="do-the-work" tabindex="-1">Do the work</h2> <p>Last, but by no means least, do the work yourself. I don’t mean “pull down the branch and make changes” (though in at least one of my teams that <em>is</em> the default operating model and it works well).</p> <p>I mean use GitHub’s suggestions macro when reviewing a pull request. If you have a suggestion how to improve something, instead of trying to explain it in text you can edit the lines yourself using the following format:</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: #ECEFF4">```</span><span style="color: #A3BE8C">suggestion</span></div><div class="line"><span style="color: #A3BE8C">This line will be suggested instead of the one that you selected</span></div><div class="line"><span style="color: #ECEFF4">```</span></div></code></div></pre> <p>Not only is making the actual change easier to parse for the original author, they can quickly apply any changes they agree with by clicking “add to batch” in the web UI.</p> <h2 id="go-forth-and-review" tabindex="-1">Go forth and review</h2> <p>By making sure the code works, thinking critically about what’s missing, and clearly distinguishing between blocking and non-blocking comments, reviewers can significantly improve the quality of the product. And when possible, making direct suggestions instead of just pointing out issues can streamline the process for everyone.</p> <p>A great review isn’t just about catching issues. It's about fostering collaboration, improving maintainability, and ultimately, building better products. So next time you review a PR, take the time to do it right. Your team (and your future self) will thank you.</p> </content> </entry> <entry> <title>Run go test -tags in VSCode</title> <link href="https://michaelheap.com/go-test-tags/"/> <updated>2025-01-10T13:01:15Z</updated> <id>https://michaelheap.com/go-test-tags/</id> <content type="html"><p>I've been writing more Go recently, and the <a href="https://code.visualstudio.com/docs/languages/go#_test">Go extension</a> for VSCode provides an awesome <em>Run Test</em> button inline.</p> <p>In many of our projects, we have a combination of unit and integration tests. To keep <code>go test</code> quick, the integration tests have an <code>integration</code> tag enabled. This means that the <em>Run Test</em> button doesn't work for integration tests.</p> <p>To make them work, add the following to <code>.vscode/settings.json</code> in your project:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">json</div><div class="code-container"><code><div class="line"><span style="color: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #8FBCBB">go.testFlags</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">-v</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">],</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #8FBCBB">go.testTags</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">integration</span><span style="color: #ECEFF4">"</span></div><div class="line"><span style="color: #ECEFF4">}</span></div></code></div></pre> </content> </entry> <entry> <title>Flatten nested foreach loops with Terraform</title> <link href="https://michaelheap.com/terraform-flatten-nested-loops/"/> <updated>2024-09-12T09:26:22Z</updated> <id>https://michaelheap.com/terraform-flatten-nested-loops/</id> <content type="html"><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> </content> </entry> <entry> <title>Bind a Hyper/Meh key with Keychron Launcher</title> <link href="https://michaelheap.com/keychron-launcher-hyper-meh/"/> <updated>2024-09-05T07:55:33Z</updated> <id>https://michaelheap.com/keychron-launcher-hyper-meh/</id> <content type="html"><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> </content> </entry> <entry> <title>Downloading webcomics with Dosage</title> <link href="https://michaelheap.com/download-webcomics/"/> <updated>2024-05-20T16:25:59Z</updated> <id>https://michaelheap.com/download-webcomics/</id> <content type="html"><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> </content> </entry> <entry> <title>Presentations on an ultrawide monitor</title> <link href="https://michaelheap.com/presenting-ultrawide/"/> <updated>2024-05-13T09:17:51Z</updated> <id>https://michaelheap.com/presenting-ultrawide/</id> <content type="html"><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> </content> </entry> <entry> <title>Kong Gateway Quickstart</title> <link href="https://michaelheap.com/kong-quickstart/"/> <updated>2024-05-07T08:34:54Z</updated> <id>https://michaelheap.com/kong-quickstart/</id> <content type="html"><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> </content> </entry> <entry> <title>Designing OpenAPI Schemas</title> <link href="https://michaelheap.com/openapi-schema-design/"/> <updated>2024-04-27T19:36:48Z</updated> <id>https://michaelheap.com/openapi-schema-design/</id> <content type="html"><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> </content> </entry> </feed>