mirror of
https://github.com/status-im/sourcecred.git
synced 2025-02-17 06:56:36 +00:00
Add foundations to unify address implementations (#355)
Summary: We have `NodeAddress` and `EdgeAddress`, which are opaque aliases of `string` each with separate associated functions. We really want to keep this setup: having the address types be structurally distinct is very nice. But currently the implementation is extremely repetitive. Core functionality is implemented twice, once for nodes and once for edges. The test code is even worse, as it is forced to include ugly, hacky, parametric generalizations to test both implementations—which are really the same code, anyway! In this commit, we introduce a system to unify the _implementations_ while keeping the _APIs_ separate. That is, users still see separate opaque types `NodeAddressT` and `EdgeAddressT`. Users now also see separate modules `NodeAddress` and `EdgeAddress`, each of which implements the same interface for its appropriate type. These modules are each implemented by the same address module factory. To get this to work, we clearly need to parameterize the module type over the address type. The problem is getting this to work in a way that interacts nicely with the opaque types. The trick is to let the factory always return a transparent module at type `string`, but to then specialize the type of the resulting module in the module in which the underlying type of the opaque type is known. This commit includes specifications for all functions that are in the current version of the API, but includes only as much implementation code as is needed to convince me that tests and Flow are actually running (i.e., very little). I’ll send implementations in separate PRs for easier review. The preliminary modules created in this commit _are_ exported from the graph module, even though they are incomplete. This is so that we can be sure that nothing will catch fire in Flow-land when we try to export them (which is plausible, given that we have nontrivial interactions between opaque types and parametric polymorphism). Test Plan: Unit tests included. Run `yarn travis`. wchargin-branch: address-unified-foundations
This commit is contained in:
parent
08cb60e762
commit
fc7e8886b1
142
src/v3/core/address.js
Normal file
142
src/v3/core/address.js
Normal file
@ -0,0 +1,142 @@
|
||||
// @flow
|
||||
|
||||
export interface AddressModule<Address> {
|
||||
/**
|
||||
* 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
|
||||
* provided, it will be included in the error message.
|
||||
*/
|
||||
assertValid(address: Address, what?: string): void;
|
||||
|
||||
/**
|
||||
* Assert at runtime that the provided array is a valid array of
|
||||
* address parts (i.e., a valid input to `fromParts`), throwing an
|
||||
* error if it is not. If `what` is provided, it will be included in
|
||||
* the error message.
|
||||
*/
|
||||
assertValidParts(parts: $ReadOnlyArray<string>, what?: string): void;
|
||||
|
||||
/**
|
||||
* Convert an array of address parts to an address. The input must be
|
||||
* a non-null array of non-null strings, none of which contains the
|
||||
* NUL character. This is the inverse of `toParts`.
|
||||
*/
|
||||
fromParts(parts: $ReadOnlyArray<string>): Address;
|
||||
|
||||
/**
|
||||
* Convert an address to the array of parts that it represents. This
|
||||
* is the inverse of `fromParts`.
|
||||
*/
|
||||
toParts(address: Address): string[];
|
||||
|
||||
/**
|
||||
* Pretty-print an address. The result will be human-readable and
|
||||
* contain only printable characters. Clients should not make any
|
||||
* assumptions about the format.
|
||||
*/
|
||||
toString(address: Address): string;
|
||||
|
||||
/**
|
||||
* Construct an address by extending the given address with the given
|
||||
* additional components. This function is equivalent to:
|
||||
*
|
||||
* return fromParts([...toParts(address), ...components]);
|
||||
*
|
||||
* but may be more efficient.
|
||||
*/
|
||||
append(address: Address, ...components: string[]): Address;
|
||||
|
||||
/**
|
||||
* Test whether the given address has the given prefix. This function
|
||||
* is equivalent to:
|
||||
*
|
||||
* const prefixParts = toParts(prefix);
|
||||
* const addressParts = toParts(address);
|
||||
* const actualPrefix = addressParts.slice(0, prefixParts.length);
|
||||
* return deepEqual(prefix, actualPrefix);
|
||||
*
|
||||
* (where `deepEqual` checks value equality on arrays of strings), but
|
||||
* may be more efficient.
|
||||
*
|
||||
* Note that this is an array-wise prefix, not a string-wise-prefix:
|
||||
* e.g., `toParts(["ban"])` is not a prefix of `toParts(["banana"])`.
|
||||
*/
|
||||
hasPrefix(address: Address, prefix: Address): boolean;
|
||||
}
|
||||
|
||||
export type Options = {|
|
||||
/**
|
||||
* The name of this kind of address, like `NodeAddress`.
|
||||
*/
|
||||
+name: string,
|
||||
|
||||
/**
|
||||
* A unique nonce for the runtime representation of this address. For
|
||||
* compact serialization, this should be short; a single letter
|
||||
* suffices.
|
||||
*/
|
||||
+nonce: string,
|
||||
|
||||
/**
|
||||
* For the purposes of nice error messages: in response to an address
|
||||
* of the wrong kind, we can inform the user what kind of address they
|
||||
* passed (e.g., "expected NodeAddress, got EdgeAddress"). This
|
||||
* dictionary maps another address module's nonce to the name of that
|
||||
* module.
|
||||
*/
|
||||
+otherNonces?: Map<string, string>,
|
||||
|};
|
||||
|
||||
export function makeAddressModule(options: Options): AddressModule<string> {
|
||||
type Address = string; // for readability and interface consistency
|
||||
const _ = options;
|
||||
|
||||
function assertValid(address: Address, what?: string): void {
|
||||
const _ = {address, what};
|
||||
throw new Error("assertValid");
|
||||
}
|
||||
|
||||
function assertValidParts(
|
||||
parts: $ReadOnlyArray<string>,
|
||||
what?: string
|
||||
): void {
|
||||
const _ = {parts, what};
|
||||
throw new Error("assertValidParts");
|
||||
}
|
||||
|
||||
function fromParts(parts: $ReadOnlyArray<string>): Address {
|
||||
const _ = parts;
|
||||
throw new Error("fromParts");
|
||||
}
|
||||
|
||||
function toParts(address: Address): string[] {
|
||||
const _ = address;
|
||||
throw new Error("toParts");
|
||||
}
|
||||
|
||||
function toString(address: Address): string {
|
||||
const _ = address;
|
||||
throw new Error("toString");
|
||||
}
|
||||
|
||||
function append(address: Address, ...parts: string[]): Address {
|
||||
const _ = {address, parts};
|
||||
throw new Error("append");
|
||||
}
|
||||
|
||||
function hasPrefix(address: Address, prefix: Address): boolean {
|
||||
const _ = {address, prefix};
|
||||
throw new Error("hasPrefix");
|
||||
}
|
||||
|
||||
const result = {
|
||||
assertValid,
|
||||
assertValidParts,
|
||||
fromParts,
|
||||
toParts,
|
||||
toString,
|
||||
append,
|
||||
hasPrefix,
|
||||
};
|
||||
return Object.freeze(result);
|
||||
}
|
39
src/v3/core/address.test.js
Normal file
39
src/v3/core/address.test.js
Normal file
@ -0,0 +1,39 @@
|
||||
// @flow
|
||||
|
||||
import {makeAddressModule} from "./address";
|
||||
|
||||
describe("core/address", () => {
|
||||
describe("makeAddressModule", () => {
|
||||
const makeModules = () => ({
|
||||
FooAddress: makeAddressModule({
|
||||
name: "FooAddress",
|
||||
nonce: "F",
|
||||
otherNonces: new Map().set("B", "BarAddress"),
|
||||
}),
|
||||
BarAddress: makeAddressModule({
|
||||
name: "BarAddress",
|
||||
nonce: "B",
|
||||
otherNonces: new Map().set("F", "FooAddress"),
|
||||
}),
|
||||
WatAddress: makeAddressModule({
|
||||
name: "WatAddress",
|
||||
nonce: "W",
|
||||
otherNonces: new Map(),
|
||||
}),
|
||||
});
|
||||
|
||||
it("makes an address module given the mandatory options", () => {
|
||||
makeAddressModule({name: "FooAddress", nonce: "F"});
|
||||
});
|
||||
it("makes address modules using all the options", () => {
|
||||
makeModules();
|
||||
});
|
||||
it("returns an object with read-only properties", () => {
|
||||
const {FooAddress} = makeModules();
|
||||
expect(() => {
|
||||
// $ExpectFlowError
|
||||
FooAddress.assertValid = FooAddress.assertValid;
|
||||
}).toThrow(/read.only property/);
|
||||
});
|
||||
});
|
||||
});
|
@ -2,8 +2,25 @@
|
||||
|
||||
import type {NodeAddress, EdgeAddress} from "./_address";
|
||||
import * as Address from "./_address";
|
||||
import {makeAddressModule, type AddressModule} from "./address";
|
||||
|
||||
export type {NodeAddress, EdgeAddress} from "./_address";
|
||||
|
||||
// New-style node and edge address types and modules. Will be made
|
||||
// public once implementation is complete.
|
||||
export opaque type _NodeAddressT: string = string;
|
||||
export opaque type _EdgeAddressT: string = string;
|
||||
export const _NodeAddress: AddressModule<_NodeAddressT> = (makeAddressModule({
|
||||
name: "NodeAddress",
|
||||
nonce: "N",
|
||||
otherNonces: new Map().set("E", "EdgeAddress"),
|
||||
}): AddressModule<string>);
|
||||
export const _EdgeAddress: AddressModule<_EdgeAddressT> = (makeAddressModule({
|
||||
name: "EdgeAddress",
|
||||
nonce: "E",
|
||||
otherNonces: new Map().set("N", "NodeAddress"),
|
||||
}): AddressModule<string>);
|
||||
|
||||
Object.freeze(Address);
|
||||
export {Address};
|
||||
|
||||
|
@ -1,7 +1,15 @@
|
||||
// @flow
|
||||
|
||||
import {Address, Direction, Graph, edgeToString} from "./graph";
|
||||
import type {NodeAddress, EdgeAddress} from "./graph";
|
||||
import {
|
||||
type EdgeAddress,
|
||||
type NodeAddress,
|
||||
type _EdgeAddressT,
|
||||
type _NodeAddressT,
|
||||
Address,
|
||||
Direction,
|
||||
Graph,
|
||||
edgeToString,
|
||||
} from "./graph";
|
||||
|
||||
describe("core/graph", () => {
|
||||
const {nodeAddress, edgeAddress} = Address;
|
||||
@ -30,6 +38,13 @@ describe("core/graph", () => {
|
||||
});
|
||||
});
|
||||
|
||||
function _unused_itExportsDistinctNodeAddressAndEdgeAddressTypes() {
|
||||
// $ExpectFlowError
|
||||
const _unused_nodeToEdge = (x: _NodeAddressT): _EdgeAddressT => x;
|
||||
// $ExpectFlowError
|
||||
const _unused_edgeToNode = (x: _EdgeAddressT): _NodeAddressT => x;
|
||||
}
|
||||
|
||||
describe("Direction values", () => {
|
||||
it("are read-only", () => {
|
||||
expect(() => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user