From b14ce7d8df81ee5fc1a2050d8ab2a5d62ca08489 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Mon, 30 Oct 2023 22:39:56 +0100 Subject: [PATCH] Update tiny-decoders (#49) --- package-lock.json | 8 +-- package.json | 2 +- run-pty.js | 136 +++++++++++++++++++++++++------------------ test/run-pty.test.js | 27 ++++++--- 4 files changed, 103 insertions(+), 70 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2d3d890..3d90df7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "node-pty": "^1.0.0", - "tiny-decoders": "^7.0.1" + "tiny-decoders": "^23.0.0" }, "bin": { "run-pty": "run-pty.js" @@ -2341,9 +2341,9 @@ "dev": true }, "node_modules/tiny-decoders": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/tiny-decoders/-/tiny-decoders-7.0.1.tgz", - "integrity": "sha512-P1LaHTLASl/lCrdtwgAAVwxt4bEAPmxpf9HMQrlCkAseaT8oH8oxm8ndy4nx5rLTcL5U/Qxp1a+FDoQfS/ZgQQ==" + "version": "23.0.0", + "resolved": "https://registry.npmjs.org/tiny-decoders/-/tiny-decoders-23.0.0.tgz", + "integrity": "sha512-gQB+za3EW9JN1ASDmrVl1a1I6Q/jzg4M75YLI9ToHCdCW4/9KvBTTNH4u3kFbg8e+/8Zn68VRF+ayuiPNqYAmg==" }, "node_modules/tinybench": { "version": "2.5.0", diff --git a/package.json b/package.json index a4dfa44..b5f62c7 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ ], "dependencies": { "node-pty": "^1.0.0", - "tiny-decoders": "^7.0.1" + "tiny-decoders": "^23.0.0" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "6.7.2", diff --git a/run-pty.js b/run-pty.js index 18bacea..e64d3be 100755 --- a/run-pty.js +++ b/run-pty.js @@ -6,7 +6,7 @@ const fs = require("fs"); const path = require("path"); const os = require("os"); const pty = require("node-pty"); -const Decode = require("tiny-decoders"); +const Codec = require("tiny-decoders"); /** * @typedef { @@ -1077,77 +1077,99 @@ const partitionArgs = (args) => { * @returns {Array} */ const parseInputFile = (string) => { - try { - return Decode.array(commandDescriptionDecoder)(JSON.parse(string)); - } catch (error) { - throw error instanceof Decode.DecoderError - ? new Error(error.format()) - : error; + const result = Codec.JSON.parse(Codec.array(commandDescriptionCodec), string); + switch (result.tag) { + case "Valid": + return result.value; + case "DecoderError": + throw new Error(Codec.format(result.error)); } }; +const statusCodec = Codec.map( + Codec.nullOr(Codec.tuple([Codec.string, Codec.string])), + { + decoder: (value) => value ?? undefined, + encoder: (value) => value ?? null, + }, +); + +const statusesCodec = Codec.flatMap(Codec.record(statusCodec), { + decoder: (record) => { + /** @type {Array<[RegExp, Codec.Infer]>} */ + const result = []; + for (const [key, value] of Object.entries(record)) { + try { + result.push([RegExp(key, "u"), value]); + } catch (error) { + return { + tag: "DecoderError", + error: { + tag: "custom", + message: error instanceof Error ? error.message : String(error), + got: key, + path: [key], + }, + }; + } + } + return { tag: "Valid", value: result }; + }, + encoder: (items) => + Object.fromEntries(items.map(([key, value]) => [key.source, value])), +}); + /** - * @type {Decode.Decoder} + * @type {Codec.Codec} */ -const commandDescriptionDecoder = Decode.fields( - /** @returns {CommandDescription} */ - (field) => { - const command = field("command", nonEmptyArray(Decode.string)); - - return { - title: field( - "title", - Decode.optional(Decode.string, commandToPresentationName(command)), - ), - cwd: field("cwd", Decode.optional(Decode.string, ".")), +const commandDescriptionCodec = Codec.map( + Codec.fields( + { + command: nonEmptyArray(Codec.string), + title: Codec.field(Codec.string, { optional: true }), + cwd: Codec.field(Codec.string, { optional: true }), + status: Codec.field(statusesCodec, { optional: true }), + defaultStatus: Codec.field(statusCodec, { optional: true }), + killAllSequence: Codec.field(Codec.string, { optional: true }), + }, + { allowExtraFields: false }, + ), + { + decoder: ({ command, - status: field( - "status", - Decode.optional( - Decode.chain(Decode.record(statusDecoder), (record) => - Object.entries(record).map(([key, value]) => { - try { - return [RegExp(key, "u"), value]; - } catch (error) { - throw Decode.DecoderError.at(error, key); - } - }), - ), - [], - ), - ), - defaultStatus: field("defaultStatus", Decode.optional(statusDecoder)), - killAllSequence: field( - "killAllSequence", - Decode.optional(Decode.string, KEY_CODES.kill), - ), - }; + title = commandToPresentationName(command), + cwd = ".", + status = [], + killAllSequence = KEY_CODES.kill, + ...rest + }) => ({ ...rest, command, title, cwd, status, killAllSequence }), + encoder: (value) => value, }, - { exact: "throw" }, ); /** - * @template T - * @param {Decode.Decoder} decoder - * @returns {Decode.Decoder>} + * @template Decoded + * @param {Codec.Codec} decoder + * @returns {Codec.Codec>} */ function nonEmptyArray(decoder) { - return Decode.chain(Decode.array(decoder), (arr) => { - if (arr.length === 0) { - throw new Decode.DecoderError({ - message: "Expected a non-empty array", - value: arr, - }); - } - return arr; + return Codec.flatMap(Codec.array(decoder), { + decoder: (arr) => + arr.length === 0 + ? { + tag: "DecoderError", + error: { + tag: "custom", + message: "Expected a non-empty array", + got: arr, + path: [], + }, + } + : { tag: "Valid", value: arr }, + encoder: (value) => value, }); } -const statusDecoder = Decode.nullable( - Decode.tuple([Decode.string, Decode.string]), - undefined, -); - /** * @param {Command} command * @returns {string} diff --git a/test/run-pty.test.js b/test/run-pty.test.js index ca2bd0d..fb6b3c7 100644 --- a/test/run-pty.test.js +++ b/test/run-pty.test.js @@ -1188,14 +1188,16 @@ describe("parse json", () => { test("empty file", () => { expect(testJsonError("empty.json")).toMatchInlineSnapshot(` Failed to read command descriptions file as JSON: - Unexpected end of JSON input + At root: + SyntaxError: Unexpected end of JSON input `); }); test("invalid json syntax", () => { expect(testJsonError("invalid-json-syntax.json")).toMatchInlineSnapshot(` Failed to read command descriptions file as JSON: - Unexpected token ']', ..."kend"] }, + At root: + SyntaxError: Unexpected token ']', ..."kend"] }, ] " is not valid JSON `); @@ -1226,9 +1228,11 @@ describe("parse json", () => { test("missing command", () => { expect(testJsonError("missing-command.json")).toMatchInlineSnapshot(` Failed to read command descriptions file as JSON: - At root[0]["command"]: - Expected an array - Got: undefined + At root[0]: + Expected an object with a field called: "command" + Got: { + "title": "Something" + } `); }); @@ -1246,6 +1250,7 @@ describe("parse json", () => { Failed to read command descriptions file as JSON: At root[0]["status"]["{}"]: Invalid regular expression: /{}/u: Lone quantifier brackets + Got: "{}" `); }); @@ -1253,8 +1258,15 @@ describe("parse json", () => { expect(testJsonError("key-typo.json")).toMatchInlineSnapshot(` Failed to read command descriptions file as JSON: At root[0]: - Expected only these fields: "command", "title", "cwd", "status", "defaultStatus", "killAllSequence" - Found extra fields: "titel" + Expected only these fields: + "command", + "title", + "cwd", + "status", + "defaultStatus", + "killAllSequence" + Found extra fields: + "titel" `); }); @@ -1266,7 +1278,6 @@ describe("parse json", () => { command: ["node"], title: "node", cwd: ".", - defaultStatus: undefined, status: [], killAllSequence: "\x03\x03", },