From e6a988b1a536dae7d4b85ca1e9d0000b689dfa82 Mon Sep 17 00:00:00 2001 From: Robin van Boven <497556+Beanow@users.noreply.github.com> Date: Thu, 23 Apr 2020 16:12:26 +0200 Subject: [PATCH] Initiatives: add normalizeNodeEntry implementation (#1754) This is where most flexibility when hand-writing JSON files is expected to come from. As it makes few assumptions about the formatting but the internal normalized type is still consistent. --- src/plugins/initiatives/nodeEntry.js | 46 +++++++++ src/plugins/initiatives/nodeEntry.test.js | 110 ++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 src/plugins/initiatives/nodeEntry.test.js diff --git a/src/plugins/initiatives/nodeEntry.js b/src/plugins/initiatives/nodeEntry.js index 4ad2e2e..a620aa0 100644 --- a/src/plugins/initiatives/nodeEntry.js +++ b/src/plugins/initiatives/nodeEntry.js @@ -3,6 +3,7 @@ import {type URL} from "../../core/references"; import {type NodeWeight} from "../../core/weights"; import {type TimestampMs, type TimestampISO} from "../../util/timestamp"; +import * as Timestamp from "../../util/timestamp"; /** * Represents an "inline contribution" node. They're called entries and named @@ -38,8 +39,53 @@ export type NodeEntryJson = $Shape<{ // Key defaults to a url-friendly-slug of the title. Override it if you need // to preserve a specific NodeAddress, or the slug produces duplicate keys. +key: string, + // Defaults to an empty array. +contributors: $ReadOnlyArray, // Timestamp of this node, but in ISO format as it's more human friendly. +timestampIso: TimestampISO, + // Defaults to null. +weight: NodeWeight | null, }>; + +/** + * Takes a NodeEntryJson and normalizes it to a NodeEntry. + * + * Will throw when required fields are missing. Otherwise handles default + * values and converting ISO timestamps. + */ +export function normalizeNodeEntry( + input: NodeEntryJson, + defaultTimestampMs: TimestampMs +): NodeEntry { + if (!input.title) { + throw new TypeError( + `Title is required for an entry, received ${JSON.stringify(input)}` + ); + } + + return { + key: input.key || _titleSlug(input.title), + title: input.title, + timestampMs: input.timestampIso + ? Timestamp.fromISO(input.timestampIso) + : defaultTimestampMs, + contributors: input.contributors || [], + weight: input.weight || null, + }; +} + +/** + * Creates a url-friendly-slug from the title of a NodeEntry. Useful for + * generating a default key. + * + * Note: keys are not required to meet the formatting rules of this slug, + * this is mostly for predictability and convenience of NodeAddresses. + */ +export function _titleSlug(title: string): string { + return String(title) + .toLowerCase() + .replace(/[^a-z0-9-_]+/g, "-") + .replace(/--+/g, "-") + .replace(/^-/, "") + .replace(/-$/, ""); +} diff --git a/src/plugins/initiatives/nodeEntry.test.js b/src/plugins/initiatives/nodeEntry.test.js new file mode 100644 index 0000000..8c969e3 --- /dev/null +++ b/src/plugins/initiatives/nodeEntry.test.js @@ -0,0 +1,110 @@ +// @flow + +import {type TimestampMs} from "../../util/timestamp"; +import * as Timestamp from "../../util/timestamp"; +import { + type NodeEntry, + type NodeEntryJson, + normalizeNodeEntry, + _titleSlug, +} from "./nodeEntry"; + +describe("plugins/initiatives/nodeEntry", () => { + describe("normalizeNodeEntry", () => { + it("should throw without a title", () => { + const timestampMs: TimestampMs = Timestamp.fromNumber(123); + const entry: NodeEntryJson = {key: "no-title"}; + const f = () => normalizeNodeEntry(entry, timestampMs); + expect(f).toThrow(TypeError); + }); + + it("should handle a minimal entry", () => { + const timestampMs: TimestampMs = Timestamp.fromNumber(123); + const entry: NodeEntryJson = {title: "Most minimal"}; + const expected: NodeEntry = { + title: "Most minimal", + key: "most-minimal", + contributors: [], + timestampMs, + weight: null, + }; + expect(normalizeNodeEntry(entry, timestampMs)).toEqual(expected); + }); + + it("should handle an entry with weights", () => { + const timestampMs: TimestampMs = Timestamp.fromNumber(123); + const entry: NodeEntryJson = {title: "Include weight", weight: 42}; + const expected: NodeEntry = { + title: "Include weight", + key: "include-weight", + contributors: [], + timestampMs, + weight: 42, + }; + expect(normalizeNodeEntry(entry, timestampMs)).toEqual(expected); + }); + + it("should handle an entry with contributors", () => { + const timestampMs: TimestampMs = Timestamp.fromNumber(123); + const entry: NodeEntryJson = { + title: "Include contributors", + contributors: ["https://foo.bar/u/abc"], + }; + const expected: NodeEntry = { + title: "Include contributors", + key: "include-contributors", + contributors: ["https://foo.bar/u/abc"], + timestampMs, + weight: null, + }; + expect(normalizeNodeEntry(entry, timestampMs)).toEqual(expected); + }); + + it("should handle an entry with timestamp", () => { + const timestampMs: TimestampMs = Timestamp.fromNumber(123); + const entry: NodeEntryJson = { + title: "Include timestamp", + timestampIso: Timestamp.toISO(Date.parse("2018-02-03T12:34:56.789Z")), + }; + const expected: NodeEntry = { + title: "Include timestamp", + key: "include-timestamp", + contributors: [], + timestampMs: Timestamp.fromNumber( + Date.parse("2018-02-03T12:34:56.789Z") + ), + weight: null, + }; + expect(normalizeNodeEntry(entry, timestampMs)).toEqual(expected); + }); + + it("should handle an entry with key", () => { + const timestampMs: TimestampMs = Timestamp.fromNumber(123); + const entry: NodeEntryJson = { + title: "Include key", + key: "much-different-key", + }; + const expected: NodeEntry = { + title: "Include key", + key: "much-different-key", + contributors: [], + timestampMs, + weight: null, + }; + expect(normalizeNodeEntry(entry, timestampMs)).toEqual(expected); + }); + }); + + describe("_titleSlug", () => { + it("should handle example titles", () => { + const expected: {[string]: string} = { + "should-be-lowercased": "Should-be-LowerCased", + "special-characters-as-dashes": "Special@$Characters #As$dashes", + "no-starting-trailing-dashes": "-No starting / trailing dashes-", + "no-duplicate-dashes": "No - Duplicate -%- Dashes", + }; + const actual = Object.keys(expected).map((k) => _titleSlug(expected[k])); + expect(actual).toEqual(Object.keys(expected)); + }); + }); +});