diff --git a/src/v3/core/__snapshots__/address.test.js.snap b/src/v3/core/__snapshots__/address.test.js.snap new file mode 100644 index 0000000..268a64b --- /dev/null +++ b/src/v3/core/__snapshots__/address.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`core/address makeAddressModule has a stable internal representation 1`] = `"\\"F\\\\u0000\\\\u0000hello\\\\u0000\\\\u0000\\\\u0000sweet\\\\u0000world\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\""`; diff --git a/src/v3/core/address.js b/src/v3/core/address.js index 9069df1..598f624 100644 --- a/src/v3/core/address.js +++ b/src/v3/core/address.js @@ -115,29 +115,79 @@ export function makeAddressModule(options: Options): AddressModule { otherNoncesWithSeparators.set(otherNonce + separator, otherName); } - const _ = {name, nonceWithSeparator}; - function assertValid(address: Address, what?: string): void { - const _ = {address, what}; - throw new Error("assertValid"); + // TODO(perf): If this function becomes a bottleneck, consider + // omitting it entirely in production. If this is undesirable, a + // number of micro-optimizations can be made. + const prefix = what == null ? "" : `${what}: `; + if (address == null) { + throw new Error(prefix + `expected ${name}, got: ${String(address)}`); + } + if (!address.endsWith(separator)) { + throw new Error(prefix + `expected ${name}, got: ${stringify(address)}`); + } + if (!address.startsWith(nonceWithSeparator)) { + for (const [ + otherNonceWithSeparator, + otherName, + ] of otherNoncesWithSeparators) { + if (address.startsWith(otherNonceWithSeparator)) { + throw new Error( + prefix + `expected ${name}, got ${otherName}: ${stringify(address)}` + ); + } + } + throw new Error(prefix + `expected ${name}, got: ${stringify(address)}`); + } + } + + function partsString(parts: $ReadOnlyArray): string { + // This is needed to properly print arrays containing `undefined`. + return "[" + parts.map((p) => String(stringify(p))).join(",") + "]"; } function assertValidParts( parts: $ReadOnlyArray, what?: string ): void { - const _ = {parts, what}; - throw new Error("assertValidParts"); + // TODO(perf): If this function becomes a bottleneck, consider + // omitting it entirely in production. If this is undesirable, a + // number of micro-optimizations can be made. + const prefix = what == null ? "" : `${what}: `; + if (parts == null) { + throw new Error( + prefix + `expected array of parts, got: ${String(parts)}` + ); + } + parts.forEach((s: string) => { + if (s == null) { + throw new Error( + prefix + + `expected array of parts, got ${String(s)} in: ${partsString( + parts + )}` + ); + } + if (s.indexOf(separator) !== -1) { + const where = `${stringify(s)} in ${partsString(parts)}`; + throw new Error(prefix + `part contains NUL character: ${where}`); + } + }); + } + + function nullDelimited(components: $ReadOnlyArray): string { + return [...components, ""].join(separator); } function fromParts(parts: $ReadOnlyArray): Address { - const _ = parts; - throw new Error("fromParts"); + assertValidParts(parts); + return nonce + separator + nullDelimited(parts); } function toParts(address: Address): string[] { - const _ = address; - throw new Error("toParts"); + assertValid(address); + const parts = address.split(separator); + return parts.slice(1, parts.length - 1); } function toString(address: Address): string { diff --git a/src/v3/core/address.test.js b/src/v3/core/address.test.js index 7d194ce..25f3dbe 100644 --- a/src/v3/core/address.test.js +++ b/src/v3/core/address.test.js @@ -58,5 +58,163 @@ describe("core/address", () => { FooAddress.assertValid = FooAddress.assertValid; }).toThrow(/read.only property/); }); + + it("has a stable internal representation", () => { + const input = ["", "hello", "", "", "sweet", "world", "", "", ""]; + const address = makeModules().FooAddress.fromParts(input); + // We stringify the address here because otherwise literal NUL + // characters will appear in the snapshot file. + expect(JSON.stringify(address)).toMatchSnapshot(); + }); + + describe("assertValid", () => { + const {FooAddress, BarAddress, WatAddress} = makeModules(); + it("rejects `undefined`", () => { + expect(() => { + // $ExpectFlowError + FooAddress.assertValid(undefined, "widget"); + }).toThrow("widget: expected FooAddress, got: undefined"); + }); + it("rejects `null`", () => { + expect(() => { + // $ExpectFlowError + FooAddress.assertValid(null, "widget"); + }).toThrow("widget: expected FooAddress, got: null"); + }); + it("rejects an address from a known module", () => { + const bar = BarAddress.fromParts(["hello"]); + expect(() => { + FooAddress.assertValid(bar, "widget"); + }).toThrow("widget: expected FooAddress, got BarAddress:"); + }); + it("rejects an address from an unknown module", () => { + const wat = WatAddress.fromParts(["hello"]); + expect(() => { + FooAddress.assertValid(wat, "widget"); + }).toThrow("widget: expected FooAddress, got:"); + }); + it("rejects a string that starts with the nonce but no separator", () => { + expect(() => { + FooAddress.assertValid("F/things\0", "widget"); + }).toThrow("widget: expected FooAddress, got:"); + }); + it("rejects a string that does not end with a separator", () => { + const address = FooAddress.fromParts(["hello"]); + const truncated = address.substring(0, address.length - 2); + expect(() => { + FooAddress.assertValid(truncated, "widget"); + }).toThrow("widget: expected FooAddress, got:"); + }); + it("rejects junk", () => { + expect(() => { + FooAddress.assertValid("junk", "widget"); + }).toThrow("widget: expected FooAddress, got:"); + }); + }); + + describe("assertValidParts", () => { + const {FooAddress} = makeModules(); + it("rejects `undefined`", () => { + expect(() => { + // $ExpectFlowError + FooAddress.assertValidParts(undefined, "widget"); + }).toThrow("widget: expected array of parts, got: undefined"); + }); + it("rejects `null`", () => { + expect(() => { + // $ExpectFlowError + FooAddress.assertValidParts(null, "widget"); + }).toThrow("widget: expected array of parts, got: null"); + }); + it("rejects an array containing `undefined`", () => { + expect(() => { + // $ExpectFlowError + FooAddress.assertValidParts(["hello", undefined, "world"], "widget"); + }).toThrow( + "widget: expected array of parts, got undefined in: " + + '["hello",undefined,"world"]' + ); + }); + it("rejects an array containing `null`", () => { + expect(() => { + // $ExpectFlowError + FooAddress.assertValidParts(["hello", null, "world"], "widget"); + }).toThrow( + "widget: expected array of parts, got null in: " + + '["hello",null,"world"]' + ); + }); + it("rejects an array with a string containing a NUL character", () => { + expect(() => { + FooAddress.assertValidParts(["hello", "n\0o", "world"], "widget"); + }).toThrow( + "widget: part contains NUL character: " + + '"n\\u0000o" in ["hello","n\\u0000o","world"]' + ); + }); + }); + + describe("fromParts", () => { + const {FooAddress, BarAddress} = makeModules(); + + // We use this next test as a proxy for fully correct validation, + // in conjunction with tests on `assertValid` and + // `assertValidParts`. + it("validates parts", () => { + expect(() => { + // $ExpectFlowError + FooAddress.fromParts(["hello", null, "world"]); + }).toThrow( + 'expected array of parts, got null in: ["hello",null,"world"]' + ); + }); + + it("encodes the address kind for the empty address", () => { + const foo = FooAddress.fromParts([]); + const bar = BarAddress.fromParts([]); + expect(foo).not.toEqual(bar); + }); + it("encodes the address kind for a normal address", () => { + const foo = FooAddress.fromParts(["hello", "world"]); + const bar = BarAddress.fromParts(["hello", "world"]); + expect(foo).not.toEqual(bar); + }); + }); + + describe("toParts", () => { + const {FooAddress, BarAddress} = makeModules(); + + // We use this next test as a proxy for fully correct validation, + // in conjunction with tests on `assertValid`. + it("validates address kind", () => { + const bar = BarAddress.fromParts(["hello"]); + expect(() => { + FooAddress.toParts(bar); + }).toThrow("expected FooAddress, got BarAddress:"); + }); + + describe("is a left identity for `fromParts`", () => { + function check(description, inputParts) { + it(description, () => { + const address = FooAddress.fromParts(inputParts); + const parts = FooAddress.toParts(address); + expect(parts).toEqual(inputParts); + }); + } + check("on the empty input", []); + check("on an input made of one empty part", [""]); + check("on an input made of lots of empty parts", ["", "", ""]); + check("on an input with lots of empty parts throughout", [ + ...["", ""], + "hello", + ...["", "", ""], + "sweet", + "world", + ...["", "", "", ""], + ]); + check("on a singleton input", ["jar"]); + check("on an input with repeated components", ["jar", "jar"]); + }); + }); }); });