Add Address.fromRaw (#1871)

This adds a fromRaw method to the Address module so that we may properly
parse serialized addresses. It has a type signature of `string =>
Address`, and throws an error if the string is not a valid address.

I basically copied the implementation out of the `assertValid` method. I
considered deduplicating them (i.e. having `assertValid` simply call
`fromRaw`), but `assertValid` had some extra logic around accepting a
prefix for the error message, and it felt simpler to simply copy+paste
rather than trying to wrap it.

Test plan:
I added unit tests; `yarn test` passes.
This commit is contained in:
Dandelion Mané 2020-06-19 11:20:02 -07:00 committed by GitHub
parent 8ea16de6f9
commit ea175a390a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 65 additions and 2 deletions

View File

@ -5,7 +5,7 @@ import deepFreeze from "deep-freeze";
import * as MapUtil from "../util/map";
export interface AddressModule<Address> {
export interface AddressModule<Address: string> {
/**
* Assert at runtime that the provided address is actually a valid
* address of this kind, throwing an error if it is not. If `what` is
@ -73,6 +73,19 @@ export interface AddressModule<Address> {
* e.g., `toParts(["ban"])` is not a prefix of `toParts(["banana"])`.
*/
hasPrefix(address: Address, prefix: Address): boolean;
/**
* Interpret the provided string as an Address.
*
* Addresses are natively stored as strings. This method verifies
* that the provided "raw" address is actually an Address, so that
* you can have a type-level assurance that a string is an Address.
*
* This is useful if e.g. you are loading serialized Addresses.
*
* Throws an error if the string is not a valid Address.
*/
fromRaw(raw: string): Address;
}
export type Options = {|
@ -224,6 +237,28 @@ export function makeAddressModule(options: Options): AddressModule<string> {
return address.startsWith(prefix);
}
function fromRaw(address: string): Address {
if (!address.endsWith(separator)) {
throw new Error(
`address does not end with separator: ${stringify(address)}`
);
}
if (!address.startsWith(nonceWithSeparator)) {
for (const [
otherNonceWithSeparator,
otherName,
] of otherNoncesWithSeparators) {
if (address.startsWith(otherNonceWithSeparator)) {
throw new Error(
`expected ${name}, got ${otherName}: ${stringify(address)}`
);
}
}
throw new Error(`expected ${name}, got: ${stringify(address)}`);
}
return address;
}
const result = {
assertValid,
assertValidParts,
@ -233,6 +268,7 @@ export function makeAddressModule(options: Options): AddressModule<string> {
toString,
append,
hasPrefix,
fromRaw,
};
return deepFreeze(result);
}

View File

@ -164,6 +164,33 @@ describe("core/address", () => {
});
});
describe("fromRaw", () => {
const {FooAddress, BarAddress} = makeModules();
function thunk(x: string) {
return () => FooAddress.fromRaw(x);
}
it("throws on an empty string", () => {
expect(thunk("")).toThrow("address does not end with separator");
});
it("throws on a string that doesn't start with the right separator", () => {
expect(thunk("\0")).toThrow("expected FooAddress, got");
});
it("throws on a string from a different address module", () => {
expect(thunk(BarAddress.empty)).toThrow(
"expected FooAddress, got BarAddress"
);
});
function roundTrip(x) {
expect(FooAddress.fromRaw(x)).toEqual(x);
}
it("works on an empty address", () => {
roundTrip(FooAddress.empty);
});
it("works on a non-empty address", () => {
roundTrip(FooAddress.fromParts(["foo", "bar"]));
});
});
describe("fromParts", () => {
const {FooAddress, BarAddress} = makeModules();

View File

@ -13,7 +13,7 @@ const EMPTY_ENTRY_SYMBOL = Symbol("EMPTY");
type Entry<V> = {|+map: RecursiveMap<V>, value: V | typeof EMPTY_ENTRY_SYMBOL|};
type RecursiveMap<V> = Map<string, Entry<V>>;
class BaseTrie<K, V> {
class BaseTrie<K: string, V> {
addressModule: AddressModule<K>;
entry: Entry<V>;