Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .changeset/calendar-readme-schema-keys-5045.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
'@object-ui/plugin-calendar': patch
---

`README.md`'s "Schema API / CalendarView" block described a `CalendarViewSchema`
that does not exist. Measured against the interface itself
(`packages/types/src/complex.ts`) and its zod mirror: `events` — the schema's
only required key besides `type` — was published as `events?`, so a reader
following the README omits it and TypeScript rejects the node; `defaultDate` was
`string` where the schema says `string | Date`; and `onDateClick` was listed as a
schema key when it is a `CalendarViewProps` **component** prop, sending readers
to a different package's surface for a key `calendar-view` does not have (the
schema's key is `onDateChange`). The block also listed 6 of the schema's 13 keys
with nothing saying it was a summary (objectui#5045).

The block now carries the requiredness the schema declares, names itself a
partial summary of `CalendarViewSchema`, and adds the author-facing
`defaultView` / `view` / `views` / `editable` / `date`. It also states plainly
what the registered `calendar-view` renderer actually reads — it builds events
from the node's `data` array and drops an authored `events` key (objectui#4433) —
so the corrected requiredness does not itself become a new wrong instruction.

This is a documentation fix to a file `plugin-calendar` publishes to npm, which
is why it carries a version: the npm landing page only picks up the correction
on a release. No behaviour, export, type, or `dist` byte changes. The pin test
added alongside it publishes nothing.
43 changes: 34 additions & 9 deletions packages/plugin-calendar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,19 +148,44 @@ ComponentRegistry.register('my-calendar', ObjectCalendarRenderer);

### CalendarView

Display a monthly calendar with events:
Display a monthly calendar with events. This is a **partial summary**: the full
contract is `CalendarViewSchema` in `@object-ui/types`, which declares 13 keys of
its own on top of the common `BaseSchema` keys (`className`, `id`, `data`,
`visible`, ...).

```typescript
{
type: 'calendar-view',
events?: CalendarEvent[],
defaultDate?: string, // ISO date string
onEventClick?: (event) => void,
onDateClick?: (date) => void,
className?: string
events: CalendarEvent[], // REQUIRED — the only required key besides `type`
defaultView?: CalendarViewMode, // 'month' | 'week' | 'day' | 'agenda' (default 'month')
view?: CalendarViewMode, // controlled
views?: CalendarViewMode[], // default ['month', 'week', 'day']
defaultDate?: string | Date,
date?: string | Date, // controlled
editable?: boolean, // default false
onEventClick?: (event: CalendarEvent) => void,
onDateChange?: (date: Date) => void,
onViewChange?: (view: CalendarViewMode) => void,
className?: string // from `BaseSchema`, not `CalendarViewSchema`
}
```

`onEventCreate` and `onEventUpdate` complete the 13; see `CalendarViewSchema` for
their signatures. `onDateClick` is **not** on this schema — it is a
`CalendarViewProps` component prop (see [Drag-and-Drop](#drag-and-drop) and
[Click-to-Create](#click-to-create)).

> **The type is not the renderer.** `CalendarViewSchema` is the shape
> `@object-ui/types` publishes for this node, not a description of what the
> registered `calendar-view` renderer reads. That renderer builds its events from
> the node's `data` array plus the `titleField` / `startDateField` /
> `endDateField` / `colorField` / `allDayField` inputs, and **drops an authored
> `events` key** (objectui#4433) — so the type's one required key does nothing
> when written as JSON. Of the keys above it also reads `view` and `className`;
> the handlers only ever arrive from a React host
> (`<SchemaRenderer ... onEventClick={fn} />`), because JSON cannot carry a
> function.

### Calendar Event Structure

The authored event shape, declared by `@object-ui/types`:
Expand Down Expand Up @@ -226,9 +251,9 @@ const schema = {
console.log('Event clicked:', event);
// Open event details modal
},
onDateClick: (date) => {
console.log('Date clicked:', date);
// Create new event
onDateChange: (date) => {
console.log('Visible date changed:', date);
// React to the calendar moving to another month/week/day
}
};
```
Expand Down
179 changes: 179 additions & 0 deletions packages/plugin-calendar/src/readme-calendar-view-schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* `packages/plugin-calendar/README.md`'s "Schema API -> CalendarView" fence
* describes real `CalendarViewSchema` keys, with the requiredness the schema
* actually declares (objectui#5045).
*
* ## Why this file exists
*
* The fence had drifted from the type in four separate ways at once, and every
* one of them was reader-facing on the npm landing page:
*
* - `events?` — the schema's ONLY required key besides `type`, published as
* optional. A reader following the README omits it and TypeScript rejects
* the node;
* - `defaultDate?: string` — the schema says `string | Date`;
* - `onDateClick` — not on `CalendarViewSchema` at all. It is a
* `CalendarViewProps` prop, i.e. a different package's *component* surface,
* so the README sent readers to a key the schema does not have;
* - six keys of thirteen listed, with nothing saying the list was partial.
*
* Nothing was holding the fence to the type, so all four drifted silently and
* were found by reading. That is the failure mode `readme-registration-keys`
* in `@object-ui/layout` names for the same class of defect: "someone read it"
* is not a mechanism. This card's fix was another hand-correction; this pin is
* what stops the next one being needed.
*
* ## Exhaustiveness is deliberately NOT asserted
*
* The fence is a declared PARTIAL summary — it names the author-facing keys and
* points at `CalendarViewSchema` for the rest. A pin demanding all thirteen
* would convert an editorial choice into a gate and force every future schema
* key into the README. What is asserted is the direction that misleads a
* reader: a key the README names must EXIST, and must carry the requiredness
* the schema gives it.
*
* ## Both sides are parsed from source, never restated here
*
* Hardcoding the key list in this file would reproduce the defect one layer up
* — the README was itself a confident hand-written restatement of the schema.
* So `CalendarViewSchema` and `BaseSchema` are read out of
* `packages/types/src/*.ts` on every run, and a red test here always means "fix
* the README (or the schema)", never "update the test". A moved or renamed
* interface fails LOUDLY rather than vacuously passing an empty key set.
*/

import { describe, it, expect } from 'vitest';
import { readFileSync, existsSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

/** Walk up to the workspace root, so the type source is found by repo layout. */
function repoRoot(): string {
let dir = dirname(fileURLToPath(import.meta.url));
for (let i = 0; i < 10; i += 1) {
if (existsSync(join(dir, 'pnpm-workspace.yaml'))) return dir;
dir = resolve(dir, '..');
}
throw new Error('repo root (pnpm-workspace.yaml) not found from this test file');
}

const ROOT = repoRoot();
const README = join(ROOT, 'packages/plugin-calendar/README.md');

interface Key {
optional: boolean;
}

/**
* The own (not inherited) property declarations of a TS interface, by brace
* matching rather than a line regex — the bodies here carry JSDoc blocks and
* function types with their own braces and semicolons.
*/
function interfaceKeys(file: string, name: string): Map<string, Key> {
const src = readFileSync(file, 'utf8');
const opener = new RegExp(`export interface ${name}\\b[^{]*\\{`).exec(src);
if (!opener) throw new Error(`interface ${name} not found in ${file}`);

let i = opener.index + opener[0].length;
const start = i;
for (let depth = 1; depth > 0; i += 1) {
if (i >= src.length) throw new Error(`unterminated interface ${name} in ${file}`);
if (src[i] === '{') depth += 1;
else if (src[i] === '}') depth -= 1;
}

const body = src
.slice(start, i - 1)
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/\/\/.*$/gm, '');

const keys = new Map<string, Key>();
let depth = 0;
let buf = '';
const flush = () => {
const m = /^(?:readonly\s+)?([A-Za-z_$][\w$]*)(\?)?\s*:/.exec(buf.trim());
if (m) keys.set(m[1], { optional: Boolean(m[2]) });
buf = '';
};
for (const ch of body) {
if ('{(['.includes(ch)) depth += 1;
if ('})]'.includes(ch)) depth -= 1;
if ((ch === ';' || ch === '\n') && depth === 0) flush();
else buf += ch;
}
flush();

if (keys.size === 0) throw new Error(`parsed zero keys out of ${name} in ${file}`);
return keys;
}

/** The "Schema API -> CalendarView" fence, and the keys it publishes. */
function readmeFence(): { keys: Map<string, Key>; ownCountClaim: number } {
const src = readFileSync(README, 'utf8');
const heading = src.indexOf('\n### CalendarView\n');
if (heading < 0) throw new Error('"### CalendarView" heading not found in the README');

const section = src.slice(heading, src.indexOf('\n### ', heading + 1));
const fence = /```typescript\n([\s\S]*?)\n```/.exec(section);
if (!fence) throw new Error('no typescript fence under "### CalendarView"');

const keys = new Map<string, Key>();
for (const line of fence[1].split('\n')) {
const m = /^ {2}([A-Za-z_$][\w$]*)(\?)?:/.exec(line);
if (m) keys.set(m[1], { optional: Boolean(m[2]) });
}
if (keys.size === 0) throw new Error('parsed zero keys out of the README fence');

const claim = /declares (\d+) keys of\s+its own/.exec(section);
if (!claim) throw new Error('the fence no longer states how many keys the schema declares');

return { keys, ownCountClaim: Number(claim[1]) };
}

const TYPES = join(ROOT, 'packages/types/src');
const own = interfaceKeys(join(TYPES, 'complex.ts'), 'CalendarViewSchema');
const base = interfaceKeys(join(TYPES, 'base.ts'), 'BaseSchema');
const fence = readmeFence();

describe('plugin-calendar README: "Schema API -> CalendarView"', () => {
it('names only keys `CalendarViewSchema` or `BaseSchema` actually declares', () => {
const phantom = [...fence.keys.keys()].filter((k) => !own.has(k) && !base.has(k));
expect(
phantom,
`README publishes ${JSON.stringify(phantom)} as \`calendar-view\` schema keys, but ` +
'neither `CalendarViewSchema` nor `BaseSchema` declares them. `onDateClick` is the ' +
'original offender: a `CalendarViewProps` component prop, not a schema key.',
).toEqual([]);
});

it('gives every key the requiredness the schema declares', () => {
const wrong: string[] = [];
for (const [key, { optional }] of fence.keys) {
const declared = own.get(key) ?? base.get(key);
if (declared && declared.optional !== optional) {
wrong.push(`${key}: README says ${optional ? 'optional' : 'required'}, schema says ${declared.optional ? 'optional' : 'required'}`);
}
}
expect(
wrong,
'The README must not restate the schema\'s requiredness incorrectly — `events` is the ' +
'schema\'s only required key besides `type`, and was published as `events?`.',
).toEqual([]);
});

it('states the number of keys `CalendarViewSchema` declares, and states it correctly', () => {
expect(
fence.ownCountClaim,
'The README tells readers how many keys the full schema has. That figure is prose and ' +
'drifts like any other; it is held to the interface here.',
).toBe(own.size);
});
});
9 changes: 7 additions & 2 deletions packages/plugin-calendar/tsconfig.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@
// a global augmentation, not an import, and `@testing-library/jest-dom` does
// not live under `@types/` — so it is never picked up automatically and has
// to be named here. Naming `types` at all switches off automatic `@types/*`
// inclusion, which is fine: nothing in these tests touches Node globals.
"types": ["@testing-library/jest-dom"],
// inclusion, which is why `node` is named alongside it:
// `readme-calendar-view-schema.test.ts` reads this package's README and the
// `@object-ui/types` sources off disk to hold the published schema docs to
// the schema (objectui#5045). Same reason `packages/layout/tsconfig.test.json`
// names it, and same reason it stays OUT of `tsconfig.json`: package SOURCE
// ships to browsers and must not compile against Node APIs.
"types": ["@testing-library/jest-dom", "node"],
// Drop the root tsconfig's source-tree `paths` so `@object-ui/*` and
// `@objectstack/spec` resolve through the workspace dependency's built
// `.d.ts` instead of pulling sibling sources in as program inputs (TS6059).
Expand Down
Loading