<?xml version="1.0" encoding="utf-8"?>

<feed xmlns="http://www.w3.org/2005/Atom">
	<title>David Sherret</title>
	<subtitle>David Sherret&#39;s personal site.</subtitle>
	<link href="https://dsherret.dev/feed.xml" rel="self"/>
	<link href="https://dsherret.dev/"/>
	
	<updated>2026-05-22T01:34:35Z</updated>
	<id>https://dsherret.dev/</id>
	<author>
		<name>David Sherret</name>
	</author>
	<entry>
		<title>gagen - Writing complex GitHub Action workflow files</title>
		<link href="https://dsherret.dev/posts/gagen/"/>
		<updated>2026-04-02T20:30:00Z</updated>
		<id>https://dsherret.dev/posts/gagen/</id>
		<content type="html">&lt;p&gt;GitHub action files can be a nightmare to maintain.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Conditions often need to be repeated across many steps.&lt;/li&gt;
&lt;li&gt;Referencing values/ids by a string is fragile (ex. matrix values).&lt;/li&gt;
&lt;li&gt;Maintaining pinned dependencies is difficult.&lt;/li&gt;
&lt;li&gt;YAML is hard to work with.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;What&#39;s an easier way to maintain these?&lt;/p&gt;
&lt;h2 id=&quot;initial-solution&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#initial-solution&quot;&gt;Initial Solution&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In the &lt;a href=&quot;https://github.com/denoland/deno&quot;&gt;Deno repo&lt;/a&gt;, our YAML file was
complicated and the CI was slow. In 2023, we decided to
&lt;a href=&quot;https://github.com/denoland/deno/pull/17335&quot;&gt;generate the YAML with TypeScript&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Essentially it looked similar to the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const ci = {
  name: &amp;quot;ci&amp;quot;,
  jobs: {
    build: {
      name: &amp;quot;...&amp;quot;,
      steps: [{
        // ...etc...
      }],
    },
  },
};

const finalText = yaml.stringify(ci);
Deno.writeTextFileSync(
  new URL(&amp;quot;./ci.generated.yml&amp;quot;, import.meta.url),
  finalText,
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This was a good first step because now applying a condition to multiple steps
only required piping the step objects through functions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function skipIfDraftPr(steps: Record&amp;lt;string, unknown&amp;gt;[]): unknown[] {
  const condition = &amp;quot;github.event.pull_request.draft == true&amp;quot;;
  return [
    ...steps.map((step) =&amp;gt; {
      step.if = &amp;quot;if&amp;quot; in step ? `${condition} &amp;amp;&amp;amp; (${step.if})` : condition;
      return step;
    }),
  ];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;slow-ci&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#slow-ci&quot;&gt;Slow CI&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Although the above was a good first step, a few years had passed and our CI was
again too slow. This was mostly due to us having way more tests now. So, we
decided to split up our single job with a matrix into build, and many test jobs
to parallelize that work. We&#39;d tried to do this in the past, but the upload and
download artifact steps were slow enough that it made it not worth it. It&#39;s 2026
now and it&#39;s fast.&lt;/p&gt;
&lt;p&gt;An issue though is that doing this would be too complicated to maintain. The
solution I came up with was &lt;a href=&quot;https://github.com/dsherret/gagen&quot;&gt;gagen&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;gagen&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#gagen&quot;&gt;&lt;code&gt;gagen&lt;/code&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;gagen&lt;/code&gt; allows you to define steps and then describe the relationships between
steps along with the conditions that a step should occur.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { conditions, step, workflow } from &amp;quot;gagen&amp;quot;;

const checkout = step({
  uses: &amp;quot;actions/checkout@v6&amp;quot;,
});

const test = step.dependsOn(checkout)({
  name: &amp;quot;Test&amp;quot;,
  run: &amp;quot;cargo test&amp;quot;,
});

const installDeno = step({
  uses: &amp;quot;denoland/setup-deno@v2&amp;quot;,
});

const lint = step
  .dependsOn(checkout)
  // this condition gets propagated to installDeno, but not checkout
  .if(conditions.isBranch(&amp;quot;main&amp;quot;).not())(
    {
      name: &amp;quot;Clippy&amp;quot;,
      run: &amp;quot;cargo clippy&amp;quot;,
    },
    step.dependsOn(installDeno)({
      name: &amp;quot;Deno Lint&amp;quot;,
      run: &amp;quot;deno lint&amp;quot;,
    }),
  );

// only specify the leaf steps — the other steps
// are pulled in automatically
workflow({
  name: &amp;quot;ci&amp;quot;,
  on: [&amp;quot;push&amp;quot;, &amp;quot;pull_request&amp;quot;],
  jobs: [{
    id: &amp;quot;build&amp;quot;,
    runsOn: &amp;quot;ubuntu-latest&amp;quot;,
    steps: [lint, test],
  }],
}).writeOrLint({
  filePath: new URL(&amp;quot;./ci.generated.yml&amp;quot;, import.meta.url),
  header: &amp;quot;# GENERATED BY ./ci.ts -- DO NOT DIRECTLY EDIT&amp;quot;,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This outputs the following workflow file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# GENERATED BY ./ci.ts -- DO NOT DIRECTLY EDIT

name: ci
on:
  - push
  - pull_request
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
      - name: Test
        run: cargo test
      - name: Clippy
        if: github.ref != &#39;refs/heads/main&#39;
        run: cargo clippy
      - uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282
        if: github.ref != &#39;refs/heads/main&#39;
      - name: Deno Lint
        if: github.ref != &#39;refs/heads/main&#39;
        run: deno lint

# gagen:pin actions/checkout@v6 = de0fac2e4500dabe0009e67214ff5f5447ce83dd
# gagen:pin denoland/setup-deno@v2 = 667a34cdef165d8d2b2e98dde39547c9daac7282
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Dependencies like &lt;code&gt;actions/checkout@v6&lt;/code&gt; get locked to the hash.
&lt;ul&gt;
&lt;li&gt;On subsequent runs, gagen uses the output file as the lockfile.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The condition to not run on main is specified only once. It&#39;s then
automatically propagated backward to the necessary steps.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;denoland/setup-deno&lt;/code&gt; step runs at the latest time that it can. This
means if the &lt;code&gt;cargo clippy&lt;/code&gt; step fails, no time is wasted running
&lt;code&gt;denoland/setup-deno&lt;/code&gt; unnecessarily (so faster feedback).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Under the hood, how gagen works is it creates a graph between steps and then
when creating each workflow it evaluates the graph and conditions. This means
you can reuse step objects between workflows and jobs too.&lt;/p&gt;
&lt;h3 id=&quot;typed-values&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#typed-values&quot;&gt;Typed values&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;We&#39;ve resolved most of the above, but now we&#39;re still left with the problem that
referencing values/ids by a string is fragile.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff&quot;&gt;- 1. Conditions often need to be repeated across many steps.
  2. Referencing values/ids by a string is fragile (ex. matrix values).
- 3. Maintaining pinned dependencies is difficult.
- 4. YAML is hard to work with.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;gagen&lt;/code&gt; provides some helpers for doing that. For example, matrices are typed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { defineMatrix, workflow } from &amp;quot;gagen&amp;quot;;

const matrix = defineMatrix({
  include: [
    { runner: &amp;quot;ubuntu-latest&amp;quot; },
    { runner: &amp;quot;macos-latest&amp;quot; },
  ],
});

matrix.runner; // ExpressionValue(&amp;quot;matrix.runner&amp;quot;) — autocompletes
matrix.foo; // TypeScript error — not a matrix key

workflow({
  // ...
  jobs: [
    {
      id: &amp;quot;build&amp;quot;,
      runsOn: matrix.runner,
      strategy: { matrix },
      steps: [test],
    },
  ],
}).writeOrLint({
  filePath: new URL(&amp;quot;./ci.generated.yml&amp;quot;, import.meta.url),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allows for getting auto-complete on the matrix values when writing
something like &lt;code&gt;matrix.os.equals(&amp;quot;linux&amp;quot;)&lt;/code&gt;, which can then be used in a step.&lt;/p&gt;
&lt;p&gt;Also, there&#39;s a helper for artifacts:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { artifact, step, workflow } from &amp;quot;jsr:@david/gagen@&amp;lt;version&amp;gt;&amp;quot;;

const buildArtifact = artifact(&amp;quot;build-output&amp;quot;);

workflow({
  name: &amp;quot;CI&amp;quot;,
  on: [&amp;quot;push&amp;quot;, &amp;quot;pull_request&amp;quot;],
  jobs: [
    {
      id: &amp;quot;build&amp;quot;,
      runsOn: &amp;quot;ubuntu-latest&amp;quot;,
      steps: [
        step({ name: &amp;quot;Build&amp;quot;, run: &amp;quot;make build&amp;quot; }),
        buildArtifact.upload({ path: &amp;quot;dist/&amp;quot; }),
      ],
    },
    // `needs: [build]` is inferred automatically from the artifact link
    {
      id: &amp;quot;deploy&amp;quot;,
      runsOn: &amp;quot;ubuntu-latest&amp;quot;,
      steps: [
        buildArtifact.download({ dirPath: &amp;quot;output/&amp;quot; }),
        step({
          name: &amp;quot;Deploy&amp;quot;,
          run: &amp;quot;make deploy&amp;quot;,
        }),
      ],
    },
  ],
}).writeOrLint({
  filePath: new URL(&amp;quot;./ci.generated.yml&amp;quot;, import.meta.url),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;how-to-keep-ci.generated.yml-up-to-date%3F&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#how-to-keep-ci.generated.yml-up-to-date%3F&quot;&gt;How to keep &lt;code&gt;ci.generated.yml&lt;/code&gt; up-to-date?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;An obvious problem with this solution is that we need to ensure the YAML file is
up to date with the code generation file.&lt;/p&gt;
&lt;p&gt;To achieve this, the &lt;code&gt;writeOrLint&lt;/code&gt; function will ensure the output is up to date
when the script being executed is passed a &lt;code&gt;--lint&lt;/code&gt; CLI flag, so we can add that
as a CI step:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// note: this requires ci.ts to have a shebang in it that
// runs the typescript code using your preferred runtime
const lintStep = step({
  name: &amp;quot;Lint CI generation&amp;quot;,
  run: &amp;quot;./.github/workflows/ci.ts --lint&amp;quot;,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;impact%3F&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#impact%3F&quot;&gt;Impact?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;By taking advantage of all this, in February I was able to increase the
complexity of the generated output and simplify the maintained code generation
script.&lt;/p&gt;
&lt;p&gt;Now it has:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A &lt;code&gt;build&lt;/code&gt; job for each platform uploading the executable artifacts.&lt;/li&gt;
&lt;li&gt;Many test jobs downloading the executable artifacts and running tests in
parallel.&lt;/li&gt;
&lt;/ol&gt;
&lt;a href=&quot;https://dsherret.dev/deno-ci.png&quot;&gt;
  &lt;img src=&quot;https://dsherret.dev/deno-ci.png&quot; alt=&quot;Deno CI&quot; style=&quot;max-width: 675px; height: auto;&quot; /&gt;
&lt;/a&gt;
&lt;p&gt;Note: The blue dips on main are release workflow runs, which do less work. Also,
sorry the chart is not great, but I created this a couple months ago and now the
raw data seems gone.&lt;/p&gt;
&lt;p&gt;The main slowness now is compiling Deno on certain platforms (like Mac x86).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/denoland/deno/blob/d198cda44b7fddb56d892a8ef2349d1630adfa37/.github/workflows/ci.ts&quot;&gt;Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/denoland/deno/blob/d198cda44b7fddb56d892a8ef2349d1630adfa37/.github/workflows/ci.generated.yml&quot;&gt;Output&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sure, this could have been done in regular YAML, but I believe the code is way
more maintainable. Yes, it&#39;s still complicated, but maintainable.&lt;/p&gt;
&lt;p&gt;For more on what &lt;code&gt;gagen&lt;/code&gt; can do, read the docs on GitHub:
https://github.com/dsherret/gagen&lt;/p&gt;</content>
	</entry>
	<entry>
		<title>First-class JSONC manipulation in JavaScript</title>
		<link href="https://dsherret.dev/posts/jsonc-morph/"/>
		<updated>2025-10-12T23:20:00Z</updated>
		<id>https://dsherret.dev/posts/jsonc-morph/</id>
		<content type="html">&lt;p&gt;Previously I wrote about
&lt;a href=&quot;../jsonc-parser-cst&quot;&gt;first-class JSONC manipulation in Rust&lt;/a&gt;. This weekend I
wrapped this project to make it available in JavaScript.&lt;/p&gt;
&lt;h2 id=&quot;current-approach-(not-great)&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#current-approach-(not-great)&quot;&gt;Current approach (not great)&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;From my understanding, the current way most people modify JSONC files in
JavaScript is via the &lt;a href=&quot;https://www.npmjs.com/package/jsonc-parser&quot;&gt;jsonc-parser&lt;/a&gt;
npm package.&lt;/p&gt;
&lt;p&gt;To use this, you create a list of edits, then you apply the edits. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { assertEquals } from &amp;quot;@std/assert&amp;quot;;
import { applyEdits, modify } from &amp;quot;jsonc-parser&amp;quot;;

const jsonText = `{
  &amp;quot;value&amp;quot;: 1
}`;
const edits = modify(
  jsonText,
  [&amp;quot;value&amp;quot;], // JSON path to modify
  2, // new value
  {
    formattingOptions: {
      insertSpaces: true,
      tabSize: 2,
    },
  },
);
const finalText = applyEdits(jsonText, edits);
assertEquals(
  finalText,
  `{
  &amp;quot;value&amp;quot;: 2
}`,
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works, but quickly becomes complex for simple scenarios. Say we have a
configuration file that could possibly look like this...&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;plugins&amp;quot;: [
    &amp;quot;https://plugins.dprint.dev/json-0.17.0.wasm&amp;quot;
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;...and we want to programmatically append a new url to the plugins array. We
need to handle the plugins property not existing, it existing with a non-array
value, it being empty, or it having other elements.&lt;/p&gt;
&lt;p&gt;Ask an AI to help you with this with &lt;code&gt;npm:jsonc-parser&lt;/code&gt; and you&#39;ll see the code
is quite complex.&lt;/p&gt;
&lt;h2 id=&quot;goal&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#goal&quot;&gt;Goal&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Similar to the last blog post, the API I idealized was one where the code looks
similar to this list where each step reads like a high-level description of
intent:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Parse the text.&lt;/li&gt;
&lt;li&gt;Get and ensure the root value is an object.&lt;/li&gt;
&lt;li&gt;Get and ensure that object has a plugins array value property.&lt;/li&gt;
&lt;li&gt;Append the url to the plugins array.&lt;/li&gt;
&lt;li&gt;Get the final text.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;solution&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#solution&quot;&gt;Solution&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I published &lt;a href=&quot;https://github.com/dsherret/jsonc-morph&quot;&gt;jsonc-morph&lt;/a&gt; this weekend
that achieves this API. You can install it via &lt;code&gt;deno add jsr:@david/jsonc-morph&lt;/code&gt;
or &lt;code&gt;npm install jsonc-morph&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Here&#39;s an example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { parse } from &amp;quot;@david/jsonc-morph&amp;quot;;
import { assertEquals } from &amp;quot;@std/assert&amp;quot;;

const jsonText = `{
  &amp;quot;plugins&amp;quot;: [
    &amp;quot;https://plugins.dprint.dev/json-0.17.0.wasm&amp;quot; // json plugin
  ]
}`;
// 1. Parse the text.
const root = parse(jsonText);
// 2. Get and ensure the root value is an object.
const rootObj = root.asObjectOrForce();
// 3. Get and ensure that object has a plugins array value property.
const plugins = rootObj.getIfArrayOrForce(&amp;quot;plugins&amp;quot;);
// 4. Append the url to the plugins array.
plugins.append(&amp;quot;https://plugins.dprint.dev/typescript-0.95.11.wasm&amp;quot;);

// 5. Get the final text.
assertEquals(
  root.toString(),
  `{
  &amp;quot;plugins&amp;quot;: [
    &amp;quot;https://plugins.dprint.dev/json-0.17.0.wasm&amp;quot;, // json plugin
    &amp;quot;https://plugins.dprint.dev/typescript-0.95.11.wasm&amp;quot;
  ]
}`,
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The complexity is abstracted away, and low level concerns are automatically
handled.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Comments in the file are maintained and not shifted around when making
changes.&lt;/li&gt;
&lt;li&gt;Proper indentation and newlines are handled for us.&lt;/li&gt;
&lt;li&gt;If the data currently uses trailing commas, that will be respected.
&lt;ul&gt;
&lt;li&gt;Trailing commas can be forced by calling &lt;code&gt;root.setTrailingCommas(true);&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You might have noticed this API is similar to my project
&lt;a href=&quot;https://github.com/dsherret/ts-morph&quot;&gt;ts-morph&lt;/a&gt;, which is for modifying
TypeScript/JavaScript files.&lt;/p&gt;
&lt;h3 id=&quot;implementation&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#implementation&quot;&gt;Implementation&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;So how does this work under the hood?&lt;/p&gt;
&lt;p&gt;This implementation uses a concrete syntax tree (CST) which is like an abstract
syntax tree (AST), but also stores the whitespace, tokens, and comments in the
tree. This allows for easily manipulating the tree in place taking into account
everything found in the file, then printing it out when done.&lt;/p&gt;
&lt;p&gt;I already did all the hard work &lt;a href=&quot;../jsonc-parser-cst&quot;&gt;in Rust&lt;/a&gt; though, so for
this JS library I used Claude to generate wrapper code with
&lt;a href=&quot;https://github.com/wasm-bindgen/wasm-bindgen&quot;&gt;wasm-bindgen&lt;/a&gt; then built it with
&lt;a href=&quot;https://github.com/denoland/wasmbuild&quot;&gt;wasmbuild&lt;/a&gt; to get a Wasm module that
makes it available to JS.&lt;/p&gt;</content>
	</entry>
	<entry>
		<title>Maintainable string and bytes pre-allocation in Rust</title>
		<link href="https://dsherret.dev/posts/capacity-builder/"/>
		<updated>2024-12-30T00:00:00Z</updated>
		<id>https://dsherret.dev/posts/capacity-builder/</id>
		<content type="html">&lt;p&gt;A common performance optimization in software development is to pre-allocate
strings/bytes before appending to them. In Rust, failing to do this may cause
the implementation of &lt;code&gt;std::string::String&lt;/code&gt; or &lt;code&gt;std::vec::Vec&lt;/code&gt; to frequently
reallocate bytes internally to deal with its growing size, which is slow.
Additionally, the amount of bytes we end up with at the end may be way more than
we actually need.&lt;/p&gt;
&lt;h2 id=&quot;why-we-pre-allocate&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#why-we-pre-allocate&quot;&gt;Why we pre-allocate&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Take the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;fn main() {
  let mut text = String::new();
  let spaces = &amp;quot; &amp;quot;.repeat(100); // a 100 byte string
  println!(&amp;quot;Len: {}, Capacity: {}&amp;quot;, text.len(), text.capacity());
  for _ in 0..9 {
    text.push_str(&amp;amp;spaces);
    println!(&amp;quot;Len: {}, Capacity: {}&amp;quot;, text.len(), text.capacity());
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When we run this, we can see the string being reallocated often as the length
increases and in the end we&#39;re left with an over-allocated string of 1600 bytes
instead of 900:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shellsession&quot;&gt;Len: 0, Capacity: 0
Len: 100, Capacity: 100
Len: 200, Capacity: 200
Len: 300, Capacity: 400
Len: 400, Capacity: 400
Len: 500, Capacity: 800
Len: 600, Capacity: 800
Len: 700, Capacity: 800
Len: 800, Capacity: 800
Len: 900, Capacity: 1600
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, when the capacity is correctly pre-allocated:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;fn main() {
  let mut text = String::with_capacity(900);
  let spaces = &amp;quot; &amp;quot;.repeat(100); // a 100 byte string
  println!(&amp;quot;Len: {}, Capacity: {}&amp;quot;, text.len(), text.capacity());
  for _ in 0..9 {
    text.push_str(&amp;amp;spaces);
    println!(&amp;quot;Len: {}, Capacity: {}&amp;quot;, text.len(), text.capacity());
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We only allocate &lt;code&gt;text&lt;/code&gt; once and end up with a string equal to its capacity:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shellsession&quot;&gt;Len: 0, Capacity: 900
Len: 100, Capacity: 900
Len: 200, Capacity: 900
Len: 300, Capacity: 900
Len: 400, Capacity: 900
Len: 500, Capacity: 900
Len: 600, Capacity: 900
Len: 700, Capacity: 900
Len: 800, Capacity: 900
Len: 900, Capacity: 900
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;problem&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#problem&quot;&gt;Problem&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Seen this kind of code before?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;let capacity = items
  .iter()
  .filter_map(|i| i.maybe_name.as_ref())
  .enumerate()
  .map(|(i, name)| if i &amp;gt; 0 { 2 } else { 0 } + name.len())
  .sum::&amp;lt;usize&amp;gt;();
let mut text = String::new();
text.try_reserve_exact(capacity)?;

for (i, name) in items
  .iter()
  .filter_map(|i| i.maybe_name.as_ref())
  .enumerate()
{
  if i &amp;gt; 0 {
    text.push_str(&amp;quot;, &amp;quot;);
  }
  text.push_str(name);
}
debug_assert_eq!(text.len(), capacity);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above code:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Calculates the byte capacity of the string ahead of time.&lt;/li&gt;
&lt;li&gt;Allocates bytes with that capacity, returning an error when it doesn&#39;t have
enough memory to allocate.&lt;/li&gt;
&lt;li&gt;Builds up the final string without causing additional allocations of &lt;code&gt;text&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Finally, it does a debug assertion to ensure the final text length matches
the capacity we calculated ahead of time.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Although this code is performant, it has a couple of problems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It&#39;s complicated.&lt;/li&gt;
&lt;li&gt;No single source of truth.
&lt;ul&gt;
&lt;li&gt;The capacity calculation code could get out of sync with the code that
builds the string... the debug assertion helps, but not if all scenarios
aren&#39;t tested in debug.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;solution&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#solution&quot;&gt;Solution&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A solution to this problem is to make the code to calculate the capacity the
same as the code to build up the string. I&#39;ve rolled this up into a crate that
makes it easy:
&lt;a href=&quot;https://github.com/dsherret/capacity_builder&quot;&gt;&lt;code&gt;capacity_builder&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;// same functionality as the above code, but simpler
use capacity_builder::StringBuilder;

let text = StringBuilder::&amp;lt;String&amp;gt;::build(|builder| {
  for (i, name) in items
    .iter()
    .filter_map(|i| i.maybe_name.as_ref())
    .enumerate()
  {
    if i &amp;gt; 0 {
      builder.append(&amp;quot;, &amp;quot;);
    }
    builder.append(name);
  }
})?;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This runs the closure twice: once to compute the capacity and a second time to
build the string. The final string will have a length equal to its capacity and
will never reallocate itself while it&#39;s being built.&lt;/p&gt;
&lt;p&gt;Some features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prevents allocations in the closure by only accepting values by reference
(possible thanks to Rust&#39;s amazing borrow checker)&lt;/li&gt;
&lt;li&gt;Numbers can be appended (or anything that implements
&lt;code&gt;capacity_builder::StringAppendable&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Can be made to work with any string type and not just &lt;code&gt;std::string::String&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Building up bytes is possible via &lt;code&gt;capacity_builder::BytesBuilder&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&#39;ve been integrating this into Deno&#39;s codebase and it&#39;s enabled us to start
pre-allocating strings/vectors in complex cases that previously weren&#39;t
maintainable.&lt;/p&gt;
&lt;p&gt;If you have any suggestions or run into any issues, please open an issue on the
project&#39;s GitHub:
&lt;a href=&quot;https://github.com/dsherret/capacity_builder&quot;&gt;https://github.com/dsherret/capacity_builder&lt;/a&gt;&lt;/p&gt;</content>
	</entry>
	<entry>
		<title>First-class JSONC manipulation in Rust</title>
		<link href="https://dsherret.dev/posts/jsonc-parser-cst/"/>
		<updated>2024-10-20T00:15:00Z</updated>
		<id>https://dsherret.dev/posts/jsonc-parser-cst/</id>
		<content type="html">&lt;p&gt;In &lt;a href=&quot;https://github.com/denoland/deno&quot;&gt;Deno&lt;/a&gt; and
&lt;a href=&quot;https://github.com/dprint/dprint&quot;&gt;dprint&lt;/a&gt; (two Rust projects I maintain), there
are certain cases where a JSON with comments (JSONC) configuration file needs to
be programmatically updated.&lt;/p&gt;
&lt;p&gt;For example, running the following in Deno...&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shellsession&quot;&gt;&amp;gt; deno add jsr:@david/dax
Add jsr:@david/dax@0.42.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;...adds the &lt;a href=&quot;https://jsr.io/@david/dax&quot;&gt;@david/dax&lt;/a&gt; JSR package as a dependency
to the configuration file.&lt;/p&gt;
&lt;h2 id=&quot;current-approach-(not-good)&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#current-approach-(not-good)&quot;&gt;Current approach (not good)&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Our current approach involves parsing a JSONC file with
&lt;a href=&quot;https://github.com/dprint/jsonc-parser&quot;&gt;jsonc-parser&lt;/a&gt; to an AST, then using
that to build up a collection of &amp;quot;text changes&amp;quot; and finally applying the text
changes to the original text.&lt;/p&gt;
&lt;p&gt;For example, say I have the following &lt;em&gt;dprint.jsonc&lt;/em&gt; file and we want to add a
new url to the plugins array:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;plugins&amp;quot;: [
    &amp;quot;https://plugins.dprint.dev/json-0.19.1.wasm&amp;quot;
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To do that, we&#39;d examine this code, then construct a collection of text changes
like the following and have some other code manipulate the original string to
apply theses changes.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;[{
  &amp;quot;range&amp;quot;: [66, 66],
  &amp;quot;newText&amp;quot;: &amp;quot;,\n    \&amp;quot;https://plugins.dprint.dev/toml-0.6.3.wasm\&amp;quot;&amp;quot;
}]
&lt;/code&gt;&lt;/pre&gt;
&lt;details&gt;
  &lt;summary&gt;Example non-Rust pseudocode&lt;/summary&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;/// Adds a plugin url to the dprint config file&#39;s plugins array.
/// ```jsonc
/// {
///   &amp;quot;plugins&amp;quot;: [
///     &amp;quot;https://plugins.dprint.dev/toml-0.6.3.wasm&amp;quot;,
///     &amp;quot;&amp;lt;new url goes here&amp;gt;&amp;quot;
///   ]
/// }
/// ```
function addPluginToJson(jsonText, url) {
  const changes = [];
  // parse to an ast
  const ast = parseJson(jsonText);

  // if the root is not an object, just replace it with one
  if (ast.value?.kind !== &amp;quot;object&amp;quot;) {
    return `{
  &amp;quot;plugins&amp;quot;: [
    &amp;quot;${url}&amp;quot;
  ]
}
`;
  }

  // find the plugins property
  const pluginsProp = ast.value.properties
    .find(p =&amp;gt; p.name === &amp;quot;plugins&amp;quot;);
  if (pluginsProp?.value?.kind !== &amp;quot;array&amp;quot;) {
    // doesn&#39;t exist, so add it to the root object
    const lastProperty = ast.value.properties.at(-1);
    const insertIndex = lastProperty?.end ?? ast.value.start + 1;
    const maybeComma = lastProperty == null ? &amp;quot;,&amp;quot; : &amp;quot;&amp;quot;;
    changes.push({
      range: [insertIndex, insertIndex],
      text: `${maybeComma}\n  &amp;quot;plugins&amp;quot;: [\n    &amp;quot;${url}&amp;quot;  ]`,
    });
  } else {
    // add the url to the existing plugins array
    const lastPlugin = pluginsProp.value.at(-1);
    const insertIndex = lastPlugin?.end ?? pluginsProp.value.start + 1;
    const maybeComma = lastPlugin == null ? &amp;quot;,&amp;quot; : &amp;quot;&amp;quot;;

    changes.push({
      range: [insertIndex, insertIndex],
      text: `${maybeComma}\n    &amp;quot;${url}&amp;quot;`,
    });
  }

  // apply the text changes to the json text
  return applyTextChanges(jsonText, changes);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;p&gt;This is very complex. To do the high level task of adding an array element, we
need to do a lot of low level work. A proper implementation of this would need
to deal with indentation, understand what newline kind the file uses, handle
comments, and understand if the file uses trailing commas.&lt;/p&gt;
&lt;p&gt;We could address these concerns in the code, but doing so would significantly
increase its complexity and hurt maintainability. It would mean similar complex
solutions throughout the codebase making new features, changes, and bug fixes
time consuming.&lt;/p&gt;
&lt;h2 id=&quot;discarded-solution%3A-better-text-change-api&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#discarded-solution%3A-better-text-change-api&quot;&gt;Discarded Solution: Better text change API&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Some solutions &lt;a href=&quot;https://www.npmjs.com/package/jsonc-parser&quot;&gt;in the wild&lt;/a&gt; look
like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const editResult = modify(jsonText, [&amp;quot;plugins&amp;quot;], newPluginUrl, {
  isArrayInsertion: true,
});
const newText = applyEdits(jsonText, editResult);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While this solution works for many cases, I don&#39;t believe it provides the
flexibility I want for more complex JSONC modifications, such as manipulating
comments. I also wanted a solution where subsets of the JSONC data can be
focused on and manipulated in place.&lt;/p&gt;
&lt;h2 id=&quot;goal&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#goal&quot;&gt;Goal&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The API I idealized was one where the code looks similar to this list where
everything is described at a high level:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Parse the text.&lt;/li&gt;
&lt;li&gt;Get and ensure the root value is an object.&lt;/li&gt;
&lt;li&gt;Get and ensure that object has a plugins array value property.&lt;/li&gt;
&lt;li&gt;Append the url to the plugins array.&lt;/li&gt;
&lt;li&gt;Get the final text.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;solution&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#solution&quot;&gt;Solution&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The newly released 0.26 version of
&lt;a href=&quot;https://github.com/dprint/jsonc-parser&quot;&gt;jsonc-parser&lt;/a&gt; now includes a &lt;code&gt;&amp;quot;cst&amp;quot;&lt;/code&gt;
feature that can be enabled in your &lt;em&gt;Cargo.toml&lt;/em&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;jsonc-parser = { version = &amp;quot;0.26&amp;quot;, features = [&amp;quot;cst&amp;quot;] }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This exposes the &lt;code&gt;jsonc_parser::cst&lt;/code&gt; module.&lt;/p&gt;
&lt;p&gt;Now, let&#39;s rewrite the above example code using this new API:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;use jsonc_parser::cst::CstRootNode;
use jsonc_parser::cst::CstInputValue;
use jsonc_parser::errors::ParseError;
use jsonc_parser::json;

/// Add a plugin url to the dprint config file&#39;s plugins array.
///
/// ```jsonc
/// {
///   &amp;quot;plugins&amp;quot;: [
///     &amp;quot;https://plugins.dprint.dev/toml-0.6.3.wasm&amp;quot;,
///     &amp;quot;&amp;lt;new url goes here&amp;gt;&amp;quot;
///   ]
/// }
/// ```
pub fn add_to_plugins_array(
  file_text: &amp;amp;str,
  url: &amp;amp;str,
) -&amp;gt; Result&amp;lt;String, ParseError&amp;gt; {
  let root_node = CstRootNode::parse(file_text, &amp;amp;Default::default())?;
  let root_obj = root_node.object_value_or_set();
  let plugins = root_obj.array_value_or_set(&amp;quot;plugins&amp;quot;);

  plugins.ensure_multiline();
  plugins.append(json!(url));

  Ok(root_node.to_string())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The complexity is abstracted away, and low level concerns are automatically
handled.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Comments in the file are maintained and not shifted around when making
changes.&lt;/li&gt;
&lt;li&gt;Proper indentation and newlines are handled for us.&lt;/li&gt;
&lt;li&gt;If the data currently uses trailing commas, that will be respected.
&lt;ul&gt;
&lt;li&gt;Trailing commas can be forced by calling
&lt;code&gt;root_obj.set_trailing_commas(...)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There&#39;s a lot more you can do with this. I&#39;d recommend reading
&lt;a href=&quot;https://docs.rs/jsonc-parser/latest/jsonc_parser/cst/index.html&quot;&gt;the documentation&lt;/a&gt;
to see what&#39;s possible and please consider
&lt;a href=&quot;https://github.com/dprint/jsonc-parser&quot;&gt;contributing&lt;/a&gt; if you see any other
improvements. Also, please open issues for any bugs or scenarios you think it
could be smarter about.&lt;/p&gt;
&lt;h3 id=&quot;implementation&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#implementation&quot;&gt;Implementation&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;This implementation uses a concrete syntax tree (CST) which is like an abstract
syntax tree (AST), but also stores the whitespace, tokens, and comments in the
tree. This allows for easily manipulating the tree in place taking into account
everything found in the file, then printing it out when done.&lt;/p&gt;
&lt;p&gt;For parsing, I didn&#39;t want to implement a new parser for the CST, so I just
reused the existing AST parser in jsonc-parser, then converted that to a CST.
The parser already had an option for collecting tokens &amp;amp; comments, and if
you have the AST, tokens, comments, &amp;amp; original text, you can easily
construct a CST.&lt;/p&gt;
&lt;p&gt;On the internal structure of the CST, I didn&#39;t want to include any dependencies
to help with this (by default, &lt;code&gt;jsonc-parser&lt;/code&gt; has zero dependencies), so I
rolled with my own solution. Internally, each node in the tree contains an
&lt;code&gt;Rc&amp;lt;RefCell&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt; where &lt;code&gt;T&lt;/code&gt; is its data and parent. The parent is referenced via
a &lt;a href=&quot;https://doc.rust-lang.org/std/rc/struct.Weak.html&quot;&gt;weak reference&lt;/a&gt; so that
the memory used gets cleaned up when you&#39;re done (this means you must not drop
the root node or a panic may occur to prevent bugs when doing certain operations
). I&#39;m unsure if this is the best solution here, but it seems to work fine and
generally the root node is kept around to get the final text anyway.&lt;/p&gt;</content>
	</entry>
	<entry>
		<title>file_test_runner</title>
		<link href="https://dsherret.dev/posts/file-test-runner/"/>
		<updated>2024-05-12T23:15:00Z</updated>
		<id>https://dsherret.dev/posts/file-test-runner/</id>
		<content type="html">&lt;p&gt;&lt;a href=&quot;https://doc.rust-lang.org/cargo/commands/cargo-test.html&quot;&gt;&lt;code&gt;cargo test&lt;/code&gt;&lt;/a&gt; in Rust
is an excellent tool, but sometimes writing Rust code isn&#39;t the best way to
maintain certain types of tests.&lt;/p&gt;
&lt;h3 id=&quot;case%3A-deno&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#case%3A-deno&quot;&gt;Case: Deno&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Take &lt;a href=&quot;https://github.com/denoland/deno&quot;&gt;Deno&#39;s codebase&lt;/a&gt;. The pattern for
writing an integration test with the Deno binary has required writing code like
the following in Rust:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;// ~/tests/integration/run_tests.rs
itest!(002_hello {
  // these are relative from ~/tests/testdata
  args: &amp;quot;run --quiet --reload run/002_hello.ts&amp;quot;,
  output: &amp;quot;run/002_hello.ts.out&amp;quot;,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This macro expands to a
&lt;a href=&quot;https://doc.rust-lang.org/reference/attributes/testing.html#the-test-attribute&quot;&gt;&lt;code&gt;#[test]&lt;/code&gt;&lt;/a&gt;
function, which launches the Deno binary with the provided arguments and asserts
its output against the provided file.&lt;/p&gt;
&lt;p&gt;Problems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Requires recompiling the test binary when adding/changing/deleting tests.&lt;/li&gt;
&lt;li&gt;Test definition is in a different folder than the files being run or
asserted—lots of tests in a single file using lots of data files in another
folder.
&lt;ul&gt;
&lt;li&gt;Changing folders felt like context switching because of how far away they
were.&lt;/li&gt;
&lt;li&gt;It was hard to associate what testdata files were for what test.
&lt;ul&gt;
&lt;li&gt;These files were often not deleted when the test definition was deleted.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;It didn&#39;t encourage developers to write a lot of tests.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Ideally a single test or group of related tests should be co-located in the same
folder as the testdata files and not be defined in Rust.&lt;/p&gt;
&lt;h3 id=&quot;case%3A-dprint&#39;s-formatters&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#case%3A-dprint&#39;s-formatters&quot;&gt;Case: dprint&#39;s formatters&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In &lt;a href=&quot;https://dprint.dev/&quot;&gt;dprint&lt;/a&gt;&#39;s formatter codebases, I&#39;ve long stored tests
in text files. For example, here&#39;s an example in
&lt;a href=&quot;https://github.com/dprint/dprint-plugin-json&quot;&gt;dprint-plugin-json&lt;/a&gt; (JSON/JSONC
formatter) at
&lt;a href=&quot;https://github.com/dprint/dprint-plugin-json/blob/3df017f5ffe9d489503b262c9cf0988573cb1566/tests/specs/strings/Strings_All.txt&quot;&gt;tests/specs/strings/Strings_All.txt&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;~~ lineWidth: 80 ~~
== should support single quote strings ==
&#39;te\&#39;st&#39;

[expect]
&amp;quot;te&#39;st&amp;quot;

== should support double quote strings ==
&amp;quot;test\&amp;quot;test&amp;quot;

[expect]
&amp;quot;test\&amp;quot;test&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, it&#39;s groups of related tests stored in the same file.&lt;/p&gt;
&lt;p&gt;Problems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Had custom filtering.&lt;/li&gt;
&lt;li&gt;Had its own custom infrastructure for running these tests.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Benefits:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Didn&#39;t require recompiling after updates.&lt;/li&gt;
&lt;li&gt;Test file was tailored to the situation being tested.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;goals-for-writing-a-new-test-runner&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#goals-for-writing-a-new-test-runner&quot;&gt;Goals for writing a new test runner&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I wanted a test runner that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Allows storing the test definition close to the files used in the tests and
the expected output.&lt;/li&gt;
&lt;li&gt;Doesn&#39;t require recompiling Rust code when adding, changing, or deleting
tests.&lt;/li&gt;
&lt;li&gt;Is non-opinionated to allow structuring the tests according to the needs of
the project.&lt;/li&gt;
&lt;li&gt;Runs the tests in parallel.&lt;/li&gt;
&lt;li&gt;Allows filtering via &lt;code&gt;cargo test &amp;lt;test_name&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;solution%3A-file_test_runner&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#solution%3A-file_test_runner&quot;&gt;Solution: file_test_runner&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The solution I&#39;ve settled on is
&lt;a href=&quot;https://github.com/denoland/file_test_runner&quot;&gt;file_test_runner&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This does two main steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Collects tests in any format on the file system.&lt;/li&gt;
&lt;li&gt;Runs each test using custom provided code.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The basic setup is as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Add a &lt;code&gt;[[test]]&lt;/code&gt; section to the project&#39;s Cargo.toml with the default test
harness disabled:&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;[[test]]
name = &amp;quot;specs&amp;quot;
path = &amp;quot;tests/spec_test.rs&amp;quot;
harness = false
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Create a &lt;code&gt;tests/spec_test.rs&lt;/code&gt; file to run the code:&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;use file_test_runner::collect_and_run_tests;
use file_test_runner::collection::CollectedTest;
use file_test_runner::collection::CollectOptions;
use file_test_runner::RunOptions;
use file_test_runner::TestResult;

fn main() {
  collect_and_run_tests(
    CollectOptions {
      base: &amp;quot;tests/specs&amp;quot;.into(),
      strategy: Box::new(..omitted..),
      filter_override: None,
    },
    RunOptions {
      parallel: true,
    },
    // custom function to run the test...
    |test| {
      // do something like this, or do some checks yourself and
      // return a value like TestResult::Passed
      TestResult::from_maybe_panic(AssertUnwindSafe(|| {
        // run the test here
      }))
    }
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Add test files or directories in any format to the &lt;code&gt;tests/specs/&lt;/code&gt; folder as
specified above and update the code above to handle it.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;collecting-tests&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#collecting-tests&quot;&gt;Collecting tests&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Tests can be collected from the file system using several
&lt;a href=&quot;https://docs.rs/file_test_runner/0.7.0/file_test_runner/collection/strategies/index.html&quot;&gt;strategies&lt;/a&gt;
(note the &lt;code&gt;strategy&lt;/code&gt; property under &lt;code&gt;CollectOptions&lt;/code&gt; above).&lt;/p&gt;
&lt;p&gt;For example, by a file in a directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;// goes recursively through each directory under the base
// (&amp;quot;tests/specs&amp;quot;) and finds the directories with a
// `__test__.jsonc` file
strategy: Box::new(TestPerDirectoryCollectionStrategy {
  file_name: &amp;quot;__test__.jsonc&amp;quot;.into(),
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or by all descendant files:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;// goes recursively through each directory under the base
// (&amp;quot;test/specs&amp;quot;) excluding readme.md files and collects
// a test per file
strategy: Box::new(TestPerFileCollectionStrategy {
  file_pattern: None,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you need more flexibility than that, you can implement your own
&lt;a href=&quot;https://docs.rs/file_test_runner/0.7.0/file_test_runner/collection/strategies/trait.TestCollectionStrategy.html&quot;&gt;&lt;code&gt;file_test_runner::collection::strategies::TestCollectionStrategy&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;pub trait TestCollectionStrategy&amp;lt;TData = ()&amp;gt; {
  fn collect_tests(
    &amp;amp;self,
    base: &amp;amp;Path
  ) -&amp;gt; Result&amp;lt;CollectedTestCategory&amp;lt;TData&amp;gt;, CollectTestsError&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is extremely flexible and even allows collecting multiple tests within the
same file (the
&lt;a href=&quot;https://docs.rs/file_test_runner/0.7.0/file_test_runner/collection/strategies/struct.FileTestMapperStrategy.html&quot;&gt;file_test_runner::collection::strategies::FileTestMapperStrategy&lt;/a&gt;
is helpful for that).&lt;/p&gt;
&lt;h3 id=&quot;running-tests&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#running-tests&quot;&gt;Running tests&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;After tests are collected, &lt;code&gt;file_test_runner&lt;/code&gt; will go through each category of
tests running them in parallel on different threads, providing each test to the
closure to run a test:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;// custom function to run the test...
|test| {
  // do something like this, or do some checks yourself and
  // return a value like TestResult::Passed
  TestResult::from_maybe_panic(AssertUnwindSafe(|| {
    // Properties:
    // * `test.name` - Fully resolved name of the test
    // * `test.path` - Path to the test file this test is associated with
    // * `test.data` - Data associated with the test that may have been
    //                 set by the collection strategy
  }))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;benefits&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#benefits&quot;&gt;Benefits&lt;/a&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;file_test_runner&lt;/code&gt; handles collecting tests, orchestrating tests, and
reporting test results to the console.&lt;/li&gt;
&lt;li&gt;Tests can be added/modified/deleted without recompiling the Rust test
binary—it&#39;s very fast to re-run a test.&lt;/li&gt;
&lt;li&gt;Test reporter output looks very similar to &lt;code&gt;cargo test&lt;/code&gt;&#39;s default, but also
shows how long each test takes to run.&lt;/li&gt;
&lt;li&gt;Tests can be filtered using &lt;code&gt;cargo test &amp;lt;test_name&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Tests can be structured in whatever makes most sense for what&#39;s being tested
since it&#39;s non-opinionated. Stuff like snapshot testing can be implemented
within the run test function.&lt;/li&gt;
&lt;li&gt;Tests are run in parallel.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can see the documentation for the implementation we ended up using in Deno
&lt;a href=&quot;https://github.com/denoland/deno/blob/dac49a116e094be1a3048305dfb6b10bbddcc030/tests/specs/README.md&quot;&gt;here&lt;/a&gt;
(we&#39;re still in the process of migrating all the &lt;code&gt;itest&lt;/code&gt;s to it).&lt;/p&gt;
&lt;h3 id=&quot;future&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#future&quot;&gt;Future&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Right now there&#39;s not any support for &lt;code&gt;cargo test -- --nocapture&lt;/code&gt;. I&#39;m not
entirely sure how to handle it (especially when a test uses multiple threads),
but at the moment it doesn&#39;t capture any output within a test and a test
implementation needs to handle that itself.&lt;/p&gt;
&lt;p&gt;Overall I&#39;m quite satisfied with this crate and it&#39;s made adding these kind of
tests in several of Deno&#39;s repos much easier.&lt;/p&gt;</content>
	</entry>
	<entry>
		<title>dax - Cross-platform shell tools for Node.js</title>
		<link href="https://dsherret.dev/posts/dax-node-js/"/>
		<updated>2024-02-09T15:00:00Z</updated>
		<id>https://dsherret.dev/posts/dax-node-js/</id>
		<content type="html">&lt;p&gt;In &lt;a href=&quot;https://dsherret.dev/posts/dax&quot;&gt;July 2022&lt;/a&gt;, I released &lt;a href=&quot;https://github.com/dsherret/dax&quot;&gt;dax&lt;/a&gt;
for &lt;a href=&quot;https://github.com/denoland/deno&quot;&gt;Deno&lt;/a&gt; providing a cross-platform shell
for JavaScript written in JavaScript:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const data = $.path(&amp;quot;data.json&amp;quot;).readJsonSync();
await $`git add . &amp;amp;&amp;amp; git commit -m &amp;quot;Release ${data.version}&amp;quot;`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is similar and inspired by &lt;a href=&quot;https://github.com/google/zx&quot;&gt;zx&lt;/a&gt;, but because
it uses a cross-platform shell with common built-in cross-platform commands,
more code is going to work the same way on different operating systems.&lt;/p&gt;
&lt;p&gt;Initially, I wrote dax for Deno because Deno is by far the best JavaScript
runtime for single file scripting—all dependencies can be expressed in the
script file itself including npm dependencies; there&#39;s no &lt;code&gt;node_modules&lt;/code&gt; folder
(less clutter), and no separate install command necessary.&lt;/p&gt;
&lt;p&gt;Once written, dax used APIs that only worked on Deno and creating a Node.js
distribution was a decent amount of work.&lt;/p&gt;
&lt;p&gt;Nowadays, Node.js has improved in its support for Web APIs and improvements to
&lt;a href=&quot;https://github.com/denoland/dnt&quot;&gt;dnt&lt;/a&gt; (a tool I created for building Deno
modules for Node) have made maintaining a Node.js distribution much easier.&lt;/p&gt;
&lt;p&gt;Due to this, I&#39;m happy to say that dax is now available on npm for users of
Node.js:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// example.mjs
import $ from &amp;quot;dax-sh&amp;quot;;

await $`echo &#39;Hello from dax!&#39;`;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-shellsession&quot;&gt;$ npm install --save-dev dax-sh
$ node example.mjs
Hello from dax!
$ time node example.mjs
Hello from dax!
node example.mjs 0.08s user 0.01 system 98% cpu 0.090 total
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can check out dax&#39;s documentation here for more details:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/dsherret/dax&quot;&gt;https://github.com/dsherret/dax&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;a-long-aside%3A-build-dax-into-deno%3F&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#a-long-aside%3A-build-dax-into-deno%3F&quot;&gt;A long aside: build dax into Deno?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Part of what kicked off my desire to create a Node.js distribution for dax was
the release of &lt;a href=&quot;https://bun.sh/blog/the-bun-shell&quot; rel=&quot;nofollow&quot;&gt;Bun&#39;s
shell&lt;/a&gt;, which
&lt;a href=&quot;https://github.com/oven-sh/bun/blob/b433beb016470b87850f3c018974de5f2e355d52/docs/runtime/shell.md?plain=1#L359&quot; rel=&quot;nofollow&quot;&gt;credits
dax&lt;/a&gt; as a source of inspiration.&lt;/p&gt;
&lt;p&gt;This led to requests for dax to be baked into Deno&#39;s runtime.&lt;/p&gt;
&lt;p style=&quot;text-align: center&quot;&gt;
  &lt;img
    src=&quot;https://dsherret.dev/dax-runtime.png&quot;
    alt=&quot;Discord message saying: &#39;Hope this means dax gets introduced into the runtime, you love to see it&#39;&quot;
    style=&quot;max-width: 100%; height: auto;&quot;
  /&gt;
&lt;/p&gt;
&lt;p&gt;In my opinion, this would be a step backwards for dax and not a good long term
decision for Deno.&lt;/p&gt;
&lt;p&gt;I want to explain why I think this and it would be interesting to hear your
feedback. Note these are my personal opinions and not the opinions of the Deno
team (which I&#39;m a member, but dax is a personal project I work on in my personal
time).&lt;/p&gt;
&lt;h3 id=&quot;runtime-coupling&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#runtime-coupling&quot;&gt;Runtime coupling&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Coupling a complex API like dax to the runtime means you can no longer upgrade
them independently. Being able to depend on a specific version of dax and a
specific version of your runtime is a massive benefit. It means you can freely
upgrade your runtime version and the code using dax will mostly likely keep
working too—the chance of encountering a new dax bug while upgrading your
runtime is very low because they&#39;re decoupled.&lt;/p&gt;
&lt;p&gt;Additionally, it also means when you upgrade your runtime, you don&#39;t need to
also upgrade all your dax code at the same time in case there&#39;s a breaking
change.&lt;/p&gt;
&lt;p&gt;It also means you likely don&#39;t need to tell people to use a certain version of
Deno in order to get the latest dax features (&amp;quot;hey, why doesn&#39;t this work? Oh,
that dax feature is only in Deno version x.x.x&amp;quot;). Instead, the code specifies
the dax version it depends on so when you execute it, it likely works or dax can
provide specific error messages for the runtime when not.&lt;/p&gt;
&lt;h3 id=&quot;vendor-lock-in&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#vendor-lock-in&quot;&gt;Vendor Lock-in&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Being able to use the same API in different runtimes is a massive benefit. It
lowers vendor lock-in risk and lowers the complexity when working with multiple
runtimes because the APIs you&#39;re using are the same. It also means when the next
great runtime comes around you&#39;re not locked in with all this code depending on
a specific runtime (or a specific version of a specific runtime 😱).&lt;/p&gt;
&lt;p&gt;When dax is published as a library, you can switch runtimes and still depend on
the same version of dax.&lt;/p&gt;
&lt;h3 id=&quot;scope&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#scope&quot;&gt;Scope&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Dax is not only a shell, but a collection shell tools. It&#39;s a swiss army knife
that provides opinionated ways of doing common tasks you need to do in
automation scripts. It has APIs for...&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;progress and selection,&lt;/li&gt;
&lt;li&gt;making URL requests,&lt;/li&gt;
&lt;li&gt;logging,&lt;/li&gt;
&lt;li&gt;dealing with paths,&lt;/li&gt;
&lt;li&gt;and in the future, CLI argument parsing and work caching.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;All these APIs work together with each other and the shell. They&#39;re opinionated
for simplicity. Baking opinionated APIs into a runtime wouldn&#39;t be a good idea
because people have different opinions and opinions change over time. In the
case of dax being a library, someone else can come along and improve on its API
or make something better in the future, at which point dax can become a relic
just like old JS frameworks.&lt;/p&gt;
&lt;p&gt;One suggestion is to cut the scope of dax back to a shell only rather than a
collection of shell tools, but the shell is still quite large. For example, you
can build your own custom &lt;code&gt;$&lt;/code&gt; to suite your needs and inject your own custom
shell commands written in JavaScript.&lt;/p&gt;
&lt;p&gt;Cutting it back further to not include that and some other features is possible,
but the shell itself is still quite intricate and there&#39;s lots of tiny design
decisions that are better left to a library like dax to get wrong and then be
improved upon by a future library or future major version of dax. Also at a
certain point scope gets cut back enough that it starts becoming less useful.&lt;/p&gt;
&lt;h3 id=&quot;built-in-runtime-apis-should-be-permanent&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#built-in-runtime-apis-should-be-permanent&quot;&gt;Built-in runtime APIs should be permanent&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I&#39;m still slowly figuring out an appropriate API for dax. I don&#39;t believe
anything is going to change drastically, but making a mistake if it were a
built-in runtime API would be fatal. Built-in APIs and the decisions made should
ideally be permanent. When they&#39;re not permanent or get removed, that creates a
lot of headaches.&lt;/p&gt;
&lt;p&gt;When it&#39;s in a library, it&#39;s behind a separately versioned API, so the chance of
your code not working with the runtime anymore is slim, and making breaking
changes in library that&#39;s behind a versioned API is much more manageable.&lt;/p&gt;
&lt;p&gt;Imagine if a similar API to dax had been integrated into the runtime that made
the mistake of spawning the system shell because we hadn&#39;t thought to make it
cross platform yet? Image what other possibilities for this API we&#39;ll discover
in the future and be glad we can easily make the changes to improve it because
it exists as a library.&lt;/p&gt;
&lt;h3 id=&quot;performance%3F&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#performance%3F&quot;&gt;Performance?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Part of the argument to integrate this API into the runtime is for performance,
but dax starts up in 90ms on my machine in Node.js and 70ms in Deno. It executes
commands almost as fast as using Deno&#39;s &lt;code&gt;Command&lt;/code&gt; API (2ms slower on my
machine). Could it be faster? Probably... I haven&#39;t done any extensive
benchmarking on dax because I develop it in my free time around all the other
projects I do.&lt;/p&gt;
&lt;p&gt;It&#39;s fast enough for my needs. You&#39;d definitely be able to show it being slower
than some native code in a hot loop, but generally automation scripts only
execute a handful of commands (maybe ~10 commands) and spend most of their time
waiting for long complex tasks to finish (for me, stuff like &lt;code&gt;cargo build&lt;/code&gt;), so
gaining some milliseconds by it being built-in and native doesn&#39;t help much in
most real world scripts.&lt;/p&gt;
&lt;p&gt;Plus being less productive writing automation scripts with a less featureful API
will use up far more of your time than the few milliseconds saved with it being
built-in, which won&#39;t even be meaningfully saved in most real world scenarios.&lt;/p&gt;
&lt;p&gt;If we&#39;re optimizing for performance only, dax actually doesn&#39;t need to be
built-in and could go native using Deno&#39;s FFI support, but in my opinion
creating less portable less auditable code written in a language not as many
people understand to have a slightly better performance experience is a bad
trade.&lt;/p&gt;
&lt;h3 id=&quot;convenience-of-no-dependency%3F&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#convenience-of-no-dependency%3F&quot;&gt;Convenience of no dependency?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I wouldn&#39;t categorize having no dependency as a convenience because the runtime
coupling I talked about in a previous section leads to inconvenience. Maybe it&#39;s
slightly annoying in Node.js because it requires adding `dax-sh`` to a
package.json and installing it, but in Deno you can just write:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;#!/usr/bin/env -S deno run -A
import $ from &amp;quot;https://deno.land/x/dax/0.39.0/mod.ts&amp;quot;;

await $`echo Hello`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Is writing that difficult? I don&#39;t believe so, and now my script has all the
information to know what version of dax to use or I can swap it out for a
similar dependency that has the API I like instead.&lt;/p&gt;
&lt;p&gt;It&#39;s great in Deno because I don&#39;t even need to run a separate install script—I
just run that script directly and it will use the version I specified. Of
course, I could use a bare specifier like &lt;code&gt;&amp;quot;dax&amp;quot;&lt;/code&gt; by creating a deno.json with
an embedded &lt;a href=&quot;https://github.com/WICG/import-maps&quot;&gt;import map&lt;/a&gt; to make
&lt;code&gt;import $ from &amp;quot;dax&amp;quot;;&lt;/code&gt; work:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;imports&amp;quot;: {
    &amp;quot;dax&amp;quot;: &amp;quot;https://deno.land/x/dax/0.39.0/mod.ts&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;jsr%3A%40deno%2Fshell%401%3F&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#jsr%3A%40deno%2Fshell%401%3F&quot;&gt;&lt;code&gt;jsr:@deno/shell@1&lt;/code&gt;?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Overall, I get the desire for having dax built-in, but I don&#39;t believe it&#39;s the
right long term decision. Perhaps if there&#39;s a desire for a shell only and not a
swiss army knife of automation scripts, then the core functionality in dax could
be extracted out to a simpler package on the upcoming
&lt;a href=&quot;https://jsr.io/&quot;&gt;JSR registry&lt;/a&gt; behind its own versioned API.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import $ from &amp;quot;jsr:@deno/shell@1&amp;quot;;

await $`echo &#39;Hello there!&#39;`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let me know if there&#39;s a desire for a less functional, more lightweight version
of dax like that and I&#39;ll look into making it happen.&lt;/p&gt;
&lt;p&gt;Again, you can now install dax via &lt;code&gt;npm install --save-dev dax-sh&lt;/code&gt; and use it in
Node.js. Read the documentation here:
&lt;a href=&quot;https://github.com/dsherret/dax&quot;&gt;https://github.com/dsherret/dax&lt;/a&gt;&lt;/p&gt;</content>
	</entry>
	<entry>
		<title>Lost scrobbles and JavaScript Jupyter Notebooks</title>
		<link href="https://dsherret.dev/posts/lost-scrobbles/"/>
		<updated>2024-01-16T21:00:00Z</updated>
		<id>https://dsherret.dev/posts/lost-scrobbles/</id>
		<content type="html">&lt;p&gt;I&#39;ve been a &lt;a href=&quot;https://en.wikipedia.org/wiki/Last.fm&quot;&gt;Last.fm&lt;/a&gt; user since 2008 and
a pro user for the past few years to support the service.&lt;/p&gt;
&lt;p&gt;For those not familiar, Last.fm allows someone to track the music they&#39;ve
listened to from mostly any music client. These listens are called &amp;quot;scrobbles&amp;quot;.&lt;/p&gt;
&lt;p&gt;For example, Spotify has integration through a connected app and every time I
listen to a song on Spotify it sends that song to Last.fm.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dsherret.dev/spotify_last_fm_integration.png&quot; alt=&quot;Shows the Last.fm Scrobbler in Spotify&#39;s apps.&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;spotify-wrapped&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#spotify-wrapped&quot;&gt;Spotify Wrapped&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Every December, Spotify releases Spotify Wrapped, which I&#39;m sure you&#39;re familiar
with.&lt;/p&gt;
&lt;p&gt;This year, I noticed some discrepancy between the results from Spotify and
Last.fm.&lt;/p&gt;
&lt;h3 id=&quot;spotify---top-artists-and-songs&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#spotify---top-artists-and-songs&quot;&gt;Spotify - Top artists and songs&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dsherret.dev/spotify_wrapped.png&quot; alt=&quot;Spotify Top Artists (Alpha 9, M83, Royksopp, The Knocks, Hania Rani). Top Songs (Sacrifice, Luna, Ordinary Love, kisses, If We Ever Find The Right Place)&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;last.fm---top-artists&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#last.fm---top-artists&quot;&gt;Last.fm - Top artists&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dsherret.dev/last_fm_top_artists.png&quot; alt=&quot;Last.fm Top Artists (i_o, Hammock, M83, Royksopp, The Knocks, Cinnamon Chasers, Roosevelt, Fay Wildhagen, Hania Rani, Cannons)&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;last.fm---top-songs&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#last.fm---top-songs&quot;&gt;Last.fm - Top songs&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dsherret.dev/last_fm_top_songs.png&quot; alt=&quot;Last.fm Top Songs (Luna, If We Ever Find The Right Place, kisses, Set Sails, One Man Band, Ordinary Love, anything but wet, The Murder of Love, Vegas High, Holding Me Like Water)&quot;&gt;&lt;/p&gt;
&lt;p&gt;There are some clear discrepancies between these two sources and I use Spotify
to listen to music ~99% of the time. Most notably, my top artist for Spotify is
Alpha 9, yet Last.fm says it&#39;s i_o. Neither appear in the other.&lt;/p&gt;
&lt;p&gt;My initial thought was that perhaps Spotify has a different reporting period
than Last.fm. This is definitely the case with my top artist on Last.fm (i_o) as
I listened to him a lot early December 2022 and I was looking at these numbers
around December 1st 2023. That said, it doesn&#39;t explain everything. One issue
I&#39;ve had is Last.fm scrobbles just not happening and I then need to reconnect it
to get it working again. I was kind of curious just how many scrobbles were
being lost.&lt;/p&gt;
&lt;p&gt;Luckily it&#39;s possible to get all the raw data from Spotify and Last.fm so that I
can try to better understand what&#39;s happening.&lt;/p&gt;
&lt;h2 id=&quot;getting-the-data&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#getting-the-data&quot;&gt;Getting the data&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id=&quot;spotify&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#spotify&quot;&gt;Spotify&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Spotify provides this data for download under Profile &amp;gt; Account &amp;gt; Privacy
settings.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.spotify.com/ca-en/account/privacy/&quot;&gt;https://www.spotify.com/ca-en/account/privacy/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dsherret.dev/spotify_extended_streaming_history.png&quot; alt=&quot;Spotify&#39;s extended streaming history form component&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;last.fm&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#last.fm&quot;&gt;Last.fm&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Last.fm is a little trickier, but luckily there is a third party website that
helps download your entire listening history via Last.fm&#39;s API:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://benjaminbenben.com/lastfm-to-csv/&quot;&gt;https://benjaminbenben.com/lastfm-to-csv/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I found this did the job, but it would occasionally error on some API calls. A
quick fix was to apply &lt;a href=&quot;https://dsherret.dev/lastfm_to_csv.diff&quot;&gt;this patch&lt;/a&gt; to
&lt;a href=&quot;https://github.com/benfoxall/lastfm-to-csv&quot;&gt;the repo&lt;/a&gt; to retry on failure in
order to make it more reliable.&lt;/p&gt;
&lt;h2 id=&quot;javascript-jupyter-notebooks&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#javascript-jupyter-notebooks&quot;&gt;JavaScript Jupyter Notebooks&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Now that I had the data, I needed a way to analyze it. My data analysis
background is very poor and usually in this case I would just write a quick
script in whatever language is easiest for the task.&lt;/p&gt;
&lt;p&gt;In this case, Deno recently released
&lt;a href=&quot;https://deno.com/blog/v1.37&quot;&gt;Jupyter Notebook support&lt;/a&gt; and I hadn&#39;t really
tried it out yet (my colleague &lt;a href=&quot;https://twitter.com/biwanczuk&quot;&gt;Bartek&lt;/a&gt; did most
of the work implementing it) nor had I ever used a Jupyter Notebook. This seemed
like a good occasion.&lt;/p&gt;
&lt;p&gt;I
&lt;a href=&quot;https://docs.deno.com/runtime/manual/tools/jupyter&quot;&gt;setup Jupyter for Deno in VS Code&lt;/a&gt;,
created a &lt;em&gt;notebook.ipynb&lt;/em&gt; file, a &lt;em&gt;deno.json&lt;/em&gt; file with &lt;code&gt;{}&lt;/code&gt; in it (to activate
Deno&#39;s language server and automatically get a Deno lockfile), then added the
Last.fm &amp;amp; Spotify data to the same folder.&lt;/p&gt;
&lt;h3 id=&quot;loading-and-normalizing-the-data&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#loading-and-normalizing-the-data&quot;&gt;Loading and normalizing the data&lt;/a&gt;&lt;/h3&gt;
&lt;h4 id=&quot;last.fm-1&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#last.fm-1&quot;&gt;Last.fm&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;The Last.fm data is a csv file. I created a notebook cell that loaded and
normalized the data like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import $, { PathRef } from &amp;quot;https://deno.land/x/dax@0.36.0/mod.ts&amp;quot;;
import { parse as parseCsv } from &amp;quot;npm:csv-string@4.1.1&amp;quot;;

interface NormalizedRow {
  artist: string;
  album: string;
  track: string;
  date: Date;
}

const lastFmText = $.path(&amp;quot;lastfm.csv&amp;quot;).readTextSync();
const lastFmData: NormalizedRow[] = parseCsv(lastFmText).map((
  row: string[],
) =&amp;gt; ({
  artist: row[0].trim(),
  album: row[1].trim(),
  track: row[2].trim(),
  date: new Date(row[3] + &amp;quot; GMT&amp;quot;),
})).reverse();

console.log(&amp;quot;Loaded&amp;quot;, lastFmData.length, &amp;quot;rows&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;spotify-1&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#spotify-1&quot;&gt;Spotify&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Spotify stores the streaming data in multiple JSON files.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Streaming_History_Audio_2012-2014_0.json
Streaming_History_Audio_2014-2015_1.json
...
Streaming_History_Audio_2022-2023_10.json
Streaming_History_Audio_2023_11.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I loaded it like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;interface SpotifyRow {
  ts: string;
  ms_played: number;
  master_metadata_track_name: string;
  master_metadata_album_artist_name: string;
  master_metadata_album_album_name: string;
  reason_start: string;
  reason_end: string;
}

function normalizeSpotify(row: SpotifyRow): NormalizedRow {
  return {
    artist: row.master_metadata_album_artist_name.trim(),
    album: row.master_metadata_album_album_name.trim(),
    track: row.master_metadata_track_name.trim(),
    date: new Date(Date.parse(row.ts) - row.ms_played),
  };
}

function loadFromFile(path: PathRef) {
  return path
    .readJsonSync&amp;lt;SpotifyRow[]&amp;gt;()
    .filter((row) =&amp;gt;
      row.master_metadata_album_artist_name != null
      &amp;amp;&amp;amp; row.master_metadata_album_album_name != null
      &amp;amp;&amp;amp; row.master_metadata_track_name != null
      // not perfect, but probably a close enough approximation
      // to what counts as a scrobble
      &amp;amp;&amp;amp; (row.reason_end === &amp;quot;trackdone&amp;quot; || row.ms_played &amp;gt; 120_000)
    )
    .map(normalizeSpotify);
}

const spotifyData = Array.from($.path(&amp;quot;.&amp;quot;).readDirSync())
  .filter(entry =&amp;gt;
    entry.isFile &amp;amp;&amp;amp; entry.name.startsWith(&amp;quot;Streaming_History_Audio&amp;quot;)
  )
  .map(entry =&amp;gt; entry.path)
  .sort((a, b) =&amp;gt; a.toString().localeCompare(b.toString()))
  .map(path =&amp;gt; loadFromFile(path))
  .flat();

console.log(&amp;quot;Loaded&amp;quot;, spotifyData.length, &amp;quot;rows&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What&#39;s hard to determine here is what Spotify play is worthy of being counted as
a Last.fm scrobble. Spotify stores all listens—even if it&#39;s only 3
seconds—whereas Last.fm only stores plays that it considers to be actually
listening to the song. I don&#39;t know how Last.fm does this, so I approximated it
with the condition &lt;code&gt;row.reason_end === &amp;quot;trackdone&amp;quot; || row.ms_played &amp;gt; 120_000&lt;/code&gt;,
which is definitely inaccurate.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Due to me not really knowing this condition, you should be interpret the charts
in this post with a low level of confidence as how I modify this condition can
have a big impact on the output.&lt;/em&gt; That said, I think how this condition is
structured is probably good enough to get some idea about what&#39;s going on.&lt;/p&gt;
&lt;p&gt;At this point I have the Last.fm data in &lt;code&gt;lastFmData&lt;/code&gt; and Spotify data in
&lt;code&gt;spotifyData&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;outputting-difference-in-total-plays-in-2023&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#outputting-difference-in-total-plays-in-2023&quot;&gt;Outputting difference in total plays in 2023&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To start, I wanted to find out on which days did I have more total plays in
Spotify vs Last.fm in 2023:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { display } from &amp;quot;https://deno.land/x/display@v1.1.2/mod.ts&amp;quot;;
import * as Plot from &amp;quot;npm:@observablehq/plot@^0.6&amp;quot;;
import { DOMParser } from &amp;quot;npm:linkedom@^0.16&amp;quot;;

interface DateRange {
  start: Date;
  end: Date;
}

function filterData(data: NormalizedRow[], dateRange: DateRange) {
  return data.filter(row =&amp;gt;
    row.date &amp;gt;= dateRange.start &amp;amp;&amp;amp; row.date &amp;lt; dateRange.end
  );
}

async function displayPlaysPerDay(dateRange: DateRange) {
  // There is most definitely a better way of doing this directly with
  // observablehq/plot, but I didn&#39;t want to spend too much time going
  // through the documentation figuring it out

  function getDayKey(date: Date) {
    return date.getFullYear() + &amp;quot;-&amp;quot; + date.getMonth() + &amp;quot;-&amp;quot; + date.getDate();
  }

  function getPlaysPerDay(data: NormalizedRow[]) {
    const counts = new Map&amp;lt;string, number&amp;gt;();
    for (const { date } of data) {
      const day = getDayKey(date);
      counts.set(day, (counts.get(day) ?? 0) + 1);
    }
    return counts;
  }

  const spotifyPlaysPerDay = getPlaysPerDay(
    filterData(spotifyData, dateRange),
  );
  const lastFmPlaysPerDay = getPlaysPerDay(
    filterData(lastFmData, dateRange),
  );
  let date = dateRange.start;
  const rows = [];
  while (date &amp;lt; dateRange.end) {
    const day = getDayKey(date);
    const spotifyCount = spotifyPlaysPerDay.get(day) ?? 0;
    const lastFmCount = lastFmPlaysPerDay.get(day) ?? 0;
    rows.push({
      date: new Date(date),
      count: spotifyCount - lastFmCount,
    });
    date.setDate(date.getDate() + 1);
  }

  // create a virtual document
  const document = new DOMParser().parseFromString(
    `&amp;lt;!DOCTYPE html&amp;gt;&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;&amp;lt;/html&amp;gt;`,
    &amp;quot;text/html&amp;quot;,
  );

  // output the results
  await display(Plot.plot({
    marks: [
      Plot.line(rows, {
        x: &amp;quot;date&amp;quot;,
        y: &amp;quot;count&amp;quot;,
        z: null,
        stroke: (r) =&amp;gt; r.count &amp;gt;= 0 ? &amp;quot;green&amp;quot; : &amp;quot;red&amp;quot;,
      }),
    ],
    document,
  }));
}

await displayPlaysPerDay({
  start: new Date(&amp;quot;2023-01-01 00:00:00 EST&amp;quot;),
  // I downloaded all the data from Last.fm around December 1st, but
  // it would be several weeks until I received the data from Spotify.
  end: new Date(&amp;quot;2023-12-01 00:00:00 EST&amp;quot;),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Outputs:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dsherret.dev/difference_plays.svg&quot; alt=&quot;Chart shows a lot of Spotify plays that don&#39;t happen on Last.fm&quot;&gt;&lt;/p&gt;
&lt;p&gt;Positive (green) values show more Spotify plays on a day. Negative (red) values
show more Last.fm scrobbles.&lt;/p&gt;
&lt;p&gt;In an ideal world, the Spotify plays would be a subset of the Last.fm ones or
only lag behind by a day or two (as shown occasionally in the chart), but that
doesn&#39;t seem to be the case here. I further looked into these numbers and found
that there are ~830 plays that are exclusive to Spotify and ~410 scrobbles
exclusive to Last.fm. It&#39;s not clear who is at fault here getting the data into
Last.fm and I don&#39;t want to speculate in this post.&lt;/p&gt;
&lt;p&gt;Overall, this is not terrible, but it also doesn&#39;t seem perfect.&lt;/p&gt;
&lt;h3 id=&quot;past-years-play-count-difference---2017-2022&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#past-years-play-count-difference---2017-2022&quot;&gt;Past years play count difference - 2017-2022&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Looking at past years, here&#39;s 2017-2022 inclusive:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;await displayPlaysPerDay({
  start: new Date(&amp;quot;2017-01-01 00:00:00 EST&amp;quot;),
  end: new Date(&amp;quot;2023-01-01 00:00:00 EST&amp;quot;),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://dsherret.dev/difference_plays_2017.svg&quot; alt=&quot;Chart shows a lot of Spotify plays that don&#39;t happen on Last.fm from 2017-2022&quot;&gt;&lt;/p&gt;
&lt;p&gt;The large red spike was me backfilling Last.fm manually with their API because
scrobbling got disconnected for a month or so.&lt;/p&gt;
&lt;h3 id=&quot;past-years-play-count-difference---2012-2016&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#past-years-play-count-difference---2012-2016&quot;&gt;Past years play count difference - 2012-2016&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Finally, here&#39;s 2012-2016 inclusive:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;await displayPlaysPerDay({
  start: new Date(&amp;quot;2012-01-01 00:00:00 EST&amp;quot;),
  end: new Date(&amp;quot;2017-01-01 00:00:00 EST&amp;quot;),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://dsherret.dev/difference_plays_2012.svg&quot; alt=&quot;Chart shows a lot of Spotify plays that don&#39;t happen on Last.fm from 2012-2016&quot;&gt;&lt;/p&gt;
&lt;p&gt;My transition to Spotify started in 2012 and it seems like the Last.fm / Spotify
integration for my account was very reliable at the start.&lt;/p&gt;
&lt;h2 id=&quot;top-10-artists-plays-exclusive-to-spotify-in-2023&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#top-10-artists-plays-exclusive-to-spotify-in-2023&quot;&gt;Top 10 artists plays exclusive to Spotify in 2023&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I also looked at the plays exclusive to Spotify in 2023 and saw this huge
standout, which explains why my top artist on Spotify (Alpha 9) was barely
present in the Last.fm:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dsherret.dev/top_exclusive_spotify_artist_plays.svg&quot; alt=&quot;Chart shows Alpha 9 having ~110 plays that were never synced to last.fm&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;thoughts-on-deno-jupyter-notebook-experience&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#thoughts-on-deno-jupyter-notebook-experience&quot;&gt;Thoughts on Deno Jupyter Notebook experience&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Overall it was quite enjoyable to use Deno in a Jupyter Notebook.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I like how dependencies are expressed in the notebook itself. Deno&#39;s single
file scripting support translates well to notebooks.
&lt;ul&gt;
&lt;li&gt;Of course, I could have
&lt;a href=&quot;https://docs.deno.com/runtime/manual/basics/import_maps&quot;&gt;put the dependencies in the deno.json file&lt;/a&gt;
if I wanted. That&#39;s useful for scenarios like sharing the same dependencies
between notebooks.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;TypeScript support in the editor makes it easy to understand APIs directly in
VS Code.&lt;/li&gt;
&lt;li&gt;With TypeScript, type checking errors alert me in real time about certain
mistakes.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There were a few annoyances that should be improved over time though.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I didn&#39;t like that I had to write this code:&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const document = new DOMParser().parseFromString(
  `&amp;lt;!DOCTYPE html&amp;gt;&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;&amp;lt;/html&amp;gt;`,
  &amp;quot;text/html&amp;quot;,
);
&lt;/code&gt;&lt;/pre&gt;
I&#39;m not sure what the solution is to getting rid of that, but I feel like
it&#39;s something that could be abstracted away. That said, it&#39;s not too big of
a deal.&lt;/li&gt;
&lt;li&gt;TypeScript types flowing between notebook cells don&#39;t work at the moment.
&lt;ul&gt;
&lt;li&gt;I opened
&lt;a href=&quot;https://github.com/denoland/deno/issues/21709&quot;&gt;https://github.com/denoland/deno/issues/21709&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;It was pointed out that
&lt;a href=&quot;https://github.com/denoland/vscode_deno/issues/932&quot;&gt;https://github.com/denoland/vscode_deno/issues/932&lt;/a&gt;
is the tracking issue.&lt;/li&gt;
&lt;li&gt;Looks like this depends on the &lt;code&gt;lsp-types&lt;/code&gt; crate in Rust supporting
notebook cells
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/gluon-lang/lsp-types/pull/268&quot;&gt;https://github.com/gluon-lang/lsp-types/pull/268&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;In VS Code, the &amp;quot;Save As&amp;quot; button for SVGs should show the last saved location
rather than the current folder in the &amp;quot;Save As&amp;quot; dialog, in my opinion.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Other than that, I really enjoyed the experience and I&#39;m looking forward to
JavaScript/TypeScript becoming more prevalent in Jupyter Notebooks.&lt;/p&gt;</content>
	</entry>
	<entry>
		<title>Disabling the required modifier informing System.Text.Json</title>
		<link href="https://dsherret.dev/posts/disable-required-modifier-system-text-json/"/>
		<updated>2023-04-15T21:00:00Z</updated>
		<id>https://dsherret.dev/posts/disable-required-modifier-system-text-json/</id>
		<content type="html">&lt;p&gt;Today, I looked into
&lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-11&quot;&gt;C# 11&lt;/a&gt;&#39;s
new features, which include the &lt;code&gt;required&lt;/code&gt; modifier. According to
&lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/required&quot;&gt;the docs&lt;/a&gt;,
this is what it does:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;required&lt;/code&gt; modifier indicates that the field or property it&#39;s applied to
must be initialized by an object initializer. Any expression that initializes
a new instance of the type must initialize all required members. The
&lt;code&gt;required&lt;/code&gt; modifier is available beginning with C# 11. The &lt;code&gt;required&lt;/code&gt; modifier
enables developers to create types where properties or fields must be properly
initialized, yet still allow initialization using object initializers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is great because I can now define a type like the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public record MyRecord
{
  public required string MyValue { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And when I go to initialize it I will get a compile time error when not
specifying this property in an object initializer, similar to how TypeScript
works by default:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var myValue = new MyRecord
{
  // Error - CS9035 - Required member &#39;MyRecord.MyValue&#39; must be
  // set in the object initializer or attribute constructor.
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;required-modifier-and-system.text.json&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#required-modifier-and-system.text.json&quot;&gt;&lt;code&gt;required&lt;/code&gt; modifier and &lt;code&gt;System.Text.Json&lt;/code&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here lies the problem. After upgrading some code to use &lt;code&gt;required&lt;/code&gt;, I started
getting runtime exceptions. The reason is that in .NET 7, three ways were added
to mark a property or field as required for JSON deserialization:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;There are three ways to mark a property or field as required for JSON
deserialization:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;By adding the
&lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/required&quot;&gt;required modifier&lt;/a&gt;,
which is new in C# 11.&lt;/li&gt;
&lt;li&gt;By annotating it with
&lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonrequiredattribute?view=net-7.0&quot;&gt;JsonRequiredAttribute&lt;/a&gt;,
which is new in .NET 7.&lt;/li&gt;
&lt;li&gt;By modifying the
&lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.metadata.jsonpropertyinfo.isrequired?view=net-7.0#system-text-json-serialization-metadata-jsonpropertyinfo-isrequired&quot;&gt;JsonPropertyInfo.IsRequired&lt;/a&gt;
property of the contract model, which is new in .NET 7.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/required-properties&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In my opinion, and without knowing all the details, the &lt;code&gt;required&lt;/code&gt; modifier
should not have been on that list. It all boils down to this:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Ensuring that a property appears in an object initializer and ensuring that a
property is required for JSON serialization are separate matters.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;exception-1---nullable-types-without-a-json-property&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#exception-1---nullable-types-without-a-json-property&quot;&gt;Exception 1 - Nullable types without a JSON property&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Take this example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using System.Text.Json;

var result = JsonSerializer.Deserialize&amp;lt;MyRecord&amp;gt;(&amp;quot;{}&amp;quot;);
Console.WriteLine(result?.MyValue);

public record MyRecord
{
  public string? MyValue { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here I have a nullable string property. In this example, it will deserialize to
&lt;code&gt;null&lt;/code&gt; because it doesn&#39;t appear as a property in the empty JSON object. This
code works fine.&lt;/p&gt;
&lt;p&gt;Now let&#39;s upgrade to C# 11 and take advantage of the &lt;code&gt;required&lt;/code&gt; keyword to
ensure that this property is always assigned to in an object initializer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public record MyRecord
{
  public required string? MyValue { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We&#39;ve just created a runtime exception in the above code.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;System.Text.Json.JsonException: &#39;JSON deserialization for type &#39;MyRecord&#39; was
missing required properties, including the following: MyValue&#39;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In my opinion, the &lt;code&gt;required&lt;/code&gt; modifier should have no effect on this and
deserialization should not throw an exception similar to before. This would be a
far less error-prone default for users of the API. How often do developers care
about nullable properties not appearing in the JSON? Nullable properties are
often excluded to reduce the serialized data&#39;s size.&lt;/p&gt;
&lt;p&gt;Instead, if I wanted this behaviour, I should instead have been able to opt into
it via the &lt;code&gt;JsonRequiredAttribute&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// my desired API for the above behaviour
public record MyRecord
{
  [JsonRequired]
  public required string? MyValue { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;.NET Runtime issue that was closed as by design:
&lt;a href=&quot;https://github.com/dotnet/runtime/issues/76527&quot;&gt;#76527&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;exception-2---ignoring-a-property-with-a-required-modifier-in-deserialization&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#exception-2---ignoring-a-property-with-a-required-modifier-in-deserialization&quot;&gt;Exception 2 - Ignoring a property with a &lt;code&gt;required&lt;/code&gt; modifier in deserialization&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Say we have some data that we use in our application on the server and we also
want to send it to the client, but without a property. This type is only ever
serialized and never deserialized.&lt;/p&gt;
&lt;p&gt;Instead of defining a new type, we could be a bit lazy and just mark the
property as ignored via &lt;code&gt;[JsonIgnore]&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public record MyRecord
{
  // should be sent to the client
  public string MyClientProperty { get; set; } = null!;
  // should only be accessible on the server and not sent to the client
  [JsonIgnore]
  public string MyServerProperty { get; set; } = null!;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works fine. Let&#39;s upgrade to using the &lt;code&gt;required&lt;/code&gt; modifier in C# 11 to
ensure the server always assigns to this property in object initializers:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public record MyRecord
{
  // should be sent to the client
  public required string MyClientProperty { get; set; }
  // should only be accessible on the server
  [JsonIgnore]
  public required string MyServerProperty { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We&#39;ve unfortunately just introduced a runtime exception in our code:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;System.InvalidOperationException: &#39;JsonPropertyInfo &#39;MyServerProperty&#39; defined
in type &#39;MyRecord&#39; is marked required but does not specify a setter.&#39;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This fails because &lt;code&gt;System.Text.Json&lt;/code&gt; does serialization AND deserialization
validation on the type. The &lt;code&gt;required&lt;/code&gt; modifier means this will fail
deserialization (which we won&#39;t ever do in this case). If anything, it seems
there should be a way to mark a type as serializable only, similar to what
&lt;a href=&quot;https://serde.rs/&quot;&gt;serde&lt;/a&gt; does.&lt;/p&gt;
&lt;p&gt;.NET Runtime issues that were closed as by design:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/dotnet/runtime/issues/82879&quot;&gt;#82879&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/dotnet/runtime/issues/78443&quot;&gt;#78443&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;solution&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#solution&quot;&gt;Solution&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The above two runtime exceptions were just what I ran into within a few minutes
of trying out the &lt;code&gt;required&lt;/code&gt; modifier so there might be more.&lt;/p&gt;
&lt;p&gt;To figure out how to get my desired behaviour, within &lt;code&gt;System.Text.Json&lt;/code&gt; we can
see it does the following to determine if a property is required or not
(&lt;a href=&quot;https://github.com/dotnet/runtime/blob/a642ac36ec6efb2c8efeadde54b6f8ad235ab929/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs#L383-L385&quot;&gt;Source&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;propertyInfo.IsRequired =
  memberInfo.GetCustomAttribute&amp;lt;JsonRequiredAttribute&amp;gt;(inherit: false) != null
    // shouldCheckForRequiredKeyword is based on the context of where the property appears
    || (shouldCheckForRequiredKeyword &amp;amp;&amp;amp; memberInfo.HasRequiredMemberAttribute());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Luckily, they have provided a way to override this and a hint is given on the
&lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/required-properties&quot;&gt;Required properties page&lt;/a&gt;
I linked to earlier. Essentially, we need to create a custom &lt;code&gt;TypeInfoResolver&lt;/code&gt;
that builds upon the functionality of the &lt;code&gt;DefaultJsonTypeInfoResolver&lt;/code&gt; to only
set a property as required when it has the &lt;code&gt;JsonRequiredAttribute&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;

var result = JsonSerializer.Deserialize&amp;lt;MyRecord&amp;gt;(
  &amp;quot;{}&amp;quot;,
  new JsonSerializerOptions
  {
    TypeInfoResolver = new DefaultJsonTypeInfoResolver
    {
      Modifiers =
      {
        static typeInfo =&amp;gt;
        {
          foreach (var info in typeInfo.Properties)
          {
            if (info.IsRequired)
            {
              info.IsRequired = info.AttributeProvider?.IsDefined(
                typeof(JsonRequiredAttribute),
                inherit: false
              ) ?? false;
            }
          }
        }
      }
    },
  }
);

Console.WriteLine(result?.MyValue);

public record MyRecord
{
    // [JsonRequired] // uncomment the attribute to see this take effect
    public required string? MyValue { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In ASP.NET Core, you can set this globally when configuring your JSON options:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;builder.Services.AddControllers().AddJsonOptions(options =&amp;gt;
{
    options.JsonSerializerOptions.TypeInfoResolver =
      new DefaultJsonTypeInfoResolver
      {
          // same code as above goes here
      };
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, you can upgrade to liberally using the &lt;code&gt;required&lt;/code&gt; modifier without worrying
about introducing probably needless &lt;code&gt;System.Text.Json&lt;/code&gt; runtime exceptions.&lt;/p&gt;
&lt;p&gt;Hope it helps!&lt;/p&gt;</content>
	</entry>
	<entry>
		<title>Updatable text in a console in Rust</title>
		<link href="https://dsherret.dev/posts/console-static-text/"/>
		<updated>2023-01-30T14:00:00Z</updated>
		<id>https://dsherret.dev/posts/console-static-text/</id>
		<content type="html">&lt;p&gt;Showing progress bars/messages and getting user input is a common task that many
CLI applications have to do. This post will outline a Rust crate called
&lt;a href=&quot;https://github.com/dsherret/console_static_text&quot;&gt;console_static_text&lt;/a&gt;, which
logs text that can be updated at the bottom of a console window.&lt;/p&gt;
&lt;p&gt;&lt;video alt=&quot;Video showing a bunch of progress bars outputting while the user is making selections.&quot; autoplay muted loop playsinline src=&quot;https://dsherret.dev/example_output.mp4&quot;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;h2 id=&quot;api&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#api&quot;&gt;API&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The API behind this is low level and basic. Essentially there is only one
function, which is to render some text that can be sent to the console that
overwrites the previous state and updates the state for the new text. The rest
of the functionality is helper methods built on top of that.&lt;/p&gt;
&lt;p&gt;Here&#39;s an example that logs the numbers 0 to 199 to the console, then clears the
text:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;# cargo.toml dependency
console_static_text = { version = &amp;quot;0.7.0&amp;quot;, features = [&amp;quot;sized&amp;quot;] }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;use console_static_text::ConsoleStaticText;
use std::time::Duration;

fn main() {
  // returns `None` when not a tty
  let mut static_text = ConsoleStaticText::new_sized().unwrap();

  for i in 0..200 {
    static_text.eprint(&amp;amp;i.to_string());
    std::thread::sleep(Duration::from_millis(30));
  }

  static_text.eprint_clear();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Outputs as:&lt;/p&gt;
&lt;p&gt;&lt;video alt=&quot;Video showing the output of the code above.&quot; autoplay muted controls loop playsinline src=&quot;https://dsherret.dev/example_count.mp4&quot;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;Note that internally &lt;code&gt;eprint&lt;/code&gt; and &lt;code&gt;eprint_clear&lt;/code&gt; are helper methods around the
&lt;code&gt;render(new_text: &amp;amp;str)&lt;/code&gt; method. For example, this is the implementation of
&lt;code&gt;static_text.eprint(...)&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;pub fn eprint(&amp;amp;mut self, new_text: &amp;amp;str) {
  // `render` returns `None` when there&#39;s nothing to update
  if let Some(text) = self.render(new_text) {
    std::io::stderr().write_all(text.as_bytes()).unwrap();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;example---automatic-word-wrapping&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#example---automatic-word-wrapping&quot;&gt;Example - Automatic word wrapping&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;console_static_text will automatically word wrap text and only update the
console if there are changes.&lt;/p&gt;
&lt;p&gt;The following example shows word wrapping and how the crate handles the console
window resizing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;use console_static_text::ConsoleStaticText;
use std::time::Duration;

fn main() {
  let mut static_text = ConsoleStaticText::new_sized().unwrap();

  let text = format!(
    &amp;quot;{}\nPress ctrl+c to exit...&amp;quot;,
    &amp;quot;some words repeated &amp;quot;.repeat(40).trim(),
  );
  let mut last_size = None;

  loop {
    let mut delay_ms = 60;
    let current_size = static_text.console_size();

    if last_size.is_some() &amp;amp;&amp;amp; last_size.unwrap() != current_size {
      // debounce while the user is resizing
      delay_ms = 200;
    } else {
      // this will not update the console when the size hasn&#39;t
      // changed since the output should be the same
      static_text.eprint_with_size(&amp;amp;text, current_size);
    }

    std::thread::sleep(Duration::from_millis(delay_ms));
    last_size = Some(current_size);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&#39;m not aware of a good cross platform way to handle console resize events, so
this example renders every little while and only if necessary (handled
internally in the static text object).&lt;/p&gt;
&lt;p&gt;&lt;video alt=&quot;Video showing the output of the code above with the console being resized.&quot; autoplay muted controls loop playsinline src=&quot;https://dsherret.dev/example_word_wrapping.mp4&quot;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;h2 id=&quot;resizing---not-perfect&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#resizing---not-perfect&quot;&gt;Resizing - Not perfect&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If the console has resized since the last render, console_static_text does its
best to redraw over the previous text, but often some text from a previous
render will be left over. From my knowledge, I don&#39;t believe it&#39;s practical to
make this perfect... for example, some terminals might add or remove line breaks
when resizing or move the current cursor position to a hard to predict spot.&lt;/p&gt;
&lt;p&gt;Here&#39;s an example showing how lines can be added or removed on Windows in a
console. Both commands executed are the same, but have different outputs based
on the size of the console when the command was executed:&lt;/p&gt;
&lt;p&gt;&lt;video alt=&quot;Video showing windows terminal resizing some text.&quot; autoplay muted controls loop playsinline src=&quot;https://dsherret.dev/windows_terminal_resizing.mp4&quot;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;I think handling all these scenarios would be a lot of effort and most users
don&#39;t expect things to be perfect when resizing the console and the render area
isn&#39;t full screen anyway.&lt;/p&gt;
&lt;h2 id=&quot;example---logging-above-while-outputting&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#example---logging-above-while-outputting&quot;&gt;Example - Logging above while outputting&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The key to logging above while the static text is outputting is to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Clear the existing static text.&lt;/li&gt;
&lt;li&gt;Log your new text.&lt;/li&gt;
&lt;li&gt;Redraw the static text.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;use console_static_text::ConsoleStaticText;
use std::io::Write;
use std::time::Duration;

fn main() {
  let mut static_text = ConsoleStaticText::new_sized().unwrap();

  for i in 0..200 {
    let i_str = i.to_string();
    if i % 10 == 0 {
      // only get the console size once and use
      // the same console size state on all calls
      let size = static_text.console_size();
      let mut new_text = String::new();

      // first clear the existing static text
      if let Some(text) = static_text.render_clear_with_size(size) {
        new_text.push_str(&amp;amp;text);
      }

      // log the new text
      new_text.push_str(&amp;amp;format!(&amp;quot;Hello from {}\n&amp;quot;, i));

      // then redraw the static text
      if let Some(text) = static_text.render_with_size(&amp;amp;i_str, size) {
        new_text.push_str(&amp;amp;text);
      }

      // now output everything to stderr in one go
      std::io::stderr().write_all(new_text.as_bytes()).unwrap();
    } else {
      static_text.eprint(&amp;amp;i_str);
    }

    std::thread::sleep(Duration::from_millis(30));
  }

  static_text.eprint_clear();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Outputs as:&lt;/p&gt;
&lt;p&gt;&lt;video alt=&quot;Video showing the example code above.&quot; autoplay muted controls loop playsinline src=&quot;https://dsherret.dev/example_logging_text_above.mp4&quot;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;h2 id=&quot;example---user-input&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#example---user-input&quot;&gt;Example - User input&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here&#39;s an example that asks the user to make a selection.&lt;/p&gt;
&lt;p&gt;&lt;video alt=&quot;Video showing the example code below.&quot; autoplay muted controls loop playsinline src=&quot;https://dsherret.dev/example_input.mp4&quot;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;It uses &lt;a href=&quot;https://crates.io/crates/crossterm&quot;&gt;Crossterm&lt;/a&gt; to get the console size,
turn on/off raw mode (necessary for getting arrow key presses), hide/show the
cursor, and get key presses. We need to use Crossterm or something like it for
this because console_static_text is only concerned with displaying text.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;use std::io::stderr;

use console_static_text::ConsoleSize;
use console_static_text::ConsoleStaticText;
use console_static_text::TextItem;
use crossterm::event;
use crossterm::event::Event;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::execute;

struct DrawState {
  active_index: usize,
  message: String,
  items: Vec&amp;lt;String&amp;gt;,
}

pub fn main() {
  assert!(crossterm::tty::IsTty::is_tty(&amp;amp;std::io::stderr()));
  let mut static_text = ConsoleStaticText::new(|| {
    // since we&#39;re already using crossterm, get the size from
    // it and don&#39;t bother with console_static_text&#39;s &amp;quot;sized&amp;quot;
    // feature in order to reduce our dependencies
    let (cols, rows) = crossterm::terminal::size().unwrap();
    ConsoleSize {
      rows: Some(rows),
      cols: Some(cols),
    }
  });
  let mut state = DrawState {
    active_index: 0,
    message: &amp;quot;Which option would you like to select?&amp;quot;.to_string(),
    items: vec![
      &amp;quot;Option 1&amp;quot;.to_string(),
      &amp;quot;Option 2&amp;quot;.to_string(),
      &amp;quot;Option 3 with long text. &amp;quot;.repeat(10),
      &amp;quot;Option 4&amp;quot;.to_string(),
    ],
  };

  // enable raw mode to get special key presses
  crossterm::terminal::enable_raw_mode().unwrap();
  // hide the cursor
  execute!(stderr(), crossterm::cursor::Hide).unwrap();

  // render, then act on up and down arrow key presses
  loop {
    let items = render(&amp;amp;state);
    static_text.eprint_items(items.iter());

    if let Event::Key(event) = event::read().unwrap() {
      // in a real implementation you will want to handle ctrl+c here
      // (make sure to handle always turning off raw mode)
      match event {
        KeyEvent {
          code: KeyCode::Up, ..
        } =&amp;gt; {
          if state.active_index == 0 {
            state.active_index = state.items.len() - 1;
          } else {
            state.active_index -= 1;
          }
        }
        KeyEvent {
          code: KeyCode::Down,
          ..
        } =&amp;gt; {
          state.active_index =
            (state.active_index + 1) % state.items.len();
        }
        KeyEvent {
          code: KeyCode::Enter,
          ..
        } =&amp;gt; {
          break;
        }
        _ =&amp;gt; {
          // ignore
        }
      }
    };
  }

  // disable raw mode, show the cursor, clear the static text, then
  // display what the user selected
  crossterm::terminal::disable_raw_mode().unwrap();
  execute!(stderr(), crossterm::cursor::Show).unwrap();
  static_text.eprint_clear();
  eprintln!(&amp;quot;Selected: {}&amp;quot;, state.items[state.active_index]);
}

/// Renders the draw state
fn render(state: &amp;amp;DrawState) -&amp;gt; Vec&amp;lt;TextItem&amp;gt; {
  let mut items = Vec::new();

  // display the question message
  items.push(TextItem::new(&amp;amp;state.message));

  // now render each item, showing a `&amp;gt;` beside the active index
  for (i, item_text) in state.items.iter().enumerate() {
    let selection_char = if i == state.active_index { &#39;&amp;gt;&#39; } else { &#39; &#39; };
    let text = format!(&amp;quot;{} {}&amp;quot;, selection_char, item_text);
    items.push(TextItem::HangingText {
      text: std::borrow::Cow::Owned(text),
      indent: 4,
    });
  }

  items
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;recommended-architecture&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#recommended-architecture&quot;&gt;Recommended architecture&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The stdout and stderr pipes are a global concept in a CLI application. For that
reason, I recommend the implementation of your output to the global stdout and
stderr pipes should be controlled in one place and have that code use a single
instance of a &lt;code&gt;ConsoleStaticText&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For example, either have a logging implementation using a single
&lt;code&gt;ConsoleStaticText&lt;/code&gt; that&#39;s passed around to do all your logging (as is done in
dprint) or create a single global instance of &lt;code&gt;ConsoleStaticText&lt;/code&gt; that&#39;s used in
the application (as is done in dax). Then create an abstraction on top of that
to handle rendering your application&#39;s state, getting user input, and handle
logging at the same time so nothing conflicts.&lt;/p&gt;
&lt;h2 id=&quot;projects-using-this&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#projects-using-this&quot;&gt;Projects using this&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The following projects I work on are using this now:
&lt;a href=&quot;https://github.com/denoland/deno&quot;&gt;Deno&lt;/a&gt; (in 1.29 and above),
&lt;a href=&quot;https://github.com/dprint/dprint&quot;&gt;dprint&lt;/a&gt;, and
&lt;a href=&quot;https://github.com/dsherret/dax&quot;&gt;dax&lt;/a&gt; (via Wasm)&lt;/p&gt;
&lt;p&gt;All the examples in this blog post are found in the console_static_text repo:
&lt;a href=&quot;https://github.com/dsherret/console_static_text&quot;&gt;https://github.com/dsherret/console_static_text&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;As always, thanks for reading!&lt;/p&gt;</content>
	</entry>
	<entry>
		<title>dax - Cross-platform shell tools for Deno</title>
		<link href="https://dsherret.dev/posts/dax/"/>
		<updated>2022-07-18T14:00:00Z</updated>
		<id>https://dsherret.dev/posts/dax/</id>
		<content type="html">&lt;p&gt;Automation scripts in repositories are often written in a shell scripting
language such as bash. That&#39;s not ideal—on top of these scripting languages
being difficult to use, they&#39;re not cross-platform. This makes it harder for
Windows users to contribute and there are often small differences between Linux
and Mac (or shell configurations) that may lead to broken scripts for
contributors.&lt;/p&gt;
&lt;p&gt;Using a cross-platform programming language, like JavaScript, is more ideal, but
often what can be expressed in a shell scripting language succinctly (such as
executing a command) is verbose when using the APIs offered out of the box by
JavaScript runtimes.&lt;/p&gt;
&lt;p&gt;The library &lt;a href=&quot;https://github.com/google/zx&quot;&gt;zx&lt;/a&gt; made this a lot easier by
bringing the best of shell scripting languages into JavaScript with the
introduction of an easy to use API, but I believe there are some improvements
that can be made to this idea.&lt;/p&gt;
&lt;p&gt;In this post I&#39;m going to outline a new tool called
&lt;a href=&quot;https://github.com/dsherret/dax&quot;&gt;dax&lt;/a&gt;, which is inspired by zx.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// example dax API usage
let branch = await $`git branch --show-current`.text();
await $`dep deploy --branch=${branch}`;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;cross-platform-shell&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#cross-platform-shell&quot;&gt;Cross-platform shell&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;dax has an API very similar to zx, but its shell is cross-platform using the
parser from &lt;a href=&quot;https://github.com/denoland/deno_task_shell&quot;&gt;deno_task_shell&lt;/a&gt; with
a rewrite of the execution logic in JavaScript.&lt;/p&gt;
&lt;p&gt;So commands like the following work the same on Linux, Mac, and Windows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;await $`echo Hello`; // Hello
await $`MY_VAR=there &amp;amp;&amp;amp; echo Hello $MY_VAR`; // Hello there
await $`LOG_LEVEL=0 some_command`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Additionally, the shell has a few built-in cross-platform commands.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// outputs &amp;quot;Hello&amp;quot;, sleeps for 1 second, then outputs &amp;quot;There&amp;quot;
await $`echo Hello &amp;amp;&amp;amp; sleep 1 &amp;amp;&amp;amp; echo There`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note: It&#39;s not possible to use a custom shell with dax as that&#39;s heavily
discouraged since it more easily leads to code that&#39;s not cross-platform. That
said, if you really need to call into &lt;code&gt;sh&lt;/code&gt;, for example, then you can run it
directly (&lt;code&gt;sh -c &amp;lt;command&amp;gt;&lt;/code&gt;).&lt;/p&gt;
&lt;h3 id=&quot;exporting-shell-environment&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#exporting-shell-environment&quot;&gt;Exporting shell environment&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Any changes to the shell environment will not be exported to the executing
process by default.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// outputs: C:\dev\my_project\sub_dir
await $`cd sub_dir &amp;amp;&amp;amp; echo $PWD &amp;amp;&amp;amp; export MY_VAR=5`;
// outputs: undefined
console.log(Deno.env.get(&amp;quot;MY_VAR&amp;quot;));
// outputs: C:\dev\my_project
console.log(Deno.cwd());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, the shell environment may be exported when desired by using the
&lt;code&gt;.exportEnv()&lt;/code&gt; method:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;await $`cd sub_dir`.exportEnv();
await $`echo $PWD`; // C:\dev\my_project\sub_dir
console.log(Deno.cwd()); // C:\dev\my_project\sub_dir

await $`export MY_VAR=5 &amp;amp;&amp;amp; cd ../`.exportEnv();
console.log(Deno.env.get(&amp;quot;MY_VAR&amp;quot;)); // 5
console.log(Deno.cwd()); // C:\dev\my_project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note also, that you can modify a shell&#39;s environment before executing without
changing the current process&#39; environment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// outputs:
// C:\dev\my_project\sub_dir
// 5
await $`echo $PWD &amp;amp;&amp;amp; echo $MY_VAR`
  .cwd(&amp;quot;./sub_dir&amp;quot;)
  .env(&amp;quot;MY_VAR&amp;quot;, &amp;quot;5&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;high-level-helpers-with-an-available-low-level-api&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#high-level-helpers-with-an-available-low-level-api&quot;&gt;High level helpers with an available low level API&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;When executing commands and you want to get the output, there are several helper
methods that make this easy:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// get the stdout of a command (makes stdout &amp;quot;quiet&amp;quot;)
const result = await $`echo 1`.text();
console.log(result); // 1

// get the result of stdout as json (makes stdout &amp;quot;quiet&amp;quot;)
const result = await $`echo &#39;{ &amp;quot;prop&amp;quot;: 5 }&#39;`.json();
console.log(result.prop); // 5

// get the result of stdout as bytes (makes stdout &amp;quot;quiet&amp;quot;)
const result = await $`echo &#39;test&#39;`.bytes();
console.log(result); // Uint8Array(5) [ 116, 101, 115, 116, 10 ]

// get the result of stdout as a list of lines (makes stdout &amp;quot;quiet&amp;quot;)
const result = await $`echo 1 &amp;amp;&amp;amp; echo 2`.lines();
console.log(result); // [&amp;quot;1&amp;quot;, &amp;quot;2&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the case you need to access to more detail, that is available too along with
several other properties not shown here:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const result = await $`deno eval &#39;console.log(1); console.error(2);&#39;`;
console.log(result.code); // 0
console.log(result.stdoutBytes); // Uint8Array(2) [ 49, 10 ]
console.log(result.stdout); // 1\n
console.log(result.stderr); // 5\n
const output = await $`echo &#39;{ &amp;quot;test&amp;quot;: 5 }&#39;`;
console.log(output.stdoutJson);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;no-custom-cli-or-globals&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#no-custom-cli-or-globals&quot;&gt;No custom CLI or globals&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Deno allows for expressing dependencies in code and this is perfect for
automation scripts. Instead of needing to install a custom CLI or npm package,
you can just import the module directly...&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// script.js
import $ from &amp;quot;https://deno.land/x/dax@0.7.0/mod.ts&amp;quot;;

await $`echo &#39;Hello there!&#39;`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;...and run it right away...&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shellsession&quot;&gt;&amp;gt; deno run -A script.js
Hello there!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command could then be easily aliased in a &lt;code&gt;deno task&lt;/code&gt; with Deno&#39;s
&lt;a href=&quot;https://deno.land/manual/tools/task_runner&quot;&gt;task runner&lt;/a&gt; meaning your
contributors only need to execute the task to run the script without needing to
follow any other setup instructions.&lt;/p&gt;
&lt;p&gt;Being able to express all your dependencies directly in your script files is a
huge advantage that Deno offers over Node.js. It makes it easy to import a
module you want to use in a single script without having to manage dev
dependencies and do an &lt;code&gt;npm install&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;no-global-configuration&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#no-global-configuration&quot;&gt;No global configuration&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Using zx in a library or application code is a little risky because it has
global configuration. With dax there is no global configuration in order to
prevent modifying the behaviour of other code using it.&lt;/p&gt;
&lt;p&gt;Additionally, with dax you can create your own local &lt;code&gt;$&lt;/code&gt; object to use that has
the configuration you like. This is done by using the builder APIs and &lt;code&gt;build$&lt;/code&gt;
function.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { build$, CommandBuilder } from &amp;quot;https://deno.land/x/dax@0.7.0/mod.ts&amp;quot;;

const commandBuilder = new CommandBuilder()
  .exportEnv()
  .noThrow();

const $ = build$({ commandBuilder });

// since exportEnv() was set, this will now actually change
// the directory of the executing process
await $`cd test &amp;amp;&amp;amp; export MY_VALUE=5`;
// will output &amp;quot;5&amp;quot;
await $`echo $MY_VALUE`;
// will output it&#39;s in the test dir
await $`echo $PWD`;
// won&#39;t throw even though this command fails (because of `.noThrow()`)
await $`deno eval &#39;Deno.exit(1);&#39;`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This &lt;code&gt;CommandBuilder&lt;/code&gt; API is what &lt;code&gt;$&lt;/code&gt; uses internally, so you can also design
your own APIs that execute shell commands using it.&lt;/p&gt;
&lt;p&gt;Read more about the builder APIs
&lt;a href=&quot;https://github.com/dsherret/dax#builder-apis&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;utilities-on-%24&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#utilities-on-%24&quot;&gt;Utilities on &lt;code&gt;$&lt;/code&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Since this is a shell scripting toolkit, the module offers several utilities
built-in and has all of these available on the &lt;code&gt;$&lt;/code&gt; object for quick access.&lt;/p&gt;
&lt;p&gt;Here&#39;s a few of them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;await $.sleep(100); // ms
await $.sleep(&amp;quot;1.5s&amp;quot;);

const denoPath = await $.which(&amp;quot;deno&amp;quot;); // path to deno executable

// re-export of deno_std&#39;s path
const fileName = $.path.basename(&amp;quot;./my_sub_dir/mod.ts&amp;quot;); // mod.ts

// re-export of deno_std&#39;s fs
for await (const file of $.fs.expandGlob(&amp;quot;**/*.ts&amp;quot;)) {
  console.log(file);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;fetch-alternative&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#fetch-alternative&quot;&gt;Fetch alternative&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;There is a &lt;code&gt;fetch&lt;/code&gt; API alternative built-in, but with a less error-prone builder
API that throws on non-2xx status codes by default.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// download a file as JSON
const data = await $.request(&amp;quot;https://plugins.dprint.dev/info.json&amp;quot;).json();
// or long form
const response = await $.request(&amp;quot;https://plugins.dprint.dev/info.json&amp;quot;);
console.log(response.code);
console.log(await response.json());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Often enough myself and others would forget to handle non-success codes in
scripts with &lt;code&gt;fetch&lt;/code&gt;, leading to cryptic errors. So the &lt;code&gt;$.request(..)&lt;/code&gt; function
throws by default. That said, this can be disabled, or only disabled for
specific status codes, via the &lt;code&gt;.noThrow()&lt;/code&gt; method (ex.
&lt;code&gt;await $.request(&amp;quot;...&amp;quot;).noThrow()&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Note: Similarly to commands, if you don&#39;t like the defaults for &lt;code&gt;$.request&lt;/code&gt;, you
can use &lt;code&gt;build$&lt;/code&gt; to create your own &lt;code&gt;$&lt;/code&gt; with a &lt;code&gt;RequestBuilder&lt;/code&gt; that changes the
defaults (see &lt;a href=&quot;https://github.com/dsherret/dax#custom-&quot;&gt;custom &lt;code&gt;$&lt;/code&gt;&lt;/a&gt;).&lt;/p&gt;
&lt;h3 id=&quot;logging-api&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#logging-api&quot;&gt;Logging API&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In an effort to simplify logging in scripts (reducing need for a &lt;code&gt;color&lt;/code&gt; API),
there is also a built-in logging API that only logs over stderr:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// logs with no formatting
$.log(&amp;quot;Hello!&amp;quot;);
// log with the first word as bold green
$.logStep(&amp;quot;Fetching data from server...&amp;quot;);
// or force multiple words to be green by using two arguments
$.logStep(&amp;quot;Setting up&amp;quot;, &amp;quot;local directory...&amp;quot;);
// similar to $.logStep, but with red
$.logError(&amp;quot;Error Some error message.&amp;quot;);
// similar to $.logStep, but with yellow
$.logWarn(&amp;quot;Warning Some warning message.&amp;quot;);
// logs out text in gray
$.logLight(&amp;quot;Some unimportant message.&amp;quot;);

// log indented within
await $.logIndent(async () =&amp;gt; {
  $.log(&amp;quot;This will be indented.&amp;quot;);
  await $.logIndent(async () =&amp;gt; {
    $.log(&amp;quot;This will indented even more.&amp;quot;);
    // do maybe async stuff here
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dsherret.dev/log_example.png&quot; alt=&quot;Shows the terminal output of the code as described in the code block directly above.&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I just started on this a little over a week ago so it might evolve a bit and
have some breaking changes. Overall, I&#39;d appreciate any feedback. I&#39;ve already
begun integrating this into some of our automation scripts used at
&lt;a href=&quot;https://github.com/denoland&quot;&gt;Deno&lt;/a&gt; to try to find some of the pain points and
verbose code that could be simplified.&lt;/p&gt;
&lt;p&gt;As always, thanks for reading and I hope this is useful!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href=&quot;https://github.com/dsherret/dax&quot;&gt;https://github.com/dsherret/dax&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;P.S. this module was originally called &lt;code&gt;ax&lt;/code&gt;, but deno.land/x doesn&#39;t allow 2
character module names... so I added a &lt;code&gt;d&lt;/code&gt; to the front for Deno, but that so
happens to be my cat&#39;s name and so this module is now named after him.&lt;/p&gt;
&lt;p style=&quot;text-align: center&quot;&gt;
  &lt;img src=&quot;https://dsherret.dev/cat.jpg&quot; alt=&quot;Picture of my cat lying down half behind a curtain with some ethernet cables.&quot; /&gt;
&lt;/p&gt;</content>
	</entry>
</feed>
