diff --git a/.changeset/green-chefs-vanish.md b/.changeset/green-chefs-vanish.md new file mode 100644 index 0000000..43e2e22 --- /dev/null +++ b/.changeset/green-chefs-vanish.md @@ -0,0 +1,5 @@ +--- +"@onflow/flow-cadut": patch +--- + +Arguments for contract initialization can now be places on multiple lines diff --git a/jest.config.js b/jest.config.js index 1c15fa8..fd67821 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,3 @@ module.exports = { - projects: ["/packages/*"], + projects: ["/packages/flow-cadut"], } diff --git a/package-lock.json b/package-lock.json index b33e7fd..ec33c6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21994,7 +21994,7 @@ }, "packages/flow-cadut": { "name": "@onflow/flow-cadut", - "version": "0.2.0-alpha.8", + "version": "0.2.0-alpha.9", "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.18.6", @@ -22018,11 +22018,11 @@ }, "packages/flow-cadut-generator": { "name": "@onflow/flow-cadut-generator", - "version": "0.0.1", + "version": "0.1.0-alpha.0", "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.18.6", - "@onflow/flow-cadut": "^0.2.0-alpha.8", + "@onflow/flow-cadut": "^0.2.0-alpha.9", "chokidar": "^3.5.3", "glob": "^8.0.3", "inquirer": "^8.2.4", @@ -23340,7 +23340,7 @@ }, "devDependencies": { "@onflow/fcl": "^0.0.73", - "@onflow/flow-cadut-generator": "^0.0.1", + "@onflow/flow-cadut-generator": "^0.1.0-alpha.0", "elliptic": "^6.5.4", "esm": "^3.2.25", "rlp": "^2.2.6", @@ -28217,7 +28217,7 @@ "requires": { "@babel/runtime": "^7.18.6", "@onflow/fcl-bundle": "^1.1.0", - "@onflow/flow-cadut": "^0.2.0-alpha.8", + "@onflow/flow-cadut": "^0.2.0-alpha.9", "chokidar": "^3.5.3", "glob": "^8.0.3", "inquirer": "^8.2.4", @@ -32143,7 +32143,7 @@ "requires": { "@onflow/fcl": "^0.0.73", "@onflow/fcl-config": "^0.0.1", - "@onflow/flow-cadut-generator": "^0.0.1", + "@onflow/flow-cadut-generator": "^0.1.0-alpha.0", "elliptic": "^6.5.4", "esm": "^3.2.25", "rlp": "^2.2.6", diff --git a/packages/dev-test/test/interaction.test.js b/packages/dev-test/test/interaction.test.js index 7caca9f..900b1a9 100644 --- a/packages/dev-test/test/interaction.test.js +++ b/packages/dev-test/test/interaction.test.js @@ -50,8 +50,9 @@ describe("arguments - scripts", () => { } ` const payer = authorization() - const [txId, err] = await mutate({cadence, payer, wait: null}) - console.log({txId, err}) + const result = await mutate({cadence, payer, wait: null}) + const err = result[1] + expect(err).toBe(null) }) it("shall properly process Address: [UInt64] array", async () => { @@ -74,8 +75,8 @@ describe("arguments - scripts", () => { }, ] - const [result, err] = await query({cadence, args}) - console.log({result, err}) + const [result] = await query({cadence, args}) + expect(result).toBe("42") }) }) diff --git a/packages/dev-test/test/signers.test.js b/packages/dev-test/test/signers.test.js index 18dafd1..e1242c0 100644 --- a/packages/dev-test/test/signers.test.js +++ b/packages/dev-test/test/signers.test.js @@ -23,7 +23,8 @@ describe("signers", () => { } `, }) - console.log({result, err}) + expect(result).toBe("42") + expect(err).toBe(null) }) it("shall properly sign transaction", async () => { @@ -45,8 +46,8 @@ describe("signers", () => { keyId, } - const [result, err] = await sendTransaction({code, payer}) - console.log(result) - console.log(err) + const result = await sendTransaction({code, payer}) + const err = result[0] + expect(err).toBe(null) }) }) diff --git a/packages/flow-cadut-plugin-find/tests/index.test.js b/packages/flow-cadut-plugin-find/tests/index.test.js index 26a884d..964b7bb 100644 --- a/packages/flow-cadut-plugin-find/tests/index.test.js +++ b/packages/flow-cadut-plugin-find/tests/index.test.js @@ -18,7 +18,8 @@ describe("FIND plugin", () => { } ` const args = ["find:shiny"] - return mapValuesToCode(code, args) + const mapped = mapValuesToCode(code, args) + expect(mapped).toBe(true) }) it("shall resolve name.find properly", async () => { @@ -28,6 +29,7 @@ describe("FIND plugin", () => { } ` const args = ["shiny.find"] - return mapValuesToCode(code, args) + const mapped = mapValuesToCode(code, args) + expect(mapped).toBe(true) }) }) diff --git a/packages/flow-cadut/jest.config.js b/packages/flow-cadut/jest.config.js new file mode 100644 index 0000000..4068c49 --- /dev/null +++ b/packages/flow-cadut/jest.config.js @@ -0,0 +1,5 @@ +const config = { + testMatch: ["**/tests/**/*.test.[jt]s?(x)"], +} + +module.exports = config diff --git a/packages/flow-cadut/src/args.js b/packages/flow-cadut/src/args.js index eec540c..719b81f 100644 --- a/packages/flow-cadut/src/args.js +++ b/packages/flow-cadut/src/args.js @@ -163,6 +163,17 @@ export const mapArgument = async (rawType, rawValue) => { return fcl.arg(value, resolvedType) } + case isBasicNumType(type): { + // Try to parse value and throw if it fails + if (value === null) { + return fcl.arg(null, resolvedType) + } + if (isNaN(parseInt(value))) { + throwTypeError("Expected proper value for integer type") + } + return fcl.arg(value.toString(), resolvedType) + } + case isFixedNumType(type): { // Try to parse value and throw if it fails if (value === null) { @@ -196,8 +207,14 @@ export const mapArgument = async (rawType, rawValue) => { return fcl.arg(mappedValue, resolvedType) } - const result = fcl.arg(value, resolvedType) - return result + if (isBasicNumType(arrayType) || isFixedNumType(arrayType)) { + return fcl.arg( + value.map(item => (isNaN(item) ? item : item.toString())), + resolvedType + ) + } + + return fcl.arg(value, resolvedType) } case isDictionary(type): { diff --git a/packages/flow-cadut/src/imports.js b/packages/flow-cadut/src/imports.js index 849d8d4..c46db0c 100644 --- a/packages/flow-cadut/src/imports.js +++ b/packages/flow-cadut/src/imports.js @@ -20,27 +20,32 @@ === REGEXP_IMPORT explanation === Matches import line in cadence code and is used for extracting address & list of contracts imported - / => start of regexp - import\s+ => should have keyword import followed by one or more spaces - - ((([\w\d]+)(\s*,\s*))*[\w\d]+) => >>MATCH[1]<< matcher group for imported contracts (one or more comma separated words including digits) + / => start of regexp + ^ => Matches the start of the string. + \s* => Matches zero or more whitespace characters. + import\s+ => Matches the word "import" followed by one or more whitespace characters. + ("?([\w\d]+\s*,\s*)*(?!from\b)[\w\d]+"?) => Matches a list of one or more variable names separated by commas, + with each variable name optionally surrounded by quotes (?"), and optional + whitespace characters in between ([\w\d]+\s*,\s*)*. The negative lookahead + assertion (?!from\b) ensures that the word "from" is not part of a + variable name. The entire list of variable names is optionally surrounded + by quotes as well (?"). + \s* => Matches zero or more whitespace characters. + (?:from)? => Matches the word "from" if it appears, but does not capture it. + \s* => Matches zero or more whitespace characters. + ([\w\d".\/]+)? => Matches the target path, which is a sequence of one or more + alphanumeric characters, quotes ("), periods (.), forward slashes (/), + or double quotes ("). + $ => Matches the end of the string. + / => end of regexp - ([\w\d]+\s*,\s*)* => match comma-separated contracts - [\w\d]+ => match individual contract name (one or more word or digit) - \s*,\s* => match trailing comma with any amount of space separation - - [\w\d]+ => match last contract name (mustn't have trailing comma, so separate from previous matcher) - - \s+from\s+ => keyword from with one or more leading and following space characters - ([\w\d".\\/]+) => >>MATCH[3]<< one or more word, digit, "" or / character for address or file import notation - / => end of regexp */ export const REGEXP_IMPORT = - /import\s+(([\w\d]+\s*,\s*)*[\w\d]+)\s+from\s*([\w\d".\\/]+)/g + /\s*import\s+("?([\w\d]+\s*,\s*)*(?!from\b)[\w\d]+"?)\s*(?:from)?\s*([\w\d"./]+)?$/gm /* === REGEXP_IMPORT_CONTRACT === - Used to separate individual contract names from comma/space separarated list of contracts + Used to separate individual contract names from comma/space separated list of contracts / => start of regexp ([\w\d]+) => >>MATCH[1]<< match individual contract name (one or more word or digit) @@ -58,7 +63,8 @@ export const extractImports = code => { return {} } - return [...code.matchAll(REGEXP_IMPORT)].reduce((contracts, match) => { + const lines = [...code.matchAll(REGEXP_IMPORT)] + return lines.reduce((contracts, match) => { const contractsStr = match[1], address = match[3] @@ -117,30 +123,45 @@ export const reportMissingImports = (code, addressMap, prefix = "") => { /** * Returns Cadence template code with replaced import addresses * @param {string} code - Cadence template code. - * @param {{string:string}} [addressMap={}] - name/address map or function to use as lookup table + * @param {{[key:string]:string}} [addressMap={}] - name/address map or function to use as lookup table * for addresses in import statements. * @param byName - lag to indicate whether we shall use names of the contracts. * @returns {*} */ export const replaceImportAddresses = (code, addressMap, byName = true) => { + const EMPTY = "empty" return code.replace(REGEXP_IMPORT, importLine => { const contracts = extractImports(importLine) + const contractMap = Object.keys(contracts).reduce((map, contract) => { const address = contracts[contract] + const key = byName ? contract : address const newAddress = addressMap instanceof Function ? addressMap(key) : addressMap[key] // If the address is not inside addressMap we shall not alter import statement - const validAddress = newAddress || address - map[validAddress] = (map[validAddress] ?? []).concat(contract) + let validAddress = newAddress || address + + if (!newAddress || contract === "Crypto") { + validAddress = EMPTY + } + + if (!map[validAddress]) { + map[validAddress] = [] + } + + map[validAddress] = map[validAddress].concat(contract) return map }, {}) return Object.keys(contractMap) .reduce((res, addr) => { const contractsStr = contractMap[addr].join(", ") - return res.concat(`import ${contractsStr} from ${addr}`) + if (addr === EMPTY) { + return res.concat(`import ${contractsStr}`) + } + return res.concat(`import ${contractsStr} from ${addr} `) }, []) .join("\n") }) diff --git a/packages/flow-cadut/src/parser.js b/packages/flow-cadut/src/parser.js index c79e051..37a0703 100644 --- a/packages/flow-cadut/src/parser.js +++ b/packages/flow-cadut/src/parser.js @@ -89,7 +89,7 @@ export const extractContractName = code => { export const extractContractParameters = code => { const complexMatcher = /(resource|struct)\s+\w+\s*{[\s\S]+?}/g const contractNameMatcher = - /(?:access\(\w+\)|pub)\s+contract\s+(?:interface)*\s*(\w*)[:\s\w]*(\s*{[.\s\S]*init\s*\((.*?)\)[.\s\S]*})?/g + /(?:access\(\w+\)|pub)\s+contract\s+(?:interface)*\s*(\w*)[:\s\w]*(\s*{[.\s\S]*init\s*\(([.\s\S\r\n]*)\)[.\s\S]*})?/g const noComments = stripComments(code) const noComplex = noComments.replace(complexMatcher, "") diff --git a/packages/flow-cadut/tests/args.test.js b/packages/flow-cadut/tests/args.test.js index 79b5613..29705ee 100644 --- a/packages/flow-cadut/tests/args.test.js +++ b/packages/flow-cadut/tests/args.test.js @@ -8,6 +8,7 @@ import { } from "../src/args" import {toFixedValue, withPrefix} from "../src/fixer" import {getTemplateInfo} from "../src" +import {isBasicNumType} from "../src/type-checker" describe("argument types", () => { test("Basic Type", async () => { @@ -77,10 +78,21 @@ describe("type resolvers", () => { for (let i = 0; i < cases.length; i++) { const {type, argInput} = cases[i] const output = resolveType(type) - const asArgument = output.asArgument(argInput) + let value = argInput + if (isBasicNumType(type)) { + value = value.toString() + } + const asArgument = output.asArgument(value) + + /*eslint-disable*/ expect(asArgument.type).toBe(type) - expect(asArgument.value.toString()).toBe(argInput.toString()) + if (isBasicNumType(asArgument.type)) { + expect(asArgument.value).toBe(argInput.toString()) + } else { + expect(asArgument.value).toBe(argInput) + } + /*eslint-enable*/ } }) test("simple array = [String]", () => { @@ -398,7 +410,7 @@ describe("mapValuesToCode", () => { expect(first.xform.label).toBe("Array") expect(first.value[0].length).toBe(values[0][0].length) for (let i = 0; i < values[0][0].length; i++) { - expect(first.value[0][i]).toBe(values[0][0][i]) + expect(first.value[0][i]).toBe(values[0][0][i].toString()) } }) }) diff --git a/packages/flow-cadut/tests/example.test.js b/packages/flow-cadut/tests/example.test.js index 036376e..aff3cc6 100644 --- a/packages/flow-cadut/tests/example.test.js +++ b/packages/flow-cadut/tests/example.test.js @@ -4,7 +4,7 @@ import { missingImports, report, replaceImportAddresses, -} from "../src/imports" +} from "../src" // arguments import { @@ -15,7 +15,7 @@ import { argType, getDictionaryTypes, getArrayType, -} from "../src/args" +} from "../src" // parser import { @@ -27,10 +27,10 @@ import { extractScriptArguments, extractTransactionArguments, extractContractName, -} from "../src/parser" +} from "../src" // Interactions -import {setEnvironment, getEnvironment} from "../src/env" +import {setEnvironment, getEnvironment} from "../src" describe("documentation examples", function () { // Imports @@ -70,8 +70,13 @@ describe("documentation examples", function () { }) it("should report to console", function () { + console.error = jest.fn() const list = ["Message", "Log"] report(list) + expect(console.error.mock.calls[0].length).toBe(2) + expect(console.error.mock.calls[0][0].includes("Missing imports")).toBe( + true + ) }) it("should replace import addresses", function () { @@ -318,6 +323,7 @@ describe("documentation examples", function () { expect(complexType).toBe("{String:String}") }) + /* eslint-disable */ // Generator // This block is commented out for CI to work properly /* @@ -333,6 +339,7 @@ describe("documentation examples", function () { await processGitRepo(url, output); }); */ + /* eslint-enable */ // Interactions it("should throw error for unknown network", async function () { diff --git a/packages/flow-cadut/tests/imports.test.js b/packages/flow-cadut/tests/imports.test.js index 8261da8..a617918 100644 --- a/packages/flow-cadut/tests/imports.test.js +++ b/packages/flow-cadut/tests/imports.test.js @@ -37,7 +37,7 @@ describe("imports RegExp tests", () => { expect(match[3]).toEqual("0x01") }) - it("REGEXP_IMPORT - shall not match import with trailing comma", () => { + it("REGEXP_IMPORT - shall match import with trailing comma", () => { const test = "import Foo, from 0x01" const [match] = test.matchAll(REGEXP_IMPORT) expect(match).toEqual(undefined) @@ -52,7 +52,7 @@ describe("imports RegExp tests", () => { it("REGEXP_IMPORT - shall not match without import address", () => { const test = "import Foo from" const [match] = test.matchAll(REGEXP_IMPORT) - expect(match).toEqual(undefined) + expect(match[1]).toEqual("Foo") }) it("REGEXP_IMPORT - shall not match without space preceeding imports", () => { @@ -154,4 +154,57 @@ describe("imports tests", () => { replaced.includes("import Messages, GiraffeNFT from 0xf8d6e0586b0a20c7") ).toBe(true) }) + + it("replaceImportAddresses - should properly inject import target for single contracts", function () { + const code = ` + import Messages + pub fun main(){} + ` + const addressMap = { + Messages: "0xf8d6e0586b0a20c7", + GiraffeNFT: "0xf8d6e0586b0a20c7", + } + const replaced = replaceImportAddresses(code, addressMap) + expect(replaced.includes("import Messages from 0xf8d6e0586b0a20c7")).toBe( + true + ) + }) + + it("replaceImportAddresses - should properly inject import target for multiple contracts", function () { + const code = ` + import Messages, GiraffeNFT + pub fun main(){} + ` + const addressMap = { + Messages: "0xf8d6e0586b0a20c7", + GiraffeNFT: "0xf8d6e0586b0a20c7", + } + const replaced = replaceImportAddresses(code, addressMap) + expect( + replaced.includes("import Messages, GiraffeNFT from 0xf8d6e0586b0a20c7") + ).toBe(true) + }) +}) + +describe("Built-in contracts", () => { + it("replaceImportAddresses - should keep import unchanged for built-in contracts", function () { + const code = ` + import Crypto + pub fun main(){} + ` + const addressMap = { + Crypto: "0xf8d6e0586b0a20c7", + } + const replaced = replaceImportAddresses(code, addressMap) + expect(replaced.includes("0xf8d6e0586b0a20c7")).toBe(false) + }) + + it("replaceImportAddresses - should keep import unchanged for contracts with address map", function () { + const code = ` + import Crypto + pub fun main(){} + ` + const replaced = replaceImportAddresses(code, {}) + expect(replaced.includes("Crypto\n")).toBe(true) + }) }) diff --git a/packages/flow-cadut/tests/parser.test.js b/packages/flow-cadut/tests/parser.test.js index 89107b3..8cb4433 100644 --- a/packages/flow-cadut/tests/parser.test.js +++ b/packages/flow-cadut/tests/parser.test.js @@ -12,6 +12,7 @@ import { stripComments, stripStrings, extractImports, + generateSchema, } from "../src" import flovatarContract from "./stubs/flovatar" @@ -218,7 +219,7 @@ describe("parser", () => { import Crypto ` const result = extractImports(code) - expect(Object.keys(result).length).toBe(5) + expect(Object.keys(result).length).toBe(6) }) }) @@ -337,6 +338,22 @@ describe("extract contract parameters", () => { expect(output.args).toBe(args) }) + test("with init method in code - multi-line args", () => { + const contractName = "Hello" + const args = `a: String, + b: {String: String} + ` + const input = ` + pub contract interface ${contractName} { + // init method here + init(${args}){} + } + ` + const output = extractContractParameters(input) + expect(output.contractName).toBe(contractName) + expect(output.args).toBe(args) + }) + test("with init method in code, contract implements interface - multiple argument", () => { const contractName = "Hello" const args = "a: String, b: {String: String}" @@ -570,3 +587,23 @@ describe("pragma extractor", () => { expect(result.description).toBe("Simple Script to ping network") }) }) + +describe("schema generator", () => { + test("single line arguments", () => { + const args = `a: Int, b: UInt8` + const schema = generateSchema(args) + expect(schema.length).toBe(2) + expect(schema[0]).toBe("a:Int") + expect(schema[1]).toBe("b:UInt8") + }) + + test("multi line arguments", () => { + const args = ` + a: Int, + b: UInt8` + const schema = generateSchema(args) + expect(schema.length).toBe(2) + expect(schema[0]).toBe("a:Int") + expect(schema[1]).toBe("b:UInt8") + }) +})