Skip to content

Commit

Permalink
Update tiny-decoders (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
lydell authored Oct 30, 2023
1 parent 8283362 commit b14ce7d
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 70 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
136 changes: 79 additions & 57 deletions run-pty.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -1077,77 +1077,99 @@ const partitionArgs = (args) => {
* @returns {Array<CommandDescription>}
*/
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<typeof statusCodec>]>} */
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<CommandDescription>}
* @type {Codec.Codec<CommandDescription>}
*/
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<T>} decoder
* @returns {Decode.Decoder<Array<T>>}
* @template Decoded
* @param {Codec.Codec<Decoded>} decoder
* @returns {Codec.Codec<Array<Decoded>>}
*/
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}
Expand Down
27 changes: 19 additions & 8 deletions test/run-pty.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
`);
Expand Down Expand Up @@ -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"
}
`);
});

Expand All @@ -1246,15 +1250,23 @@ describe("parse json", () => {
Failed to read command descriptions file as JSON:
At root[0]["status"]["{}"]:
Invalid regular expression: /{}/u: Lone quantifier brackets
Got: "{}"
`);
});

test("key typo", () => {
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"
`);
});

Expand All @@ -1266,7 +1278,6 @@ describe("parse json", () => {
command: ["node"],
title: "node",
cwd: ".",
defaultStatus: undefined,
status: [],
killAllSequence: "\x03\x03",
},
Expand Down

0 comments on commit b14ce7d

Please sign in to comment.