mirror of
https://github.com/status-im/sourcecred.git
synced 2025-02-17 23:16:23 +00:00
Make TimestampMs validation optional (#1789)
Currently, the Timestamp module requires that all TimestampMs types be validated via the `fromNumber` method. However, as discussed in #1788, this creates marginal benefit (just knowing that a TimestampMs is an integer does not give great confidence that it's a meaningful and correct timestamp for any use case), and creates a lot of overhead, especially in test code. It makes adopting the TimestampMs type across the codebase an arduous and mostly thankless task that would involve re-writing dozens or hundreds of test instances to add zero actual safety. Therefore, I've made the type non-opaque, and renamed `fromNumber` to `validate`. I then changed existing test code to not use the validator, since we don't need any manual validation of our test cases, and `123` is not a meaningful timestamp in any case :) Test plan: `yarn test`
This commit is contained in:
parent
96c34e3c6c
commit
34bb471d38
@ -9,7 +9,6 @@ import {NodeAddress} from "../core/graph";
|
|||||||
import type {Alias} from "../plugins/identity/alias";
|
import type {Alias} from "../plugins/identity/alias";
|
||||||
import type {PluginDeclaration} from "./pluginDeclaration";
|
import type {PluginDeclaration} from "./pluginDeclaration";
|
||||||
import type {TimestampMs} from "../util/timestamp";
|
import type {TimestampMs} from "../util/timestamp";
|
||||||
import * as Timestamp from "../util/timestamp";
|
|
||||||
import {TimelineCred} from "./timeline/timelineCred";
|
import {TimelineCred} from "./timeline/timelineCred";
|
||||||
import {nodeWeightEvaluator} from "../core/algorithm/weightEvaluator";
|
import {nodeWeightEvaluator} from "../core/algorithm/weightEvaluator";
|
||||||
|
|
||||||
@ -66,14 +65,12 @@ export function fromTimelineCredAndPlugins(
|
|||||||
// know what period to mint it in.
|
// know what period to mint it in.
|
||||||
// When we transition to CredRank, we should remove this check.
|
// When we transition to CredRank, we should remove this check.
|
||||||
const minted = timestampMs == null ? 0 : nodeEvaluator(address);
|
const minted = timestampMs == null ? 0 : nodeEvaluator(address);
|
||||||
const timestamp =
|
|
||||||
timestampMs == null ? null : Timestamp.fromNumber(timestampMs);
|
|
||||||
return {
|
return {
|
||||||
address: NodeAddress.toParts(address),
|
address: NodeAddress.toParts(address),
|
||||||
cred,
|
cred,
|
||||||
minted,
|
minted,
|
||||||
description,
|
description,
|
||||||
timestamp,
|
timestamp: timestampMs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -7,7 +7,6 @@ import {
|
|||||||
NodeAddress,
|
NodeAddress,
|
||||||
} from "../../core/graph";
|
} from "../../core/graph";
|
||||||
import * as Weights from "../../core/weights";
|
import * as Weights from "../../core/weights";
|
||||||
import * as Timestamp from "../../util/timestamp";
|
|
||||||
import sortBy from "../../util/sortBy";
|
import sortBy from "../../util/sortBy";
|
||||||
import type {ReferenceDetector, URL} from "../../core/references";
|
import type {ReferenceDetector, URL} from "../../core/references";
|
||||||
import {createWeightedGraph, initiativeWeight} from "./createGraph";
|
import {createWeightedGraph, initiativeWeight} from "./createGraph";
|
||||||
@ -30,7 +29,7 @@ import {
|
|||||||
const exampleEntry = (overrides: $Shape<NodeEntry>): NodeEntry => ({
|
const exampleEntry = (overrides: $Shape<NodeEntry>): NodeEntry => ({
|
||||||
key: overrides.title ? _titleSlug(overrides.title) : "sample-title",
|
key: overrides.title ? _titleSlug(overrides.title) : "sample-title",
|
||||||
title: "Sample title",
|
title: "Sample title",
|
||||||
timestampMs: Timestamp.fromNumber(123),
|
timestampMs: 123,
|
||||||
contributors: [],
|
contributors: [],
|
||||||
weight: 456,
|
weight: 456,
|
||||||
...overrides,
|
...overrides,
|
||||||
@ -40,7 +39,7 @@ function _createInitiative(overrides?: $Shape<Initiative>): Initiative {
|
|||||||
return {
|
return {
|
||||||
id: createId("UNSET_SUBTYPE", "42"),
|
id: createId("UNSET_SUBTYPE", "42"),
|
||||||
title: "Unset test initiative",
|
title: "Unset test initiative",
|
||||||
timestampMs: Timestamp.fromNumber(123),
|
timestampMs: 123,
|
||||||
completed: false,
|
completed: false,
|
||||||
contributions: {urls: [], entries: []},
|
contributions: {urls: [], entries: []},
|
||||||
dependencies: {urls: [], entries: []},
|
dependencies: {urls: [], entries: []},
|
||||||
@ -66,7 +65,7 @@ class MockInitiativeRepository implements InitiativeRepository {
|
|||||||
const initiative = _createInitiative({
|
const initiative = _createInitiative({
|
||||||
id: createId("TEST_SUBTYPE", String(num)),
|
id: createId("TEST_SUBTYPE", String(num)),
|
||||||
title: `Example Initiative ${num}`,
|
title: `Example Initiative ${num}`,
|
||||||
timestampMs: Timestamp.fromNumber(400 + num),
|
timestampMs: 400 + num,
|
||||||
...shape,
|
...shape,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import {type TimestampMs} from "../../util/timestamp";
|
import {type TimestampMs} from "../../util/timestamp";
|
||||||
import * as Timestamp from "../../util/timestamp";
|
|
||||||
import {type NodeEntry} from "./nodeEntry";
|
import {type NodeEntry} from "./nodeEntry";
|
||||||
import {
|
import {
|
||||||
type EdgeSpec,
|
type EdgeSpec,
|
||||||
@ -14,7 +13,7 @@ import {
|
|||||||
const exampleEntry = (overrides: $Shape<NodeEntry>): NodeEntry => ({
|
const exampleEntry = (overrides: $Shape<NodeEntry>): NodeEntry => ({
|
||||||
key: "sample-title",
|
key: "sample-title",
|
||||||
title: "Sample title",
|
title: "Sample title",
|
||||||
timestampMs: Timestamp.fromNumber(123),
|
timestampMs: 123,
|
||||||
contributors: [],
|
contributors: [],
|
||||||
weight: 456,
|
weight: 456,
|
||||||
...overrides,
|
...overrides,
|
||||||
@ -23,7 +22,7 @@ const exampleEntry = (overrides: $Shape<NodeEntry>): NodeEntry => ({
|
|||||||
describe("plugins/initiatives/edgeSpec", () => {
|
describe("plugins/initiatives/edgeSpec", () => {
|
||||||
describe("normalizeEdgeSpec", () => {
|
describe("normalizeEdgeSpec", () => {
|
||||||
it("should add empty arrays when missing", () => {
|
it("should add empty arrays when missing", () => {
|
||||||
const timestampMs: TimestampMs = Timestamp.fromNumber(321);
|
const timestampMs: TimestampMs = 321;
|
||||||
const json: EdgeSpecJson = {};
|
const json: EdgeSpecJson = {};
|
||||||
|
|
||||||
const actual = normalizeEdgeSpec(json, timestampMs);
|
const actual = normalizeEdgeSpec(json, timestampMs);
|
||||||
@ -31,7 +30,7 @@ describe("plugins/initiatives/edgeSpec", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should preserve urls when present", () => {
|
it("should preserve urls when present", () => {
|
||||||
const timestampMs: TimestampMs = Timestamp.fromNumber(321);
|
const timestampMs: TimestampMs = 321;
|
||||||
const json: EdgeSpecJson = {
|
const json: EdgeSpecJson = {
|
||||||
urls: ["https://foo.bar/a", "https://foo.bar/b"],
|
urls: ["https://foo.bar/a", "https://foo.bar/b"],
|
||||||
};
|
};
|
||||||
@ -41,7 +40,7 @@ describe("plugins/initiatives/edgeSpec", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should normalize any entries from json format", () => {
|
it("should normalize any entries from json format", () => {
|
||||||
const timestampMs: TimestampMs = Timestamp.fromNumber(321);
|
const timestampMs: TimestampMs = 321;
|
||||||
const json: EdgeSpecJson = {
|
const json: EdgeSpecJson = {
|
||||||
entries: [{title: "Minimal example"}],
|
entries: [{title: "Minimal example"}],
|
||||||
};
|
};
|
||||||
@ -62,7 +61,7 @@ describe("plugins/initiatives/edgeSpec", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should throw on entries with the same key", () => {
|
it("should throw on entries with the same key", () => {
|
||||||
const timestampMs: TimestampMs = Timestamp.fromNumber(321);
|
const timestampMs: TimestampMs = 321;
|
||||||
const json: EdgeSpecJson = {
|
const json: EdgeSpecJson = {
|
||||||
entries: [
|
entries: [
|
||||||
{key: "1", title: "first"},
|
{key: "1", title: "first"},
|
||||||
|
@ -52,14 +52,14 @@ describe("plugins/initiatives/nodeEntry", () => {
|
|||||||
|
|
||||||
describe("normalizeNodeEntry", () => {
|
describe("normalizeNodeEntry", () => {
|
||||||
it("should throw without a title", () => {
|
it("should throw without a title", () => {
|
||||||
const timestampMs: TimestampMs = Timestamp.fromNumber(123);
|
const timestampMs: TimestampMs = 123;
|
||||||
const entry: NodeEntryJson = {key: "no-title"};
|
const entry: NodeEntryJson = {key: "no-title"};
|
||||||
const f = () => normalizeNodeEntry(entry, timestampMs);
|
const f = () => normalizeNodeEntry(entry, timestampMs);
|
||||||
expect(f).toThrow(TypeError);
|
expect(f).toThrow(TypeError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle a minimal entry", () => {
|
it("should handle a minimal entry", () => {
|
||||||
const timestampMs: TimestampMs = Timestamp.fromNumber(123);
|
const timestampMs = 123;
|
||||||
const entry: NodeEntryJson = {title: "Most minimal"};
|
const entry: NodeEntryJson = {title: "Most minimal"};
|
||||||
const expected: NodeEntry = {
|
const expected: NodeEntry = {
|
||||||
title: "Most minimal",
|
title: "Most minimal",
|
||||||
@ -72,7 +72,7 @@ describe("plugins/initiatives/nodeEntry", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should handle an entry with weights", () => {
|
it("should handle an entry with weights", () => {
|
||||||
const timestampMs: TimestampMs = Timestamp.fromNumber(123);
|
const timestampMs: TimestampMs = 123;
|
||||||
const entry: NodeEntryJson = {title: "Include weight", weight: 42};
|
const entry: NodeEntryJson = {title: "Include weight", weight: 42};
|
||||||
const expected: NodeEntry = {
|
const expected: NodeEntry = {
|
||||||
title: "Include weight",
|
title: "Include weight",
|
||||||
@ -85,7 +85,7 @@ describe("plugins/initiatives/nodeEntry", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should handle an entry with contributors", () => {
|
it("should handle an entry with contributors", () => {
|
||||||
const timestampMs: TimestampMs = Timestamp.fromNumber(123);
|
const timestampMs: TimestampMs = 123;
|
||||||
const entry: NodeEntryJson = {
|
const entry: NodeEntryJson = {
|
||||||
title: "Include contributors",
|
title: "Include contributors",
|
||||||
contributors: ["https://foo.bar/u/abc"],
|
contributors: ["https://foo.bar/u/abc"],
|
||||||
@ -101,7 +101,7 @@ describe("plugins/initiatives/nodeEntry", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should handle an entry with timestamp", () => {
|
it("should handle an entry with timestamp", () => {
|
||||||
const timestampMs: TimestampMs = Timestamp.fromNumber(123);
|
const timestampMs: TimestampMs = 123;
|
||||||
const entry: NodeEntryJson = {
|
const entry: NodeEntryJson = {
|
||||||
title: "Include timestamp",
|
title: "Include timestamp",
|
||||||
timestampIso: Timestamp.toISO(Date.parse("2018-02-03T12:34:56.789Z")),
|
timestampIso: Timestamp.toISO(Date.parse("2018-02-03T12:34:56.789Z")),
|
||||||
@ -110,16 +110,14 @@ describe("plugins/initiatives/nodeEntry", () => {
|
|||||||
title: "Include timestamp",
|
title: "Include timestamp",
|
||||||
key: "include-timestamp",
|
key: "include-timestamp",
|
||||||
contributors: [],
|
contributors: [],
|
||||||
timestampMs: Timestamp.fromNumber(
|
timestampMs: Date.parse("2018-02-03T12:34:56.789Z"),
|
||||||
Date.parse("2018-02-03T12:34:56.789Z")
|
|
||||||
),
|
|
||||||
weight: null,
|
weight: null,
|
||||||
};
|
};
|
||||||
expect(normalizeNodeEntry(entry, timestampMs)).toEqual(expected);
|
expect(normalizeNodeEntry(entry, timestampMs)).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle an entry with key", () => {
|
it("should handle an entry with key", () => {
|
||||||
const timestampMs: TimestampMs = Timestamp.fromNumber(123);
|
const timestampMs: TimestampMs = 123;
|
||||||
const entry: NodeEntryJson = {
|
const entry: NodeEntryJson = {
|
||||||
title: "Include key",
|
title: "Include key",
|
||||||
key: "much-different-key",
|
key: "much-different-key",
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// A timestamp representation in ms since epoch.
|
// A timestamp representation in ms since epoch.
|
||||||
export opaque type TimestampMs: number = number;
|
export type TimestampMs = number;
|
||||||
|
|
||||||
// A timestamp representation in ISO 8601 format.
|
// A timestamp representation in ISO 8601 format.
|
||||||
export opaque type TimestampISO: string = string;
|
export opaque type TimestampISO: string = string;
|
||||||
@ -22,7 +22,7 @@ export opaque type TimestampISO: string = string;
|
|||||||
* than a forced refactor across the codebase.
|
* than a forced refactor across the codebase.
|
||||||
*/
|
*/
|
||||||
export function toISO(timestampLike: TimestampMs | number): TimestampISO {
|
export function toISO(timestampLike: TimestampMs | number): TimestampISO {
|
||||||
const timestampMs: TimestampMs = fromNumber(timestampLike);
|
const timestampMs: TimestampMs = validate(timestampLike);
|
||||||
return new Date(timestampMs).toISOString();
|
return new Date(timestampMs).toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,13 +47,12 @@ export function fromISO(timestampISO: TimestampISO): TimestampMs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a TimestampMs from a number input.
|
* Validate that a number is potentially a valid timestamp.
|
||||||
*
|
*
|
||||||
* Since much of the previous types have used `number` as a type instead of
|
* This checks that the number is a finite integer, which avoids some potential
|
||||||
* TimestampMs. Accepting `number` will give an easier upgrade path, rather
|
* numbers that are not valid timestamps.
|
||||||
* than a forced refactor across the codebase.
|
|
||||||
*/
|
*/
|
||||||
export function fromNumber(timestampMs: number): TimestampMs {
|
export function validate(timestampMs: number): TimestampMs {
|
||||||
const asNumber = Number(timestampMs);
|
const asNumber = Number(timestampMs);
|
||||||
if (
|
if (
|
||||||
timestampMs === null ||
|
timestampMs === null ||
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import type {TimestampMs, TimestampISO} from "./timestamp";
|
import type {TimestampMs, TimestampISO} from "./timestamp";
|
||||||
import {fromISO, toISO, fromNumber} from "./timestamp";
|
import {fromISO, toISO, validate} from "./timestamp";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to write readable "toThrow" tests. This intentionally
|
* Helper function to write readable "toThrow" tests. This intentionally
|
||||||
@ -33,14 +33,14 @@ describe("util/timestamp", () => {
|
|||||||
describe("roundtrips", () => {
|
describe("roundtrips", () => {
|
||||||
it("should handle number roundtrips", () => {
|
it("should handle number roundtrips", () => {
|
||||||
const origin: number = fullExample.ms;
|
const origin: number = fullExample.ms;
|
||||||
expect(fromNumber(origin)).toBe(origin);
|
expect(validate(origin)).toBe(origin);
|
||||||
expect(fromISO(toISO(origin))).toBe(origin);
|
expect(fromISO(toISO(origin))).toBe(origin);
|
||||||
expect(fromNumber(fromISO(toISO(origin)))).toBe(origin);
|
expect(validate(fromISO(toISO(origin)))).toBe(origin);
|
||||||
});
|
});
|
||||||
it("should handle ISO roundtrips", () => {
|
it("should handle ISO roundtrips", () => {
|
||||||
const origin: TimestampISO = fullExample.ISO;
|
const origin: TimestampISO = fullExample.ISO;
|
||||||
expect(toISO(fromISO(origin))).toBe(origin);
|
expect(toISO(fromISO(origin))).toBe(origin);
|
||||||
expect(toISO(fromNumber(fromISO(origin)))).toBe(origin);
|
expect(toISO(validate(fromISO(origin)))).toBe(origin);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -96,34 +96,34 @@ describe("util/timestamp", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("fromNumber", () => {
|
describe("validate", () => {
|
||||||
it("should throw on null", () => {
|
it("should throw on null", () => {
|
||||||
given(null).expect(fromNumber).toThrow(TypeError);
|
given(null).expect(validate).toThrow(TypeError);
|
||||||
});
|
});
|
||||||
it("should throw on undefined", () => {
|
it("should throw on undefined", () => {
|
||||||
given(undefined).expect(fromNumber).toThrow(TypeError);
|
given(undefined).expect(validate).toThrow(TypeError);
|
||||||
});
|
});
|
||||||
it("should throw on NaN", () => {
|
it("should throw on NaN", () => {
|
||||||
given(NaN).expect(fromNumber).toThrow(TypeError);
|
given(NaN).expect(validate).toThrow(TypeError);
|
||||||
});
|
});
|
||||||
it("should throw on Infinity", () => {
|
it("should throw on Infinity", () => {
|
||||||
given(Infinity).expect(fromNumber).toThrow(TypeError);
|
given(Infinity).expect(validate).toThrow(TypeError);
|
||||||
});
|
});
|
||||||
it("should throw on floating point numbers", () => {
|
it("should throw on floating point numbers", () => {
|
||||||
given(1 / 3)
|
given(1 / 3)
|
||||||
.expect(fromNumber)
|
.expect(validate)
|
||||||
.toThrow(TypeError);
|
.toThrow(TypeError);
|
||||||
});
|
});
|
||||||
it("should handle 0 correctly", () => {
|
it("should handle 0 correctly", () => {
|
||||||
expect(fromNumber(0)).toBe(0);
|
expect(validate(0)).toBe(0);
|
||||||
});
|
});
|
||||||
it("should handle Date.now correctly", () => {
|
it("should handle Date.now correctly", () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
expect(fromNumber(now)).toBe(now);
|
expect(validate(now)).toBe(now);
|
||||||
});
|
});
|
||||||
it("should handle examples correctly", () => {
|
it("should handle examples correctly", () => {
|
||||||
expect(fromNumber(fullExample.ms)).toBe(fullExample.ms);
|
expect(validate(fullExample.ms)).toBe(fullExample.ms);
|
||||||
expect(fromNumber(partialExample.ms)).toBe(partialExample.ms);
|
expect(validate(partialExample.ms)).toBe(partialExample.ms);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user