From 487c863d1bba529d091cc9b1194946d2b8fe60e9 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Sat, 23 Sep 2023 18:08:35 +0100 Subject: [PATCH 1/4] Delete .github/ubiquibot-config.yml --- .github/ubiquibot-config.yml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .github/ubiquibot-config.yml diff --git a/.github/ubiquibot-config.yml b/.github/ubiquibot-config.yml deleted file mode 100644 index 3658a5818..000000000 --- a/.github/ubiquibot-config.yml +++ /dev/null @@ -1,6 +0,0 @@ -priceMultiplier: 1.5 -# newContributorGreeting: -# enabled: true -# header: "Thank you for contributing to UbiquiBot! Please be sure to set your wallet address before completing your first bounty so that the automatic payout upon task completion will work for you." -# helpMenu: true -# footer: "###### Also please star this repository and [@ubiquity/devpool-directory](https://github.com/ubiquity/devpool-directory/) to show your support. It helps a lot!" \ No newline at end of file From ede57a1fc5faf0f3e259837bc2512c6ec74fbdfe Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Sun, 15 Oct 2023 13:45:01 +0100 Subject: [PATCH 2/4] feat: f-strings python inspired string formatting --- package.json | 3 +- src/configs/strings.ts | 84 +++++++++++++++++++++++++++++++++ src/tests/stringsTest.test.ts | 87 +++++++++++++++++++++++++++++++++++ 3 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/tests/stringsTest.test.ts diff --git a/package.json b/package.json index 807d2253b..0487ec479 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "start:watch": "nodemon --exec 'yarn start'", "utils:cspell": "cspell --config .cspell.json 'src/**/*.{js,ts,json,md,yml}'", "start": "probot run ./lib/index.js", - "prepare": "husky install" + "prepare": "husky install", + "test": "jest" }, "dependencies": { "@actions/core": "^1.10.0", diff --git a/src/configs/strings.ts b/src/configs/strings.ts index 3f3128920..bda3d0ae5 100644 --- a/src/configs/strings.ts +++ b/src/configs/strings.ts @@ -7,3 +7,87 @@ export const GLOBAL_STRINGS = { "Please select a child issue from the specification checklist to work on. The `/start` command is disabled on parent issues.", autopayComment: "Automatic payment for this issue is enabled:", }; + +const OPEN_BRACE = "{"; +const CLOSE_BRACE = "}"; +const ESCAPED_OPEN_BRACE = "{{"; +const ESCAPED_CLOSE_BRACE = "}}"; + +type InputValues = Record; +type ParsedFStringNode = { type: "literal"; text: string } | { type: "variable"; name: string }; + +const parseFString = (f_string: string): ParsedFStringNode[] => { + const nodes: ParsedFStringNode[] = []; + let currentPosition = 0; + + while (currentPosition < f_string.length) { + switch (f_string[currentPosition]) { + case OPEN_BRACE: { + if (f_string.substr(currentPosition, 2) === ESCAPED_OPEN_BRACE) { + const closePosition = f_string.indexOf(ESCAPED_CLOSE_BRACE, currentPosition + 2); + + if (closePosition > -1) { + nodes.push({ type: "literal", text: f_string.substring(currentPosition + 1, closePosition + 1) }); + currentPosition = closePosition + 2; + } else { + nodes.push({ type: "literal", text: OPEN_BRACE }); + currentPosition += 1; + } + } else { + const endBracePosition = f_string.indexOf(CLOSE_BRACE, currentPosition); + if (endBracePosition < 0) throw new Error("Unclosed '{' in f_string."); + + nodes.push({ + type: "variable", + name: f_string.substring(currentPosition + 1, endBracePosition), + }); + currentPosition = endBracePosition + 1; + } + break; + } + case CLOSE_BRACE: { + if (f_string.substr(currentPosition, 2) === ESCAPED_CLOSE_BRACE) { + nodes.push({ type: "literal", text: CLOSE_BRACE }); + currentPosition += 2; + continue; + } else { + throw new Error("Single '}' in f_string. Position: " + currentPosition); + } + break; + } + default: { + const nextOpenBracePosition = f_string.indexOf(OPEN_BRACE, currentPosition); + if (nextOpenBracePosition === -1) { + nodes.push({ type: "literal", text: f_string.substring(currentPosition) }); + currentPosition = f_string.length; + } else { + nodes.push({ type: "literal", text: f_string.substring(currentPosition, nextOpenBracePosition) }); + currentPosition = nextOpenBracePosition; + } + } + } + } + return nodes; +}; + +const interpolateFString = (f_string: string, values: InputValues) => + parseFString(f_string).reduce((res, node) => { + if (node.type === "variable") { + if (node.name in values) { + return res + values[node.name]; + } + throw new Error(`Missing value for variable: ${node.name}`); + } + return res + node.text; + }, ""); + +export const formatFString = (f_string: string, inputValues: InputValues) => interpolateFString(f_string, inputValues); + +export const checkValidFString = (f_string: string, inputVariables: string[]) => { + try { + const dummyInputs: InputValues = inputVariables.reduce((res, input) => ({ ...res, [input]: "" }), {}); + interpolateFString(f_string, dummyInputs); + } catch (e: any) { + throw new Error(`Invalid f-string: ${e.message}`); + } +}; diff --git a/src/tests/stringsTest.test.ts b/src/tests/stringsTest.test.ts new file mode 100644 index 000000000..92717cd88 --- /dev/null +++ b/src/tests/stringsTest.test.ts @@ -0,0 +1,87 @@ +import { checkValidFString, formatFString } from "../configs"; + +const varStrings = { + askUpdate: "Do you have any updates {username}?", + assignNotice: "The `/assign` command is disabled for this repository due to {reason}.", + askPricing: "Using the {{/ask}} command, will cost you {price}.", + manyVars: "{repo} is {status} as {reason}, it's {repoDesc}.", +}; + +describe("f-string utilities", () => { + describe("formatFString", () => { + it("should correctly format f-string with a valid input value", () => { + const values1 = { + username: "Keyrxng", + }; + const expectedString1 = "Do you have any updates Keyrxng?"; + const result1 = formatFString(varStrings.askUpdate, values1); + expect(result1).toBe(expectedString1); + + const values2 = { + reason: "maintenance", + }; + const expectedString2 = "The `/assign` command is disabled for this repository due to maintenance."; + const result2 = formatFString(varStrings.assignNotice, values2); + expect(result2).toBe(expectedString2); + + const values3 = { + price: "$10", + }; + const expectedString3 = "Using the {/ask} command, will cost you $10."; + const result3 = formatFString(varStrings.askPricing, values3); + expect(result3).toBe(expectedString3); + }); + + it("should correctly format f-string with multiple input values", () => { + const values = { + repo: "Block#Builder", + status: "no longer maintained", + reason: "it's too boring", + repoDesc: "used for building blocks", + }; + const expectedString = "Block#Builder is no longer maintained as it's too boring, it's used for building blocks."; + const result = formatFString(varStrings.manyVars, values); + expect(result).toBe(expectedString); + }); + + it("should throw error for missing input values using varStrings", () => { + expect(() => formatFString(varStrings.askUpdate, {})).toThrowError("Missing value for variable: username"); + }); + }); + + describe("checkValidFString", () => { + it("should not throw error for valid f-string using varStrings", () => { + const variables1 = ["username"]; + expect(() => checkValidFString(varStrings.askUpdate, variables1)).not.toThrow(); + + const variables2 = ["reason"]; + expect(() => checkValidFString(varStrings.assignNotice, variables2)).not.toThrow(); + + const variables3 = ["price"]; + expect(() => checkValidFString(varStrings.askPricing, variables3)).not.toThrow(); + }); + + it("should throw error for invalid brackets", () => { + let faultyTemplate = "{unassignComment} {missingVar"; + const variables = ["unassign", "keyrxng"]; + expect(() => checkValidFString(faultyTemplate, variables)).toThrowError("Invalid f-string: Unclosed '{' in f_string."); + + faultyTemplate = "{unassignComment} missingVar}"; + expect(() => checkValidFString(faultyTemplate, variables)).toThrowError("Invalid f-string: Missing value for variable: unassignComment"); + + faultyTemplate = "{unassignComment} missingVar}}"; + + expect(() => checkValidFString(faultyTemplate, variables)).toThrowError("Invalid f-string: Missing value for variable: unassignComment"); + + faultyTemplate = "{unassignComment} {{missingVar"; + + expect(() => checkValidFString(faultyTemplate, variables)).toThrowError("Invalid f-string: Unclosed '{' in f_string."); + }); + + it("should throw error for missing variables using varStrings", () => { + const template = varStrings.manyVars; + const variables = ["repo", "status"]; + expect(() => checkValidFString(template, variables)).toThrowError("Invalid f-string: Missing value for variable: reason"); + }); + }); +}); From 615512ff9830a5ae03a7df8ade971b3214aa7baa Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Sun, 15 Oct 2023 13:51:12 +0100 Subject: [PATCH 3/4] fix: kebab case rename test file --- src/tests/{stringsTest.test.ts => strings-test.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/tests/{stringsTest.test.ts => strings-test.test.ts} (100%) diff --git a/src/tests/stringsTest.test.ts b/src/tests/strings-test.test.ts similarity index 100% rename from src/tests/stringsTest.test.ts rename to src/tests/strings-test.test.ts From 0a83bf7229a421919acac68c04ff30379ed67a81 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Wed, 1 Nov 2023 14:30:18 +0000 Subject: [PATCH 4/4] fix: ts equivalent --- jest.config.js | 5 ++ src/configs/strings.ts | 87 +++------------------------------- src/tests/strings-test.test.ts | 50 +++++-------------- 3 files changed, 23 insertions(+), 119 deletions(-) create mode 100644 jest.config.js diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 000000000..7693ff771 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,5 @@ +module.exports = { + preset: "ts-jest", + testEnvironment: "node", + moduleFileExtensions: ["ts", "js", "json", "node"], +}; diff --git a/src/configs/strings.ts b/src/configs/strings.ts index bda3d0ae5..9c4e9fbd2 100644 --- a/src/configs/strings.ts +++ b/src/configs/strings.ts @@ -8,86 +8,13 @@ export const GLOBAL_STRINGS = { autopayComment: "Automatic payment for this issue is enabled:", }; -const OPEN_BRACE = "{"; -const CLOSE_BRACE = "}"; -const ESCAPED_OPEN_BRACE = "{{"; -const ESCAPED_CLOSE_BRACE = "}}"; +// Typescript equivalent of Python's f-string -type InputValues = Record; -type ParsedFStringNode = { type: "literal"; text: string } | { type: "variable"; name: string }; +type Variables = Record; -const parseFString = (f_string: string): ParsedFStringNode[] => { - const nodes: ParsedFStringNode[] = []; - let currentPosition = 0; - - while (currentPosition < f_string.length) { - switch (f_string[currentPosition]) { - case OPEN_BRACE: { - if (f_string.substr(currentPosition, 2) === ESCAPED_OPEN_BRACE) { - const closePosition = f_string.indexOf(ESCAPED_CLOSE_BRACE, currentPosition + 2); - - if (closePosition > -1) { - nodes.push({ type: "literal", text: f_string.substring(currentPosition + 1, closePosition + 1) }); - currentPosition = closePosition + 2; - } else { - nodes.push({ type: "literal", text: OPEN_BRACE }); - currentPosition += 1; - } - } else { - const endBracePosition = f_string.indexOf(CLOSE_BRACE, currentPosition); - if (endBracePosition < 0) throw new Error("Unclosed '{' in f_string."); - - nodes.push({ - type: "variable", - name: f_string.substring(currentPosition + 1, endBracePosition), - }); - currentPosition = endBracePosition + 1; - } - break; - } - case CLOSE_BRACE: { - if (f_string.substr(currentPosition, 2) === ESCAPED_CLOSE_BRACE) { - nodes.push({ type: "literal", text: CLOSE_BRACE }); - currentPosition += 2; - continue; - } else { - throw new Error("Single '}' in f_string. Position: " + currentPosition); - } - break; - } - default: { - const nextOpenBracePosition = f_string.indexOf(OPEN_BRACE, currentPosition); - if (nextOpenBracePosition === -1) { - nodes.push({ type: "literal", text: f_string.substring(currentPosition) }); - currentPosition = f_string.length; - } else { - nodes.push({ type: "literal", text: f_string.substring(currentPosition, nextOpenBracePosition) }); - currentPosition = nextOpenBracePosition; - } - } - } - } - return nodes; -}; - -const interpolateFString = (f_string: string, values: InputValues) => - parseFString(f_string).reduce((res, node) => { - if (node.type === "variable") { - if (node.name in values) { - return res + values[node.name]; - } - throw new Error(`Missing value for variable: ${node.name}`); - } - return res + node.text; - }, ""); - -export const formatFString = (f_string: string, inputValues: InputValues) => interpolateFString(f_string, inputValues); - -export const checkValidFString = (f_string: string, inputVariables: string[]) => { - try { - const dummyInputs: InputValues = inputVariables.reduce((res, input) => ({ ...res, [input]: "" }), {}); - interpolateFString(f_string, dummyInputs); - } catch (e: any) { - throw new Error(`Invalid f-string: ${e.message}`); - } +export const formatFString = (template: string, variables: T): string => { + return template.replace(/{(.*?)}/g, (_match, key) => { + if (!(key.trim() in variables)) throw new Error(`Missing value for variable: ${key.trim()}`); + return String(variables[key.trim()]); + }); }; diff --git a/src/tests/strings-test.test.ts b/src/tests/strings-test.test.ts index 92717cd88..4e81ff5df 100644 --- a/src/tests/strings-test.test.ts +++ b/src/tests/strings-test.test.ts @@ -1,9 +1,9 @@ -import { checkValidFString, formatFString } from "../configs"; +import { formatFString } from "../configs"; const varStrings = { askUpdate: "Do you have any updates {username}?", assignNotice: "The `/assign` command is disabled for this repository due to {reason}.", - askPricing: "Using the {{/ask}} command, will cost you {price}.", + askPricing: "Using the /ask command, will cost you {price}.", manyVars: "{repo} is {status} as {reason}, it's {repoDesc}.", }; @@ -15,6 +15,8 @@ describe("f-string utilities", () => { }; const expectedString1 = "Do you have any updates Keyrxng?"; const result1 = formatFString(varStrings.askUpdate, values1); + + console.log(`Expected: ${expectedString1}\n Result: ${result1}`); expect(result1).toBe(expectedString1); const values2 = { @@ -22,13 +24,17 @@ describe("f-string utilities", () => { }; const expectedString2 = "The `/assign` command is disabled for this repository due to maintenance."; const result2 = formatFString(varStrings.assignNotice, values2); + + console.log(`Expected: ${expectedString2}\n Result: ${result2}`); expect(result2).toBe(expectedString2); const values3 = { price: "$10", }; - const expectedString3 = "Using the {/ask} command, will cost you $10."; + const expectedString3 = "Using the /ask command, will cost you $10."; const result3 = formatFString(varStrings.askPricing, values3); + + console.log(`Expected: ${expectedString3}\n Result: ${result3}`); expect(result3).toBe(expectedString3); }); @@ -41,6 +47,8 @@ describe("f-string utilities", () => { }; const expectedString = "Block#Builder is no longer maintained as it's too boring, it's used for building blocks."; const result = formatFString(varStrings.manyVars, values); + + console.log(`Expected: ${expectedString}\n Result: ${result}`); expect(result).toBe(expectedString); }); @@ -48,40 +56,4 @@ describe("f-string utilities", () => { expect(() => formatFString(varStrings.askUpdate, {})).toThrowError("Missing value for variable: username"); }); }); - - describe("checkValidFString", () => { - it("should not throw error for valid f-string using varStrings", () => { - const variables1 = ["username"]; - expect(() => checkValidFString(varStrings.askUpdate, variables1)).not.toThrow(); - - const variables2 = ["reason"]; - expect(() => checkValidFString(varStrings.assignNotice, variables2)).not.toThrow(); - - const variables3 = ["price"]; - expect(() => checkValidFString(varStrings.askPricing, variables3)).not.toThrow(); - }); - - it("should throw error for invalid brackets", () => { - let faultyTemplate = "{unassignComment} {missingVar"; - const variables = ["unassign", "keyrxng"]; - expect(() => checkValidFString(faultyTemplate, variables)).toThrowError("Invalid f-string: Unclosed '{' in f_string."); - - faultyTemplate = "{unassignComment} missingVar}"; - expect(() => checkValidFString(faultyTemplate, variables)).toThrowError("Invalid f-string: Missing value for variable: unassignComment"); - - faultyTemplate = "{unassignComment} missingVar}}"; - - expect(() => checkValidFString(faultyTemplate, variables)).toThrowError("Invalid f-string: Missing value for variable: unassignComment"); - - faultyTemplate = "{unassignComment} {{missingVar"; - - expect(() => checkValidFString(faultyTemplate, variables)).toThrowError("Invalid f-string: Unclosed '{' in f_string."); - }); - - it("should throw error for missing variables using varStrings", () => { - const template = varStrings.manyVars; - const variables = ["repo", "status"]; - expect(() => checkValidFString(template, variables)).toThrowError("Invalid f-string: Missing value for variable: reason"); - }); - }); });