# core/converters/route.converter.ts

> 70 lines of code and 20 definitions.

Tree: Bane's Lab Build Scripts
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/build-scripts#file-build-scripts-core-converters-route-converter-ts
Source text: https://banes-lab.com/source/build-scripts/core/converters/route.converter.ts.txt

Listed in [core/converters](https://banes-lab.com/api/source/build-scripts/core/converters.md), after [core/converters/report.converter.ts](https://banes-lab.com/source/build-scripts/core/converters/report.converter.ts.md) and before [core/converters/schema.converter.ts](https://banes-lab.com/source/build-scripts/core/converters/schema.converter.ts.md).

## Definitions

- `hasText` (lexical_declaration, line 11)
- `latestStamp` (lexical_declaration, line 42, exported)
- `fingerprint` (lexical_declaration, line 70, exported)
- `isStamp` (lexical_declaration, line 19)
- `parseLedger` (lexical_declaration, line 28, exported)
- `hasOptionalText` (lexical_declaration, line 15)
- `datedRoutes` (lexical_declaration, line 51, exported)
- `routeFingerprint` (lexical_declaration, line 47, exported)
- `SAMPLE_LENGTH` (lexical_declaration, line 9)
- `parsed` (lexical_declaration, line 29, exported)
- `malformed` (lexical_declaration, line 33, exported)
- `stamp` (lexical_declaration, line 56, exported)
- `stampRoutes` (lexical_declaration, line 64, exported)
- `today` (lexical_declaration, line 65, exported)
- `next` (lexical_declaration, line 66, exported)
- `stamps` (lexical_declaration, line 67, exported)
- `dates` (lexical_declaration, line 68, exported)
- `held` (lexical_declaration, line 71, exported)
- `lastmod` (lexical_declaration, line 72, exported)
- `published` (lexical_declaration, line 73, exported)

## Contained in

- [core/converters](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-converters.md)

## Uses

- [configuration/strings/route.strings.ts](https://banes-lab.com/source/build-scripts/configuration/strings/route.strings.ts.md)
- [core/formatters/site.formatter.ts](https://banes-lab.com/source/build-scripts/core/formatters/site.formatter.ts.md)

## Used by

- [core/converters/catalog.converter.ts](https://banes-lab.com/source/build-scripts/core/converters/catalog.converter.ts.md)
- [core/coordinators/chapter.coordinator.ts](https://banes-lab.com/source/build-scripts/core/coordinators/chapter.coordinator.ts.md)
- [core/persistence/page.persistence.ts](https://banes-lab.com/source/build-scripts/core/persistence/page.persistence.ts.md)
- [core/persistence/route.persistence.ts](https://banes-lab.com/source/build-scripts/core/persistence/route.persistence.ts.md)

## Linked from

- [configuration/strings](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-configuration-strings.md)
- [core/converters](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-converters.md)
- [core/coordinators](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-coordinators.md)
- [core/formatters](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-formatters.md)
- [core/persistence](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-persistence.md)

## Source

```typescript
import type { DiscoveredRoute, Discovery } from "#types/site.types";
import type { HeldStamp, RouteDates, RouteLedger, RouteStamp, StampedRoutes } from "#types/route.types";
import { isoDate, renderRoutePayload } from "#core/formatters/site.formatter";
import { ledgerNotObject, malformedStamps, undatedRoute } from "#configuration/strings/route.strings";
import type { PageRoute } from "#types/page.types";
import { fingerprintOf } from "@govlab/content-fingerprint";
import { isRecord } from "#core/selectors/base.selector";

const SAMPLE_LENGTH = 80;

const hasText = function hasText(value: object, key: string): boolean {
    return key in value && typeof Reflect.get(value, key) === "string";
};

const hasOptionalText = function hasOptionalText(value: object, key: string): boolean {
    return !(key in value) || hasText(value, key);
};

const isStamp = function isStamp(value: unknown): value is HeldStamp {
    return (
        isRecord(value) &&
        hasText(value, "fingerprint") &&
        hasText(value, "lastmod") &&
        hasOptionalText(value, "published")
    );
};

export const parseLedger = function parseLedger(text: string): RouteLedger {
    const parsed: unknown = JSON.parse(text);
    if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
        throw new TypeError(ledgerNotObject(text.slice(0, SAMPLE_LENGTH)));
    }
    const malformed = Object.entries(parsed).filter((entry) => !isStamp(entry[1]));
    if (malformed.length > 0) {
        throw new TypeError(malformedStamps(malformed.map(([path]) => path)));
    }
    return Object.fromEntries(
        Object.entries(parsed).filter((entry): entry is [string, HeldStamp] => isStamp(entry[1])),
    );
};

export const latestStamp = function latestStamp(ledger: RouteLedger): string | null {
    const dates = Object.values(ledger).flatMap((stamp) => (stamp === undefined ? [] : [stamp.lastmod]));
    return dates.toSorted((left, right) => left.localeCompare(right)).at(-1) ?? null;
};

export const routeFingerprint = function routeFingerprint(discovery: Discovery, route: DiscoveredRoute): string {
    return fingerprintOf([route.title, route.description, route.markdown, renderRoutePayload(discovery, route)]);
};

export const datedRoutes = function datedRoutes(
    routes: readonly PageRoute[],
    dates: StampedRoutes["dates"],
): readonly PageRoute[] {
    return routes.map((route) => {
        const stamp = dates.get(route.path);
        if (stamp === undefined) {
            throw new Error(undatedRoute(route.path));
        }
        return { ...route, dates: { modified: stamp.lastmod, published: stamp.published } };
    });
};

export const stampRoutes = function stampRoutes(ledger: RouteLedger, discovery: Discovery, moment: Date): StampedRoutes {
    const today = isoDate(moment);
    const next: Record<string, RouteStamp> = {};
    const stamps = new Map<string, string>();
    const dates = new Map<string, RouteDates>();
    for (const route of discovery.routes) {
        const fingerprint = routeFingerprint(discovery, route);
        const held = ledger[route.path];
        const lastmod = held?.fingerprint === fingerprint ? held.lastmod : today;
        const published = held?.published ?? held?.lastmod ?? today;
        next[route.path] = { fingerprint, lastmod, published };
        stamps.set(route.path, lastmod);
        dates.set(route.path, { lastmod, published });
    }
    return { dates, ledger: next, stamps };
};
```
