Faster validations + record reference stability at the same time (#2848)
This PR adds a validation mode whereby previous known-to-be-valid values can be used to speed up the validation process itself. At the same time it enables us to do fine-grained equality checking on records much more quickly than by using something like lodash isEqual, and using that we can prevent triggering effects for record updates that don't actually alter any values in the store. Here's some preliminary perf testing of average time spent in `store.put()` during some common interactions | task | before (ms) | after (ms) | | ---- | ---- | ---- | | drawing lines | 0.0403 | 0.0214 | | drawing boxes | 0.0408 | 0.0348 | | translating lines | 0.0352 | 0.0042 | | translating boxes | 0.0051 | 0.0032 | | rotating lines | 0.0312 | 0.0065 | | rotating boxes | 0.0053 | 0.0035 | | brush selecting boxes | 0.0200 | 0.0232 | | traversal with shapes | 0.0130 | 0.0108 | | traversal without shapes | 0.0201 | 0.0173 | **traversal** means moving the camera and pointer around the canvas #### Discussion At the scale of hundredths of a millisecond these .put operations are so fast that even if they became literally instantaneous the change would not be human perceptible. That said, there is an overall marked improvement here. Especially for dealing with draw shapes. These figures are also mostly in line with expectations, aside from a couple of things: - I don't understand why the `brush selecting boxes` task got slower after the change. - I don't understand why the `traversal` tasks are slower than the `translating boxes` task, both before and after. I would expect that .putting shape records would be much slower than .putting pointer/camera records (since the latter have fewer and simpler properties) ### Change Type - [x] `patch` — Bug fix ### Test Plan 1. Add a step-by-step description of how to test your PR here. 2. - [ ] Unit Tests - [ ] End to end tests ### Release Notes - Add a brief release note for your PR here.
This commit is contained in:
parent
50f77fe75c
commit
4a2040f92c
35 changed files with 1486 additions and 293 deletions
|
@ -11,7 +11,7 @@
|
|||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development wrangler dev --log-level info --persist-to tmp-assets",
|
||||
"test-ci": "lazy inherit --passWithNoTests",
|
||||
"test": "yarn run -T jest",
|
||||
"test": "yarn run -T jest --passWithNoTests",
|
||||
"test-coverage": "lazy inherit --passWithNoTests",
|
||||
"lint": "yarn run -T tsx ../../scripts/lint.ts"
|
||||
},
|
||||
|
|
|
@ -15,7 +15,7 @@ const SIZES = [
|
|||
{ value: 'm', icon: 'size-medium' },
|
||||
{ value: 'l', icon: 'size-large' },
|
||||
{ value: 'xl', icon: 'size-extra-large' },
|
||||
]
|
||||
] as const
|
||||
|
||||
// There's a guide at the bottom of this file!
|
||||
|
||||
|
|
|
@ -31,9 +31,12 @@ export const FilterStyleUi = track(function FilterStyleUi() {
|
|||
onChange={(e) => {
|
||||
editor.batch(() => {
|
||||
if (editor.isIn('select')) {
|
||||
editor.setStyleForSelectedShapes(MyFilterStyle, e.target.value)
|
||||
editor.setStyleForSelectedShapes(
|
||||
MyFilterStyle,
|
||||
MyFilterStyle.validate(e.target.value)
|
||||
)
|
||||
}
|
||||
editor.setStyleForNextShapes(MyFilterStyle, e.target.value)
|
||||
editor.setStyleForNextShapes(MyFilterStyle, MyFilterStyle.validate(e.target.value))
|
||||
})
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
"check-scripts": "tsx scripts/check-scripts.ts",
|
||||
"api-check": "lazy api-check",
|
||||
"test-ci": "lazy test-ci",
|
||||
"test": "yarn run -T jest",
|
||||
"test": "lazy test",
|
||||
"test-coverage": "lazy test-coverage && node scripts/offer-coverage.mjs",
|
||||
"e2e": "lazy e2e --filter='apps/examples'"
|
||||
},
|
||||
|
|
|
@ -32,6 +32,7 @@ import { Signal } from '@tldraw/state';
|
|||
import { StoreSchema } from '@tldraw/store';
|
||||
import { StoreSnapshot } from '@tldraw/store';
|
||||
import { StyleProp } from '@tldraw/tlschema';
|
||||
import { StylePropValue } from '@tldraw/tlschema';
|
||||
import { TLArrowShape } from '@tldraw/tlschema';
|
||||
import { TLArrowShapeArrowheadStyle } from '@tldraw/tlschema';
|
||||
import { TLAsset } from '@tldraw/tlschema';
|
||||
|
@ -864,7 +865,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
setOpacityForSelectedShapes(opacity: number, historyOptions?: TLCommandHistoryOptions): this;
|
||||
setSelectedShapes(shapes: TLShape[] | TLShapeId[], historyOptions?: TLCommandHistoryOptions): this;
|
||||
setStyleForNextShapes<T>(style: StyleProp<T>, value: T, historyOptions?: TLCommandHistoryOptions): this;
|
||||
setStyleForSelectedShapes<T>(style: StyleProp<T>, value: T, historyOptions?: TLCommandHistoryOptions): this;
|
||||
setStyleForSelectedShapes<S extends StyleProp<any>>(style: S, value: StylePropValue<S>, historyOptions?: TLCommandHistoryOptions): this;
|
||||
shapeUtils: {
|
||||
readonly [K in string]?: ShapeUtil<TLUnknownShape>;
|
||||
};
|
||||
|
@ -884,7 +885,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
stretchShapes(shapes: TLShape[] | TLShapeId[], operation: 'horizontal' | 'vertical'): this;
|
||||
// (undocumented)
|
||||
styleProps: {
|
||||
[key: string]: Map<StyleProp<unknown>, string>;
|
||||
[key: string]: Map<StyleProp<any>, string>;
|
||||
};
|
||||
readonly textMeasure: TextManager;
|
||||
toggleLock(shapes: TLShape[] | TLShapeId[]): this;
|
||||
|
@ -1462,10 +1463,10 @@ export { react }
|
|||
// @public
|
||||
export class ReadonlySharedStyleMap {
|
||||
// (undocumented)
|
||||
[Symbol.iterator](): IterableIterator<[StyleProp<unknown>, SharedStyle<unknown>]>;
|
||||
[Symbol.iterator](): IterableIterator<[StyleProp<any>, SharedStyle<unknown>]>;
|
||||
constructor(entries?: Iterable<[StyleProp<unknown>, SharedStyle<unknown>]>);
|
||||
// (undocumented)
|
||||
entries(): IterableIterator<[StyleProp<unknown>, SharedStyle<unknown>]>;
|
||||
entries(): IterableIterator<[StyleProp<any>, SharedStyle<unknown>]>;
|
||||
// (undocumented)
|
||||
equals(other: ReadonlySharedStyleMap): boolean;
|
||||
// (undocumented)
|
||||
|
@ -1473,9 +1474,9 @@ export class ReadonlySharedStyleMap {
|
|||
// (undocumented)
|
||||
getAsKnownValue<T>(prop: StyleProp<T>): T | undefined;
|
||||
// (undocumented)
|
||||
keys(): IterableIterator<StyleProp<unknown>>;
|
||||
keys(): IterableIterator<StyleProp<any>>;
|
||||
// @internal (undocumented)
|
||||
protected map: Map<StyleProp<unknown>, SharedStyle<unknown>>;
|
||||
protected map: Map<StyleProp<any>, SharedStyle<unknown>>;
|
||||
// (undocumented)
|
||||
get size(): number;
|
||||
// (undocumented)
|
||||
|
|
|
@ -17658,7 +17658,7 @@
|
|||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "setStyleForSelectedShapes<T>(style: "
|
||||
"text": "setStyleForSelectedShapes<S extends "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -17667,15 +17667,28 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<T>"
|
||||
"text": "<any>"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ">(style: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "S"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", value: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "StylePropValue",
|
||||
"canonicalReference": "@tldraw/tlschema!StylePropValue:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "T"
|
||||
"text": "<S>"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -17701,10 +17714,10 @@
|
|||
],
|
||||
"typeParameters": [
|
||||
{
|
||||
"typeParameterName": "T",
|
||||
"typeParameterName": "S",
|
||||
"constraintTokenRange": {
|
||||
"startIndex": 0,
|
||||
"endIndex": 0
|
||||
"startIndex": 1,
|
||||
"endIndex": 3
|
||||
},
|
||||
"defaultTypeTokenRange": {
|
||||
"startIndex": 0,
|
||||
|
@ -17714,8 +17727,8 @@
|
|||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 8,
|
||||
"endIndex": 9
|
||||
"startIndex": 11,
|
||||
"endIndex": 12
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
|
@ -17723,14 +17736,6 @@
|
|||
"parameters": [
|
||||
{
|
||||
"parameterName": "style",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 3
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "value",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 4,
|
||||
"endIndex": 5
|
||||
|
@ -17738,10 +17743,18 @@
|
|||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "historyOptions",
|
||||
"parameterName": "value",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 6,
|
||||
"endIndex": 7
|
||||
"endIndex": 8
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "historyOptions",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 9,
|
||||
"endIndex": 10
|
||||
},
|
||||
"isOptional": true
|
||||
}
|
||||
|
@ -18263,7 +18276,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<unknown>, string>;\n }"
|
||||
"text": "<any>, string>;\n }"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -28523,7 +28536,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<unknown>, "
|
||||
"text": "<any>, "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -28632,7 +28645,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<unknown>, "
|
||||
"text": "<any>, "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -28872,7 +28885,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<unknown>>"
|
||||
"text": "<any>>"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
InstancePageStateRecordType,
|
||||
PageRecordType,
|
||||
StyleProp,
|
||||
StylePropValue,
|
||||
TLArrowShape,
|
||||
TLAsset,
|
||||
TLAssetId,
|
||||
|
@ -737,7 +738,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
*/
|
||||
shapeUtils: { readonly [K in string]?: ShapeUtil<TLUnknownShape> }
|
||||
|
||||
styleProps: { [key: string]: Map<StyleProp<unknown>, string> }
|
||||
styleProps: { [key: string]: Map<StyleProp<any>, string> }
|
||||
|
||||
/**
|
||||
* Get a shape util from a shape itself.
|
||||
|
@ -7385,9 +7386,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
*
|
||||
* @public
|
||||
*/
|
||||
setStyleForSelectedShapes<T>(
|
||||
style: StyleProp<T>,
|
||||
value: T,
|
||||
setStyleForSelectedShapes<S extends StyleProp<any>>(
|
||||
style: S,
|
||||
value: StylePropValue<S>,
|
||||
historyOptions?: TLCommandHistoryOptions
|
||||
): this {
|
||||
const selectedShapes = this.getSelectedShapes()
|
||||
|
|
|
@ -35,7 +35,7 @@ function sharedStyleEquals<T>(a: SharedStyle<T>, b: SharedStyle<T> | undefined)
|
|||
*/
|
||||
export class ReadonlySharedStyleMap {
|
||||
/** @internal */
|
||||
protected map: Map<StyleProp<unknown>, SharedStyle<unknown>>
|
||||
protected map: Map<StyleProp<any>, SharedStyle<unknown>>
|
||||
|
||||
constructor(entries?: Iterable<[StyleProp<unknown>, SharedStyle<unknown>]>) {
|
||||
this.map = new Map(entries)
|
||||
|
|
|
@ -163,9 +163,7 @@ export class RecordType<R extends UnknownRecord, RequiredProperties extends keyo
|
|||
typeName: R['typeName'], config: {
|
||||
readonly createDefaultProperties: () => Exclude<OmitMeta<R>, RequiredProperties>;
|
||||
readonly migrations: Migrations;
|
||||
readonly validator?: {
|
||||
validate: (r: unknown) => R;
|
||||
} | StoreValidator<R>;
|
||||
readonly validator?: StoreValidator<R>;
|
||||
readonly scope?: RecordScope;
|
||||
});
|
||||
clone(record: R): R;
|
||||
|
@ -183,11 +181,9 @@ export class RecordType<R extends UnknownRecord, RequiredProperties extends keyo
|
|||
// (undocumented)
|
||||
readonly scope: RecordScope;
|
||||
readonly typeName: R['typeName'];
|
||||
validate(record: unknown): R;
|
||||
validate(record: unknown, recordBefore?: R): R;
|
||||
// (undocumented)
|
||||
readonly validator: {
|
||||
validate: (r: unknown) => R;
|
||||
} | StoreValidator<R>;
|
||||
readonly validator: StoreValidator<R>;
|
||||
withDefaultProperties<DefaultProps extends Omit<Partial<R>, 'id' | 'typeName'>>(createDefaultProperties: () => DefaultProps): RecordType<R, Exclude<RequiredProperties, keyof DefaultProps>>;
|
||||
}
|
||||
|
||||
|
@ -344,6 +340,7 @@ export type StoreSnapshot<R extends UnknownRecord> = {
|
|||
// @public (undocumented)
|
||||
export type StoreValidator<R extends UnknownRecord> = {
|
||||
validate: (record: unknown) => R;
|
||||
validateUsingKnownGoodVersion?: (knownGoodVersion: R, record: unknown) => R;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
|
|
|
@ -2062,7 +2062,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";\n readonly validator?: {\n validate: (r: unknown) => R;\n } | "
|
||||
"text": ";\n readonly validator?: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -2650,6 +2650,14 @@
|
|||
"kind": "Content",
|
||||
"text": "unknown"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", recordBefore?: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "R"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
|
@ -2665,8 +2673,8 @@
|
|||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 3,
|
||||
"endIndex": 4
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
|
@ -2679,6 +2687,14 @@
|
|||
"endIndex": 2
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "recordBefore",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 3,
|
||||
"endIndex": 4
|
||||
},
|
||||
"isOptional": true
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
|
@ -2694,10 +2710,6 @@
|
|||
"kind": "Content",
|
||||
"text": "readonly validator: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "{\n validate: (r: unknown) => R;\n } | "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "StoreValidator",
|
||||
|
@ -2718,7 +2730,7 @@
|
|||
"name": "validator",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 4
|
||||
"endIndex": 3
|
||||
},
|
||||
"isStatic": false,
|
||||
"isProtected": false,
|
||||
|
@ -5535,7 +5547,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "{\n validate: (record: unknown) => R;\n}"
|
||||
"text": "{\n validate: (record: unknown) => R;\n validateUsingKnownGoodVersion?: (knownGoodVersion: R, record: unknown) => R;\n}"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
|
|
@ -29,7 +29,7 @@ export class RecordType<
|
|||
> {
|
||||
readonly createDefaultProperties: () => Exclude<OmitMeta<R>, RequiredProperties>
|
||||
readonly migrations: Migrations
|
||||
readonly validator: StoreValidator<R> | { validate: (r: unknown) => R }
|
||||
readonly validator: StoreValidator<R>
|
||||
|
||||
readonly scope: RecordScope
|
||||
|
||||
|
@ -44,7 +44,7 @@ export class RecordType<
|
|||
config: {
|
||||
readonly createDefaultProperties: () => Exclude<OmitMeta<R>, RequiredProperties>
|
||||
readonly migrations: Migrations
|
||||
readonly validator?: StoreValidator<R> | { validate: (r: unknown) => R }
|
||||
readonly validator?: StoreValidator<R>
|
||||
readonly scope?: RecordScope
|
||||
}
|
||||
) {
|
||||
|
@ -198,7 +198,10 @@ export class RecordType<
|
|||
* Check that the passed in record passes the validations for this type. Returns its input
|
||||
* correctly typed if it does, but throws an error otherwise.
|
||||
*/
|
||||
validate(record: unknown): R {
|
||||
validate(record: unknown, recordBefore?: R): R {
|
||||
if (recordBefore && this.validator.validateUsingKnownGoodVersion) {
|
||||
return this.validator.validateUsingKnownGoodVersion(recordBefore, record)
|
||||
}
|
||||
return this.validator.validate(record)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,6 +84,7 @@ export type StoreSnapshot<R extends UnknownRecord> = {
|
|||
/** @public */
|
||||
export type StoreValidator<R extends UnknownRecord> = {
|
||||
validate: (record: unknown) => R
|
||||
validateUsingKnownGoodVersion?: (knownGoodVersion: R, record: unknown) => R
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
@ -389,24 +390,19 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|||
if (beforeUpdate) record = beforeUpdate(initialValue, record, source)
|
||||
|
||||
// Validate the record
|
||||
record = this.schema.validateRecord(
|
||||
const validated = this.schema.validateRecord(
|
||||
this,
|
||||
record,
|
||||
phaseOverride ?? 'updateRecord',
|
||||
initialValue
|
||||
)
|
||||
|
||||
if (validated === initialValue) continue
|
||||
|
||||
recordAtom.set(devFreeze(record))
|
||||
|
||||
// need to deref atom in case nextValue is not identical but is .equals?
|
||||
const finalValue = recordAtom.__unsafe__getWithoutCapture()
|
||||
|
||||
// If the value has changed, assign it to updates.
|
||||
// todo: is this always going to be true?
|
||||
if (initialValue !== finalValue) {
|
||||
didChange = true
|
||||
updates[record.id] = [initialValue, finalValue]
|
||||
}
|
||||
updates[record.id] = [initialValue, recordAtom.__unsafe__getWithoutCapture()]
|
||||
} else {
|
||||
if (beforeCreate) record = beforeCreate(record, source)
|
||||
|
||||
|
|
|
@ -85,7 +85,7 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
|
|||
if (!recordType) {
|
||||
throw new Error(`Missing definition for record type ${record.typeName}`)
|
||||
}
|
||||
return recordType.validate(record)
|
||||
return recordType.validate(record, recordBefore ?? undefined)
|
||||
} catch (error: unknown) {
|
||||
if (this.options.onValidationFailure) {
|
||||
return this.options.onValidationFailure({
|
||||
|
|
|
@ -195,9 +195,9 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
isPrecise: boolean;
|
||||
}>;
|
||||
point: ObjectValidator< {
|
||||
type: "point";
|
||||
x: number;
|
||||
y: number;
|
||||
type: "point";
|
||||
}>;
|
||||
}, never>;
|
||||
end: UnionValidator<"type", {
|
||||
|
@ -209,9 +209,9 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
isPrecise: boolean;
|
||||
}>;
|
||||
point: ObjectValidator< {
|
||||
type: "point";
|
||||
x: number;
|
||||
y: number;
|
||||
type: "point";
|
||||
}>;
|
||||
}, never>;
|
||||
bend: Validator<number>;
|
||||
|
@ -1253,7 +1253,7 @@ export function TldrawUiButtonIcon({ icon, small, invertIcon }: TLUiButtonIconPr
|
|||
export function TldrawUiButtonLabel({ children }: TLUiButtonLabelProps): JSX_2.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const TldrawUiButtonPicker: MemoExoticComponent<(<T extends string>(props: TLUiButtonPickerProps<T>) => JSX_2.Element)>;
|
||||
export const TldrawUiButtonPicker: typeof _TldrawUiButtonPicker;
|
||||
|
||||
// @public (undocumented)
|
||||
export function TldrawUiComponentsProvider({ overrides, children, }: TLUiComponentsProviderProps): JSX_2.Element;
|
||||
|
@ -2108,7 +2108,7 @@ export function useNativeClipboardEvents(): void;
|
|||
export function useReadonly(): boolean;
|
||||
|
||||
// @public (undocumented)
|
||||
export function useRelevantStyles(stylesToCheck?: readonly (EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow"> | EnumStyleProp<"dashed" | "dotted" | "draw" | "solid"> | EnumStyleProp<"l" | "m" | "s" | "xl"> | EnumStyleProp<"none" | "pattern" | "semi" | "solid">)[]): null | ReadonlySharedStyleMap;
|
||||
export function useRelevantStyles(stylesToCheck?: readonly StyleProp<any>[]): null | ReadonlySharedStyleMap;
|
||||
|
||||
// @public (undocumented)
|
||||
export function useTldrawUiComponents(): Partial<{
|
||||
|
|
|
@ -1387,7 +1387,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<{\n type: \"point\";\n x: number;\n y: number;\n }>;\n }, never>;\n end: import(\"@tldraw/editor\")."
|
||||
"text": "<{\n x: number;\n y: number;\n type: \"point\";\n }>;\n }, never>;\n end: import(\"@tldraw/editor\")."
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -1432,7 +1432,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<{\n type: \"point\";\n x: number;\n y: number;\n }>;\n }, never>;\n bend: import(\"@tldraw/editor\")."
|
||||
"text": "<{\n x: number;\n y: number;\n type: \"point\";\n }>;\n }, never>;\n bend: import(\"@tldraw/editor\")."
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -14716,34 +14716,12 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "import(\"react\")."
|
||||
"text": "typeof "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "MemoExoticComponent",
|
||||
"canonicalReference": "@types/react!React.MemoExoticComponent:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<(<T extends string>(props: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLUiButtonPickerProps",
|
||||
"canonicalReference": "@tldraw/tldraw!TLUiButtonPickerProps:interface"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<T>) => import(\"react/jsx-runtime\")."
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "JSX.Element",
|
||||
"canonicalReference": "@types/react!JSX.Element:interface"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ")>"
|
||||
"text": "_TldrawUiButtonPicker",
|
||||
"canonicalReference": "@tldraw/tldraw!~_TldrawUiButtonPicker:function"
|
||||
}
|
||||
],
|
||||
"fileUrlPath": "packages/tldraw/src/lib/ui/components/primitives/TldrawUiButtonPicker.tsx",
|
||||
|
@ -14752,7 +14730,7 @@
|
|||
"name": "TldrawUiButtonPicker",
|
||||
"variableTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 8
|
||||
"endIndex": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -23506,43 +23484,16 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "readonly (import(\"@tldraw/editor\")."
|
||||
"text": "readonly "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "EnumStyleProp",
|
||||
"canonicalReference": "@tldraw/tlschema!EnumStyleProp:class"
|
||||
"text": "StyleProp",
|
||||
"canonicalReference": "@tldraw/tlschema!StyleProp:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<\"black\" | \"blue\" | \"green\" | \"grey\" | \"light-blue\" | \"light-green\" | \"light-red\" | \"light-violet\" | \"orange\" | \"red\" | \"violet\" | \"yellow\"> | import(\"@tldraw/editor\")."
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "EnumStyleProp",
|
||||
"canonicalReference": "@tldraw/tlschema!EnumStyleProp:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<\"dashed\" | \"dotted\" | \"draw\" | \"solid\"> | import(\"@tldraw/editor\")."
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "EnumStyleProp",
|
||||
"canonicalReference": "@tldraw/tlschema!EnumStyleProp:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<\"l\" | \"m\" | \"s\" | \"xl\"> | import(\"@tldraw/editor\")."
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "EnumStyleProp",
|
||||
"canonicalReference": "@tldraw/tlschema!EnumStyleProp:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<\"none\" | \"pattern\" | \"semi\" | \"solid\">)[]"
|
||||
"text": "<any>[]"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -23564,8 +23515,8 @@
|
|||
],
|
||||
"fileUrlPath": "packages/tldraw/src/lib/ui/hooks/useRevelantStyles.ts",
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 11,
|
||||
"endIndex": 13
|
||||
"startIndex": 5,
|
||||
"endIndex": 7
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"overloadIndex": 1,
|
||||
|
@ -23574,7 +23525,7 @@
|
|||
"parameterName": "stylesToCheck",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 10
|
||||
"endIndex": 4
|
||||
},
|
||||
"isOptional": true
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@ import {
|
|||
LineShapeSplineStyle,
|
||||
ReadonlySharedStyleMap,
|
||||
StyleProp,
|
||||
TLArrowShapeArrowheadStyle,
|
||||
TLDefaultVerticalAlignStyle,
|
||||
minBy,
|
||||
useEditor,
|
||||
useValue,
|
||||
|
@ -199,7 +201,7 @@ function TextStylePickerSet({ styles }: { styles: ReadonlySharedStyleMap }) {
|
|||
<TldrawUiButtonIcon icon="vertical-align-center" />
|
||||
</TldrawUiButton>
|
||||
) : (
|
||||
<DropdownPicker
|
||||
<DropdownPicker<TLDefaultVerticalAlignStyle>
|
||||
type="icon"
|
||||
id="geo-vertical-alignment"
|
||||
uiType="verticalAlign"
|
||||
|
@ -270,7 +272,7 @@ function ArrowheadStylePickerSet({ styles }: { styles: ReadonlySharedStyleMap })
|
|||
}
|
||||
|
||||
return (
|
||||
<DoubleDropdownPicker
|
||||
<DoubleDropdownPicker<TLArrowShapeArrowheadStyle>
|
||||
label={'style-panel.arrowheads'}
|
||||
uiTypeA="arrowheadStart"
|
||||
styleA={ArrowShapeArrowheadStartStyle}
|
||||
|
|
|
@ -27,7 +27,7 @@ interface DoubleDropdownPickerProps<T extends string> {
|
|||
onValueChange: (style: StyleProp<T>, value: T, squashing: boolean) => void
|
||||
}
|
||||
|
||||
export const DoubleDropdownPicker = React.memo(function DoubleDropdownPicker<T extends string>({
|
||||
function _DoubleDropdownPicker<T extends string>({
|
||||
label,
|
||||
uiTypeA,
|
||||
uiTypeB,
|
||||
|
@ -137,4 +137,9 @@ export const DoubleDropdownPicker = React.memo(function DoubleDropdownPicker<T e
|
|||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// need to memo like this to get generics
|
||||
export const DoubleDropdownPicker = React.memo(
|
||||
_DoubleDropdownPicker
|
||||
) as typeof _DoubleDropdownPicker
|
||||
|
|
|
@ -25,7 +25,7 @@ interface DropdownPickerProps<T extends string> {
|
|||
onValueChange: (style: StyleProp<T>, value: T, squashing: boolean) => void
|
||||
}
|
||||
|
||||
export const DropdownPicker = React.memo(function DropdownPicker<T extends string>({
|
||||
function _DropdownPicker<T extends string>({
|
||||
id,
|
||||
label,
|
||||
uiType,
|
||||
|
@ -76,4 +76,7 @@ export const DropdownPicker = React.memo(function DropdownPicker<T extends strin
|
|||
</TldrawUiDropdownMenuContent>
|
||||
</TldrawUiDropdownMenuRoot>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// need to export like this to get generics
|
||||
export const DropdownPicker = React.memo(_DropdownPicker) as typeof _DropdownPicker
|
||||
|
|
|
@ -25,10 +25,7 @@ export interface TLUiButtonPickerProps<T extends string> {
|
|||
onValueChange: (style: StyleProp<T>, value: T, squashing: boolean) => void
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const TldrawUiButtonPicker = memo(function TldrawUiButtonPicker<T extends string>(
|
||||
props: TLUiButtonPickerProps<T>
|
||||
) {
|
||||
function _TldrawUiButtonPicker<T extends string>(props: TLUiButtonPickerProps<T>) {
|
||||
const {
|
||||
uiType,
|
||||
items,
|
||||
|
@ -125,4 +122,6 @@ export const TldrawUiButtonPicker = memo(function TldrawUiButtonPicker<T extends
|
|||
))}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
/** @public */
|
||||
export const TldrawUiButtonPicker = memo(_TldrawUiButtonPicker) as typeof _TldrawUiButtonPicker
|
||||
|
|
|
@ -5,11 +5,12 @@ import {
|
|||
DefaultSizeStyle,
|
||||
ReadonlySharedStyleMap,
|
||||
SharedStyleMap,
|
||||
StyleProp,
|
||||
useEditor,
|
||||
useValue,
|
||||
} from '@tldraw/editor'
|
||||
|
||||
const selectToolStyles = Object.freeze([
|
||||
const selectToolStyles: readonly StyleProp<any>[] = Object.freeze([
|
||||
DefaultColorStyle,
|
||||
DefaultDashStyle,
|
||||
DefaultFillStyle,
|
||||
|
|
|
@ -56,7 +56,7 @@ const tldrawFileValidator: T.Validator<TldrawFile> = T.object({
|
|||
}),
|
||||
records: T.arrayOf(
|
||||
T.object({
|
||||
id: T.string as T.Validator<RecordId<any>>,
|
||||
id: T.string as any as T.Validator<RecordId<any>>,
|
||||
typeName: T.string,
|
||||
}).allowUnknownProperties()
|
||||
),
|
||||
|
|
|
@ -45,12 +45,12 @@ export const arrowShapeProps: {
|
|||
normalizedAnchor: VecModel;
|
||||
isExact: boolean;
|
||||
isPrecise: boolean;
|
||||
}>;
|
||||
} & {}>;
|
||||
point: T.ObjectValidator<{
|
||||
type: "point";
|
||||
x: number;
|
||||
y: number;
|
||||
}>;
|
||||
type: "point";
|
||||
} & {}>;
|
||||
}, never>;
|
||||
end: T.UnionValidator<"type", {
|
||||
binding: T.ObjectValidator<{
|
||||
|
@ -59,12 +59,12 @@ export const arrowShapeProps: {
|
|||
normalizedAnchor: VecModel;
|
||||
isExact: boolean;
|
||||
isPrecise: boolean;
|
||||
}>;
|
||||
} & {}>;
|
||||
point: T.ObjectValidator<{
|
||||
type: "point";
|
||||
x: number;
|
||||
y: number;
|
||||
}>;
|
||||
type: "point";
|
||||
} & {}>;
|
||||
}, never>;
|
||||
bend: T.Validator<number>;
|
||||
text: T.Validator<string>;
|
||||
|
@ -116,13 +116,19 @@ export const CameraRecordType: RecordType<TLCamera, never>;
|
|||
export const canvasUiColorTypeValidator: T.Validator<"accent" | "black" | "laser" | "muted-1" | "selection-fill" | "selection-stroke" | "white">;
|
||||
|
||||
// @public
|
||||
export function createAssetValidator<Type extends string, Props extends JsonObject>(type: Type, props: T.Validator<Props>): T.ObjectValidator<{
|
||||
export function createAssetValidator<Type extends string, Props extends JsonObject>(type: Type, props: T.Validator<Props>): T.ObjectValidator<{ [P in "id" | "meta" | "typeName" | (undefined extends Props ? never : "props") | (undefined extends Type ? never : "type")]: {
|
||||
id: TLAssetId;
|
||||
typeName: 'asset';
|
||||
type: Type;
|
||||
props: Props;
|
||||
meta: JsonObject;
|
||||
}>;
|
||||
}[P]; } & { [P_1 in (undefined extends Props ? "props" : never) | (undefined extends Type ? "type" : never)]?: {
|
||||
id: TLAssetId;
|
||||
typeName: 'asset';
|
||||
type: Type;
|
||||
props: Props;
|
||||
meta: JsonObject;
|
||||
}[P_1] | undefined; }>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const createPresenceStateDerivation: ($user: Signal<{
|
||||
|
@ -139,7 +145,7 @@ export function createShapeValidator<Type extends string, Props extends JsonObje
|
|||
[K in keyof Props]: T.Validatable<Props[K]>;
|
||||
}, meta?: {
|
||||
[K in keyof Meta]: T.Validatable<Meta[K]>;
|
||||
}): T.ObjectValidator<TLBaseShape<Type, Props>>;
|
||||
}): T.ObjectValidator<{ [P in "id" | "index" | "isLocked" | "meta" | "opacity" | "parentId" | "rotation" | "typeName" | "x" | "y" | (undefined extends Props ? never : "props") | (undefined extends Type ? never : "type")]: TLBaseShape<Type, Props>[P]; } & { [P_1 in (undefined extends Props ? "props" : never) | (undefined extends Type ? "type" : never)]?: TLBaseShape<Type, Props>[P_1] | undefined; }>;
|
||||
|
||||
// @public
|
||||
export function createTLSchema({ shapes }: {
|
||||
|
@ -196,7 +202,7 @@ export const drawShapeProps: {
|
|||
segments: T.ArrayOfValidator<{
|
||||
type: "free" | "straight";
|
||||
points: VecModel[];
|
||||
}>;
|
||||
} & {}>;
|
||||
isComplete: T.Validator<boolean>;
|
||||
isClosed: T.Validator<boolean>;
|
||||
isPen: T.Validator<boolean>;
|
||||
|
@ -515,7 +521,7 @@ export const highlightShapeProps: {
|
|||
segments: T.ArrayOfValidator<{
|
||||
type: "free" | "straight";
|
||||
points: VecModel[];
|
||||
}>;
|
||||
} & {}>;
|
||||
isComplete: T.Validator<boolean>;
|
||||
isPen: T.Validator<boolean>;
|
||||
};
|
||||
|
@ -533,10 +539,10 @@ export const imageShapeProps: {
|
|||
playing: T.Validator<boolean>;
|
||||
url: T.Validator<string>;
|
||||
assetId: T.Validator<TLAssetId | null>;
|
||||
crop: T.Validator<{
|
||||
crop: T.Validator<({
|
||||
topLeft: VecModel;
|
||||
bottomRight: VecModel;
|
||||
} | null>;
|
||||
} & {}) | null>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
|
@ -716,12 +722,8 @@ export const rootShapeMigrations: Migrations;
|
|||
// @public (undocumented)
|
||||
export type SchemaShapeInfo = {
|
||||
migrations?: Migrations;
|
||||
props?: Record<string, {
|
||||
validate: (prop: any) => any;
|
||||
}>;
|
||||
meta?: Record<string, {
|
||||
validate: (prop: any) => any;
|
||||
}>;
|
||||
props?: Record<string, AnyValidator>;
|
||||
meta?: Record<string, AnyValidator>;
|
||||
};
|
||||
|
||||
// @internal (undocumented)
|
||||
|
@ -755,8 +757,13 @@ export class StyleProp<Type> implements T.Validatable<Type> {
|
|||
readonly type: T.Validatable<Type>;
|
||||
// (undocumented)
|
||||
validate(value: unknown): Type;
|
||||
// (undocumented)
|
||||
validateUsingKnownGoodVersion(prevValue: Type, newValue: unknown): Type;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type StylePropValue<T extends StyleProp<any>> = T extends StyleProp<infer U> ? U : never;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const textShapeMigrations: Migrations;
|
||||
|
||||
|
|
|
@ -355,7 +355,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";\n isExact: boolean;\n isPrecise: boolean;\n }>;\n point: "
|
||||
"text": ";\n isExact: boolean;\n isPrecise: boolean;\n } & {}>;\n point: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -364,7 +364,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<{\n type: \"point\";\n x: number;\n y: number;\n }>;\n }, never>;\n end: "
|
||||
"text": "<{\n x: number;\n y: number;\n type: \"point\";\n } & {}>;\n }, never>;\n end: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -400,7 +400,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";\n isExact: boolean;\n isPrecise: boolean;\n }>;\n point: "
|
||||
"text": ";\n isExact: boolean;\n isPrecise: boolean;\n } & {}>;\n point: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -409,7 +409,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<{\n type: \"point\";\n x: number;\n y: number;\n }>;\n }, never>;\n bend: "
|
||||
"text": "<{\n x: number;\n y: number;\n type: \"point\";\n } & {}>;\n }, never>;\n bend: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -880,7 +880,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<{\n id: "
|
||||
"text": "<{ [P in \"id\" | \"meta\" | \"typeName\" | (undefined extends Props ? never : \"props\") | (undefined extends Type ? never : \"type\")]: {\n id: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -898,7 +898,25 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";\n}>"
|
||||
"text": ";\n}[P]; } & { [P_1 in (undefined extends Props ? \"props\" : never) | (undefined extends Type ? \"type\" : never)]?: {\n id: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLAssetId",
|
||||
"canonicalReference": "@tldraw/tlschema!TLAssetId:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";\n typeName: 'asset';\n type: Type;\n props: Props;\n meta: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "JsonObject",
|
||||
"canonicalReference": "@tldraw/utils!JsonObject:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";\n}[P_1] | undefined; }>"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -908,7 +926,7 @@
|
|||
"fileUrlPath": "packages/tlschema/src/assets/TLBaseAsset.ts",
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 10,
|
||||
"endIndex": 16
|
||||
"endIndex": 20
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"overloadIndex": 1,
|
||||
|
@ -1154,7 +1172,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<"
|
||||
"text": "<{ [P in \"id\" | \"index\" | \"isLocked\" | \"meta\" | \"opacity\" | \"parentId\" | \"rotation\" | \"typeName\" | \"x\" | \"y\" | (undefined extends Props ? never : \"props\") | (undefined extends Type ? never : \"type\")]: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -1163,7 +1181,16 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<Type, Props>>"
|
||||
"text": "<Type, Props>[P]; } & { [P_1 in (undefined extends Props ? \"props\" : never) | (undefined extends Type ? \"type\" : never)]?: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLBaseShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLBaseShape:interface"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<Type, Props>[P_1] | undefined; }>"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -1173,7 +1200,7 @@
|
|||
"fileUrlPath": "packages/tlschema/src/shapes/TLBaseShape.ts",
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 17,
|
||||
"endIndex": 21
|
||||
"endIndex": 23
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"overloadIndex": 1,
|
||||
|
@ -1698,7 +1725,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "[];\n }>;\n isComplete: "
|
||||
"text": "[];\n } & {}>;\n isComplete: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -2304,7 +2331,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "[];\n }>;\n isComplete: "
|
||||
"text": "[];\n } & {}>;\n isComplete: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -2408,7 +2435,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<{\n topLeft: import(\"../misc/geometry-types\")."
|
||||
"text": "<({\n topLeft: import(\"../misc/geometry-types\")."
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -2426,7 +2453,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";\n } | null>;\n}"
|
||||
"text": ";\n } & {}) | null>;\n}"
|
||||
}
|
||||
],
|
||||
"fileUrlPath": "packages/tlschema/src/shapes/TLImageShape.ts",
|
||||
|
@ -3061,7 +3088,16 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<string, {\n validate: (prop: any) => any;\n }>;\n meta?: "
|
||||
"text": "<string, "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "AnyValidator",
|
||||
"canonicalReference": "@tldraw/tlschema!~AnyValidator:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ">;\n meta?: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -3070,7 +3106,16 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<string, {\n validate: (prop: any) => any;\n }>;\n}"
|
||||
"text": "<string, "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "AnyValidator",
|
||||
"canonicalReference": "@tldraw/tlschema!~AnyValidator:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ">;\n}"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -3082,7 +3127,7 @@
|
|||
"name": "SchemaShapeInfo",
|
||||
"typeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 8
|
||||
"endIndex": 12
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -3548,6 +3593,70 @@
|
|||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "validate"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/tlschema!StyleProp#validateUsingKnownGoodVersion:member(1)",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "validateUsingKnownGoodVersion(prevValue: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "Type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", newValue: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "unknown"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "Type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [
|
||||
{
|
||||
"parameterName": "prevValue",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "newValue",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 3,
|
||||
"endIndex": 4
|
||||
},
|
||||
"isOptional": false
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "validateUsingKnownGoodVersion"
|
||||
}
|
||||
],
|
||||
"implementsTokenRanges": [
|
||||
|
@ -3557,6 +3666,67 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "TypeAlias",
|
||||
"canonicalReference": "@tldraw/tlschema!StylePropValue:type",
|
||||
"docComment": "/**\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "export type StylePropValue<T extends "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "StyleProp",
|
||||
"canonicalReference": "@tldraw/tlschema!StyleProp:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<any>"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "> = "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "T extends "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "StyleProp",
|
||||
"canonicalReference": "@tldraw/tlschema!StyleProp:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<infer U> ? U : never"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"fileUrlPath": "packages/tlschema/src/styles/StyleProp.ts",
|
||||
"releaseTag": "Public",
|
||||
"name": "StylePropValue",
|
||||
"typeParameters": [
|
||||
{
|
||||
"typeParameterName": "T",
|
||||
"constraintTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 3
|
||||
},
|
||||
"defaultTypeTokenRange": {
|
||||
"startIndex": 0,
|
||||
"endIndex": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"typeTokenRange": {
|
||||
"startIndex": 4,
|
||||
"endIndex": 7
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Variable",
|
||||
"canonicalReference": "@tldraw/tlschema!textShapeProps:var",
|
||||
|
|
|
@ -27,14 +27,14 @@ export const assetIdValidator = idValidator<TLAssetId>('asset')
|
|||
export function createAssetValidator<Type extends string, Props extends JsonObject>(
|
||||
type: Type,
|
||||
props: T.Validator<Props>
|
||||
): T.ObjectValidator<{
|
||||
) {
|
||||
return T.object<{
|
||||
id: TLAssetId
|
||||
typeName: 'asset'
|
||||
type: Type
|
||||
props: Props
|
||||
meta: JsonObject
|
||||
}> {
|
||||
return T.object({
|
||||
}>({
|
||||
id: assetIdValidator,
|
||||
typeName: T.literal('asset'),
|
||||
type: T.literal(type),
|
||||
|
|
|
@ -14,11 +14,16 @@ import { createShapeRecordType, getShapePropKeysByStyle } from './records/TLShap
|
|||
import { storeMigrations } from './store-migrations'
|
||||
import { StyleProp } from './styles/StyleProp'
|
||||
|
||||
type AnyValidator = {
|
||||
validate: (prop: any) => any
|
||||
validateUsingKnownGoodVersion?: (prevVersion: any, newVersion: any) => any
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type SchemaShapeInfo = {
|
||||
migrations?: Migrations
|
||||
props?: Record<string, { validate: (prop: any) => any }>
|
||||
meta?: Record<string, { validate: (prop: any) => any }>
|
||||
props?: Record<string, AnyValidator>
|
||||
meta?: Record<string, AnyValidator>
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
|
|
@ -136,7 +136,7 @@ export {
|
|||
type TLTextShapeProps,
|
||||
} from './shapes/TLTextShape'
|
||||
export { videoShapeMigrations, videoShapeProps, type TLVideoShape } from './shapes/TLVideoShape'
|
||||
export { EnumStyleProp, StyleProp } from './styles/StyleProp'
|
||||
export { EnumStyleProp, StyleProp, type StylePropValue } from './styles/StyleProp'
|
||||
export {
|
||||
DefaultColorStyle,
|
||||
DefaultColorThemePalette,
|
||||
|
|
|
@ -52,8 +52,8 @@ export function createShapeValidator<
|
|||
type: T.literal(type),
|
||||
isLocked: T.boolean,
|
||||
opacity: opacityValidator,
|
||||
props: props ? T.object(props) : (T.jsonValue as T.ObjectValidator<Props>),
|
||||
meta: meta ? T.object(meta) : (T.jsonValue as T.ObjectValidator<Meta>),
|
||||
props: props ? T.object(props) : (T.jsonValue as any),
|
||||
meta: meta ? T.object(meta) : (T.jsonValue as any),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -90,6 +90,14 @@ export class StyleProp<Type> implements T.Validatable<Type> {
|
|||
validate(value: unknown) {
|
||||
return this.type.validate(value)
|
||||
}
|
||||
|
||||
validateUsingKnownGoodVersion(prevValue: Type, newValue: unknown) {
|
||||
if (this.type.validateUsingKnownGoodVersion) {
|
||||
return this.type.validateUsingKnownGoodVersion(prevValue, newValue)
|
||||
} else {
|
||||
return this.validate(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -107,3 +115,8 @@ export class EnumStyleProp<T> extends StyleProp<T> {
|
|||
super(id, defaultValue, T.literalEnum(...values))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type StylePropValue<T extends StyleProp<any>> = T extends StyleProp<infer U> ? U : never
|
||||
|
|
|
@ -86,7 +86,11 @@ const number: Validator<number>;
|
|||
// @public
|
||||
function object<Shape extends object>(config: {
|
||||
readonly [K in keyof Shape]: Validatable<Shape[K]>;
|
||||
}): ObjectValidator<Shape>;
|
||||
}): ObjectValidator<{
|
||||
[P in ExtractRequiredKeys<Shape>]: Shape[P];
|
||||
} & {
|
||||
[P in ExtractOptionalKeys<Shape>]?: Shape[P];
|
||||
}>;
|
||||
|
||||
// @public (undocumented)
|
||||
export class ObjectValidator<Shape extends object> extends Validator<Shape> {
|
||||
|
@ -136,6 +140,7 @@ declare namespace T {
|
|||
nullable,
|
||||
literalEnum,
|
||||
ValidatorFn,
|
||||
ValidatorUsingKnownGoodVersionFn,
|
||||
Validatable,
|
||||
ValidationError,
|
||||
TypeOf,
|
||||
|
@ -166,7 +171,7 @@ declare namespace T {
|
|||
export { T }
|
||||
|
||||
// @public (undocumented)
|
||||
type TypeOf<V extends Validatable<unknown>> = V extends Validatable<infer T> ? T : never;
|
||||
type TypeOf<V extends Validatable<any>> = V extends Validatable<infer T> ? T : never;
|
||||
|
||||
// @public
|
||||
function union<Key extends string, Config extends UnionValidatorConfig<Key, Config>>(key: Key, config: Config): UnionValidator<Key, Config>;
|
||||
|
@ -187,6 +192,7 @@ const unknownObject: Validator<Record<string, unknown>>;
|
|||
// @public (undocumented)
|
||||
type Validatable<T> = {
|
||||
validate: (value: unknown) => T;
|
||||
validateUsingKnownGoodVersion?: (knownGoodValue: T, newValue: unknown) => T;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
|
@ -202,7 +208,7 @@ class ValidationError extends Error {
|
|||
|
||||
// @public (undocumented)
|
||||
export class Validator<T> implements Validatable<T> {
|
||||
constructor(validationFn: ValidatorFn<T>);
|
||||
constructor(validationFn: ValidatorFn<T>, validateUsingKnownGoodVersionFn?: undefined | ValidatorUsingKnownGoodVersionFn<T, T>);
|
||||
check(name: string, checkFn: (value: T) => void): Validator<T>;
|
||||
// (undocumented)
|
||||
check(checkFn: (value: T) => void): Validator<T>;
|
||||
|
@ -212,12 +218,19 @@ export class Validator<T> implements Validatable<T> {
|
|||
refine<U>(otherValidationFn: (value: T) => U): Validator<U>;
|
||||
validate(value: unknown): T;
|
||||
// (undocumented)
|
||||
validateUsingKnownGoodVersion(knownGoodValue: T, newValue: unknown): T;
|
||||
// (undocumented)
|
||||
readonly validateUsingKnownGoodVersionFn?: undefined | ValidatorUsingKnownGoodVersionFn<T, T>;
|
||||
// (undocumented)
|
||||
readonly validationFn: ValidatorFn<T>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
type ValidatorFn<T> = (value: unknown) => T;
|
||||
|
||||
// @public (undocumented)
|
||||
type ValidatorUsingKnownGoodVersionFn<In, Out = In> = (knownGoodValue: In, value: unknown) => Out;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
|
||||
```
|
||||
|
|
|
@ -2142,7 +2142,25 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<Shape>"
|
||||
"text": "<{\n [P in "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "ExtractRequiredKeys",
|
||||
"canonicalReference": "@tldraw/validate!~ExtractRequiredKeys:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<Shape>]: Shape[P];\n} & {\n [P in "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "ExtractOptionalKeys",
|
||||
"canonicalReference": "@tldraw/validate!~ExtractOptionalKeys:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<Shape>]?: Shape[P];\n}>"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -2152,7 +2170,7 @@
|
|||
"fileUrlPath": "packages/validate/src/lib/validation.ts",
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 7,
|
||||
"endIndex": 9
|
||||
"endIndex": 13
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"overloadIndex": 1,
|
||||
|
@ -2722,7 +2740,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<unknown>"
|
||||
"text": "<any>"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -3193,7 +3211,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "{\n validate: (value: unknown) => T;\n}"
|
||||
"text": "{\n validate: (value: unknown) => T;\n validateUsingKnownGoodVersion?: (knownGoodValue: T, newValue: unknown) => T;\n}"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -3461,6 +3479,23 @@
|
|||
"kind": "Content",
|
||||
"text": "<T>"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", validateUsingKnownGoodVersionFn?: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "undefined | "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "ValidatorUsingKnownGoodVersionFn",
|
||||
"canonicalReference": "@tldraw/validate!T.ValidatorUsingKnownGoodVersionFn:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<T, T>"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ");"
|
||||
|
@ -3477,6 +3512,14 @@
|
|||
"endIndex": 3
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "validateUsingKnownGoodVersionFn",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 4,
|
||||
"endIndex": 7
|
||||
},
|
||||
"isOptional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -3841,6 +3884,109 @@
|
|||
"isAbstract": false,
|
||||
"name": "validate"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/validate!T.Validator#validateUsingKnownGoodVersion:member(1)",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "validateUsingKnownGoodVersion(knownGoodValue: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "T"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", newValue: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "unknown"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "T"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [
|
||||
{
|
||||
"parameterName": "knownGoodValue",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "newValue",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 3,
|
||||
"endIndex": 4
|
||||
},
|
||||
"isOptional": false
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "validateUsingKnownGoodVersion"
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "@tldraw/validate!T.Validator#validateUsingKnownGoodVersionFn:member",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "readonly validateUsingKnownGoodVersionFn?: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "undefined | "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "ValidatorUsingKnownGoodVersionFn",
|
||||
"canonicalReference": "@tldraw/validate!T.ValidatorUsingKnownGoodVersionFn:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<T, T>"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isReadonly": true,
|
||||
"isOptional": true,
|
||||
"releaseTag": "Public",
|
||||
"name": "validateUsingKnownGoodVersionFn",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 4
|
||||
},
|
||||
"isStatic": false,
|
||||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "@tldraw/validate!T.Validator#validationFn:member",
|
||||
|
@ -3922,6 +4068,64 @@
|
|||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "TypeAlias",
|
||||
"canonicalReference": "@tldraw/validate!T.ValidatorUsingKnownGoodVersionFn:type",
|
||||
"docComment": "/**\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "export type ValidatorUsingKnownGoodVersionFn<In, Out = "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "In"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "> = "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "(knownGoodValue: In, value: unknown) => Out"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"fileUrlPath": "packages/validate/src/lib/validation.ts",
|
||||
"releaseTag": "Public",
|
||||
"name": "ValidatorUsingKnownGoodVersionFn",
|
||||
"typeParameters": [
|
||||
{
|
||||
"typeParameterName": "In",
|
||||
"constraintTokenRange": {
|
||||
"startIndex": 0,
|
||||
"endIndex": 0
|
||||
},
|
||||
"defaultTypeTokenRange": {
|
||||
"startIndex": 0,
|
||||
"endIndex": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"typeParameterName": "Out",
|
||||
"constraintTokenRange": {
|
||||
"startIndex": 0,
|
||||
"endIndex": 0
|
||||
},
|
||||
"defaultTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"typeTokenRange": {
|
||||
"startIndex": 3,
|
||||
"endIndex": 4
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -4224,6 +4428,23 @@
|
|||
"kind": "Content",
|
||||
"text": "<T>"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", validateUsingKnownGoodVersionFn?: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "undefined | "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "ValidatorUsingKnownGoodVersionFn",
|
||||
"canonicalReference": "@tldraw/validate!T.ValidatorUsingKnownGoodVersionFn:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<T, T>"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ");"
|
||||
|
@ -4240,6 +4461,14 @@
|
|||
"endIndex": 3
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "validateUsingKnownGoodVersionFn",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 4,
|
||||
"endIndex": 7
|
||||
},
|
||||
"isOptional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -4604,6 +4833,109 @@
|
|||
"isAbstract": false,
|
||||
"name": "validate"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/validate!Validator#validateUsingKnownGoodVersion:member(1)",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "validateUsingKnownGoodVersion(knownGoodValue: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "T"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", newValue: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "unknown"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "T"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [
|
||||
{
|
||||
"parameterName": "knownGoodValue",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "newValue",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 3,
|
||||
"endIndex": 4
|
||||
},
|
||||
"isOptional": false
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "validateUsingKnownGoodVersion"
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "@tldraw/validate!Validator#validateUsingKnownGoodVersionFn:member",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "readonly validateUsingKnownGoodVersionFn?: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "undefined | "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "ValidatorUsingKnownGoodVersionFn",
|
||||
"canonicalReference": "@tldraw/validate!T.ValidatorUsingKnownGoodVersionFn:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<T, T>"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isReadonly": true,
|
||||
"isOptional": true,
|
||||
"releaseTag": "Public",
|
||||
"name": "validateUsingKnownGoodVersionFn",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 4
|
||||
},
|
||||
"isStatic": false,
|
||||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "@tldraw/validate!Validator#validationFn:member",
|
||||
|
|
|
@ -53,6 +53,7 @@
|
|||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"lazyrepo": "0.0.0-alpha.27"
|
||||
"lazyrepo": "0.0.0-alpha.27",
|
||||
"lodash.isequal": "^4.5.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,9 +9,26 @@ import {
|
|||
|
||||
/** @public */
|
||||
export type ValidatorFn<T> = (value: unknown) => T
|
||||
/** @public */
|
||||
export type ValidatorUsingKnownGoodVersionFn<In, Out = In> = (
|
||||
knownGoodValue: In,
|
||||
value: unknown
|
||||
) => Out
|
||||
|
||||
/** @public */
|
||||
export type Validatable<T> = { validate: (value: unknown) => T }
|
||||
export type Validatable<T> = {
|
||||
validate: (value: unknown) => T
|
||||
/**
|
||||
* This is a performance optimizing version of validate that can use a previous
|
||||
* version of the value to avoid revalidating every part of the new value if
|
||||
* any part of it has not changed since the last validation.
|
||||
*
|
||||
* If the value has not changed but is not referentially equal, the function
|
||||
* should return the previous value.
|
||||
* @returns
|
||||
*/
|
||||
validateUsingKnownGoodVersion?: (knownGoodValue: T, newValue: unknown) => T
|
||||
}
|
||||
|
||||
function formatPath(path: ReadonlyArray<number | string>): string | null {
|
||||
if (!path.length) {
|
||||
|
@ -92,11 +109,14 @@ function typeToString(value: unknown): string {
|
|||
}
|
||||
|
||||
/** @public */
|
||||
export type TypeOf<V extends Validatable<unknown>> = V extends Validatable<infer T> ? T : never
|
||||
export type TypeOf<V extends Validatable<any>> = V extends Validatable<infer T> ? T : never
|
||||
|
||||
/** @public */
|
||||
export class Validator<T> implements Validatable<T> {
|
||||
constructor(readonly validationFn: ValidatorFn<T>) {}
|
||||
constructor(
|
||||
readonly validationFn: ValidatorFn<T>,
|
||||
readonly validateUsingKnownGoodVersionFn?: ValidatorUsingKnownGoodVersionFn<T>
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Asserts that the passed value is of the correct type and returns it. The returned value is
|
||||
|
@ -110,6 +130,18 @@ export class Validator<T> implements Validatable<T> {
|
|||
return validated
|
||||
}
|
||||
|
||||
validateUsingKnownGoodVersion(knownGoodValue: T, newValue: unknown): T {
|
||||
if (Object.is(knownGoodValue, newValue)) {
|
||||
return knownGoodValue as T
|
||||
}
|
||||
|
||||
if (this.validateUsingKnownGoodVersionFn) {
|
||||
return this.validateUsingKnownGoodVersionFn(knownGoodValue, newValue)
|
||||
}
|
||||
|
||||
return this.validate(newValue)
|
||||
}
|
||||
|
||||
/** Checks that the passed value is of the correct type. */
|
||||
isValid(value: unknown): value is T {
|
||||
try {
|
||||
|
@ -141,9 +173,19 @@ export class Validator<T> implements Validatable<T> {
|
|||
* if the value can't be converted to the new type, or return the new type otherwise.
|
||||
*/
|
||||
refine<U>(otherValidationFn: (value: T) => U): Validator<U> {
|
||||
return new Validator((value) => {
|
||||
return new Validator(
|
||||
(value) => {
|
||||
return otherValidationFn(this.validate(value))
|
||||
})
|
||||
},
|
||||
|
||||
(knownGoodValue, newValue) => {
|
||||
const validated = this.validateUsingKnownGoodVersion(knownGoodValue as any, newValue)
|
||||
if (Object.is(knownGoodValue, validated)) {
|
||||
return knownGoodValue
|
||||
}
|
||||
return otherValidationFn(validated)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -179,13 +221,40 @@ export class Validator<T> implements Validatable<T> {
|
|||
/** @public */
|
||||
export class ArrayOfValidator<T> extends Validator<T[]> {
|
||||
constructor(readonly itemValidator: Validatable<T>) {
|
||||
super((value) => {
|
||||
super(
|
||||
(value) => {
|
||||
const arr = array.validate(value)
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
prefixError(i, () => itemValidator.validate(arr[i]))
|
||||
}
|
||||
return arr as T[]
|
||||
})
|
||||
},
|
||||
(knownGoodValue, newValue) => {
|
||||
if (!itemValidator.validateUsingKnownGoodVersion) return this.validate(newValue)
|
||||
const arr = array.validate(newValue)
|
||||
let isDifferent = knownGoodValue.length !== arr.length
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const item = arr[i]
|
||||
if (i >= knownGoodValue.length) {
|
||||
isDifferent = true
|
||||
prefixError(i, () => itemValidator.validate(item))
|
||||
continue
|
||||
}
|
||||
// sneaky quick check here to avoid the prefix + validator overhead
|
||||
if (Object.is(knownGoodValue[i], item)) {
|
||||
continue
|
||||
}
|
||||
const checkedItem = prefixError(i, () =>
|
||||
itemValidator.validateUsingKnownGoodVersion!(knownGoodValue[i], item)
|
||||
)
|
||||
if (!Object.is(checkedItem, knownGoodValue[i])) {
|
||||
isDifferent = true
|
||||
}
|
||||
}
|
||||
|
||||
return isDifferent ? (newValue as T[]) : knownGoodValue
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
nonEmpty() {
|
||||
|
@ -213,7 +282,8 @@ export class ObjectValidator<Shape extends object> extends Validator<Shape> {
|
|||
},
|
||||
private readonly shouldAllowUnknownProperties = false
|
||||
) {
|
||||
super((object) => {
|
||||
super(
|
||||
(object) => {
|
||||
if (typeof object !== 'object' || object === null) {
|
||||
throw new ValidationError(`Expected object, got ${typeToString(object)}`)
|
||||
}
|
||||
|
@ -233,7 +303,47 @@ export class ObjectValidator<Shape extends object> extends Validator<Shape> {
|
|||
}
|
||||
|
||||
return object as Shape
|
||||
},
|
||||
(knownGoodValue, newValue) => {
|
||||
if (typeof newValue !== 'object' || newValue === null) {
|
||||
throw new ValidationError(`Expected object, got ${typeToString(newValue)}`)
|
||||
}
|
||||
|
||||
let isDifferent = false
|
||||
|
||||
for (const [key, validator] of Object.entries(config)) {
|
||||
const prev = getOwnProperty(knownGoodValue, key)
|
||||
const next = getOwnProperty(newValue, key)
|
||||
// sneaky quick check here to avoid the prefix + validator overhead
|
||||
if (Object.is(prev, next)) {
|
||||
continue
|
||||
}
|
||||
const checked = prefixError(key, () => {
|
||||
return (validator as Validator<unknown>).validateUsingKnownGoodVersion(prev, next)
|
||||
})
|
||||
if (!Object.is(checked, prev)) {
|
||||
isDifferent = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldAllowUnknownProperties) {
|
||||
for (const key of Object.keys(newValue)) {
|
||||
if (!hasOwnProperty(config, key)) {
|
||||
throw new ValidationError(`Unexpected property`, [key])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(knownGoodValue)) {
|
||||
if (!hasOwnProperty(newValue, key)) {
|
||||
isDifferent = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return isDifferent ? (newValue as Shape) : knownGoodValue
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
allowUnknownProperties() {
|
||||
|
@ -257,7 +367,7 @@ export class ObjectValidator<Shape extends object> extends Validator<Shape> {
|
|||
extend<Extension extends Record<string, unknown>>(extension: {
|
||||
readonly [K in keyof Extension]: Validatable<Extension[K]>
|
||||
}): ObjectValidator<Shape & Extension> {
|
||||
return new ObjectValidator({ ...this.config, ...extension }) as ObjectValidator<
|
||||
return new ObjectValidator({ ...this.config, ...extension }) as any as ObjectValidator<
|
||||
Shape & Extension
|
||||
>
|
||||
}
|
||||
|
@ -280,26 +390,62 @@ export class UnionValidator<
|
|||
private readonly config: Config,
|
||||
private readonly unknownValueValidation: (value: object, variant: string) => UnknownValue
|
||||
) {
|
||||
super((input) => {
|
||||
if (typeof input !== 'object' || input === null) {
|
||||
throw new ValidationError(`Expected an object, got ${typeToString(input)}`, [])
|
||||
}
|
||||
super(
|
||||
(input) => {
|
||||
this.expectObject(input)
|
||||
|
||||
const variant = getOwnProperty(input, key) as keyof Config | undefined
|
||||
if (typeof variant !== 'string') {
|
||||
throw new ValidationError(
|
||||
`Expected a string for key "${key}", got ${typeToString(variant)}`
|
||||
)
|
||||
}
|
||||
|
||||
const matchingSchema = hasOwnProperty(config, variant) ? config[variant] : undefined
|
||||
const { matchingSchema, variant } = this.getMatchingSchemaAndVariant(input)
|
||||
if (matchingSchema === undefined) {
|
||||
return this.unknownValueValidation(input, variant)
|
||||
}
|
||||
|
||||
return prefixError(`(${key} = ${variant})`, () => matchingSchema.validate(input))
|
||||
},
|
||||
(prevValue, newValue) => {
|
||||
this.expectObject(newValue)
|
||||
this.expectObject(prevValue)
|
||||
|
||||
const { matchingSchema, variant } = this.getMatchingSchemaAndVariant(newValue)
|
||||
if (matchingSchema === undefined) {
|
||||
return this.unknownValueValidation(newValue, variant)
|
||||
}
|
||||
|
||||
if (getOwnProperty(prevValue, key) !== getOwnProperty(newValue, key)) {
|
||||
// the type has changed so bail out and do a regular validation
|
||||
return prefixError(`(${key} = ${variant})`, () => matchingSchema.validate(newValue))
|
||||
}
|
||||
|
||||
return prefixError(`(${key} = ${variant})`, () => {
|
||||
if (matchingSchema.validateUsingKnownGoodVersion) {
|
||||
return matchingSchema.validateUsingKnownGoodVersion(prevValue, newValue)
|
||||
} else {
|
||||
return matchingSchema.validate(newValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private expectObject(value: unknown): asserts value is object {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
throw new ValidationError(`Expected an object, got ${typeToString(value)}`, [])
|
||||
}
|
||||
}
|
||||
|
||||
private getMatchingSchemaAndVariant(object: object): {
|
||||
matchingSchema: Validatable<any> | undefined
|
||||
variant: string
|
||||
} {
|
||||
const variant = getOwnProperty(object, this.key) as keyof Config | undefined
|
||||
if (typeof variant !== 'string') {
|
||||
throw new ValidationError(
|
||||
`Expected a string for key "${this.key}", got ${typeToString(variant)}`
|
||||
)
|
||||
}
|
||||
|
||||
const matchingSchema = hasOwnProperty(this.config, variant) ? this.config[variant] : undefined
|
||||
return { matchingSchema, variant }
|
||||
}
|
||||
|
||||
validateUnknownVariants<Unknown>(
|
||||
unknownValueValidation: (value: object, variant: string) => Unknown
|
||||
|
@ -314,7 +460,8 @@ export class DictValidator<Key extends string, Value> extends Validator<Record<K
|
|||
public readonly keyValidator: Validatable<Key>,
|
||||
public readonly valueValidator: Validatable<Value>
|
||||
) {
|
||||
super((object) => {
|
||||
super(
|
||||
(object) => {
|
||||
if (typeof object !== 'object' || object === null) {
|
||||
throw new ValidationError(`Expected object, got ${typeToString(object)}`)
|
||||
}
|
||||
|
@ -327,7 +474,51 @@ export class DictValidator<Key extends string, Value> extends Validator<Record<K
|
|||
}
|
||||
|
||||
return object as Record<Key, Value>
|
||||
},
|
||||
(knownGoodValue, newValue) => {
|
||||
if (typeof newValue !== 'object' || newValue === null) {
|
||||
throw new ValidationError(`Expected object, got ${typeToString(newValue)}`)
|
||||
}
|
||||
|
||||
let isDifferent = false
|
||||
|
||||
for (const [key, value] of Object.entries(newValue)) {
|
||||
if (!hasOwnProperty(knownGoodValue, key)) {
|
||||
isDifferent = true
|
||||
prefixError(key, () => {
|
||||
keyValidator.validate(key)
|
||||
valueValidator.validate(value)
|
||||
})
|
||||
continue
|
||||
}
|
||||
const prev = getOwnProperty(knownGoodValue, key)
|
||||
const next = value
|
||||
// sneaky quick check here to avoid the prefix + validator overhead
|
||||
if (Object.is(prev, next)) {
|
||||
continue
|
||||
}
|
||||
const checked = prefixError(key, () => {
|
||||
if (valueValidator.validateUsingKnownGoodVersion) {
|
||||
return valueValidator.validateUsingKnownGoodVersion(prev as any, next)
|
||||
} else {
|
||||
return valueValidator.validate(next)
|
||||
}
|
||||
})
|
||||
if (!Object.is(checked, prev)) {
|
||||
isDifferent = true
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(knownGoodValue)) {
|
||||
if (!hasOwnProperty(newValue, key)) {
|
||||
isDifferent = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return isDifferent ? (newValue as Record<Key, Value>) : knownGoodValue
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -477,6 +668,14 @@ export const unknownObject = new Validator<Record<string, unknown>>((value) => {
|
|||
return value as Record<string, unknown>
|
||||
})
|
||||
|
||||
type ExtractRequiredKeys<T extends object> = {
|
||||
[K in keyof T]: undefined extends T[K] ? never : K
|
||||
}[keyof T]
|
||||
|
||||
type ExtractOptionalKeys<T extends object> = {
|
||||
[K in keyof T]: undefined extends T[K] ? K : never
|
||||
}[keyof T]
|
||||
|
||||
/**
|
||||
* Validate an object has a particular shape.
|
||||
*
|
||||
|
@ -484,8 +683,18 @@ export const unknownObject = new Validator<Record<string, unknown>>((value) => {
|
|||
*/
|
||||
export function object<Shape extends object>(config: {
|
||||
readonly [K in keyof Shape]: Validatable<Shape[K]>
|
||||
}): ObjectValidator<Shape> {
|
||||
return new ObjectValidator(config)
|
||||
}): ObjectValidator<
|
||||
{ [P in ExtractRequiredKeys<Shape>]: Shape[P] } & { [P in ExtractOptionalKeys<Shape>]?: Shape[P] }
|
||||
> {
|
||||
return new ObjectValidator(config) as any
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
(value.constructor === Object || !value.constructor)
|
||||
)
|
||||
}
|
||||
|
||||
function isValidJson(value: any): value is JsonValue {
|
||||
|
@ -502,7 +711,7 @@ function isValidJson(value: any): value is JsonValue {
|
|||
return value.every(isValidJson)
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
if (isPlainObject(value)) {
|
||||
return Object.values(value).every(isValidJson)
|
||||
}
|
||||
|
||||
|
@ -514,13 +723,64 @@ function isValidJson(value: any): value is JsonValue {
|
|||
*
|
||||
* @public
|
||||
*/
|
||||
export const jsonValue = new Validator<JsonValue>((value): JsonValue => {
|
||||
export const jsonValue: Validator<JsonValue> = new Validator<JsonValue>(
|
||||
(value): JsonValue => {
|
||||
if (isValidJson(value)) {
|
||||
return value as JsonValue
|
||||
}
|
||||
|
||||
throw new ValidationError(`Expected json serializable value, got ${typeof value}`)
|
||||
})
|
||||
},
|
||||
(knownGoodValue, newValue) => {
|
||||
if (Array.isArray(knownGoodValue) && Array.isArray(newValue)) {
|
||||
let isDifferent = knownGoodValue.length !== newValue.length
|
||||
for (let i = 0; i < newValue.length; i++) {
|
||||
if (i >= knownGoodValue.length) {
|
||||
isDifferent = true
|
||||
jsonValue.validate(newValue[i])
|
||||
continue
|
||||
}
|
||||
const prev = knownGoodValue[i]
|
||||
const next = newValue[i]
|
||||
if (Object.is(prev, next)) {
|
||||
continue
|
||||
}
|
||||
const checked = jsonValue.validateUsingKnownGoodVersion!(prev, next)
|
||||
if (!Object.is(checked, prev)) {
|
||||
isDifferent = true
|
||||
}
|
||||
}
|
||||
return isDifferent ? (newValue as JsonValue) : knownGoodValue
|
||||
} else if (isPlainObject(knownGoodValue) && isPlainObject(newValue)) {
|
||||
let isDifferent = false
|
||||
for (const key of Object.keys(newValue)) {
|
||||
if (!hasOwnProperty(knownGoodValue, key)) {
|
||||
isDifferent = true
|
||||
jsonValue.validate(newValue[key])
|
||||
continue
|
||||
}
|
||||
const prev = knownGoodValue[key]
|
||||
const next = newValue[key]
|
||||
if (Object.is(prev, next)) {
|
||||
continue
|
||||
}
|
||||
const checked = jsonValue.validateUsingKnownGoodVersion!(prev!, next)
|
||||
if (!Object.is(checked, prev)) {
|
||||
isDifferent = true
|
||||
}
|
||||
}
|
||||
for (const key of Object.keys(knownGoodValue)) {
|
||||
if (!hasOwnProperty(newValue, key)) {
|
||||
isDifferent = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return isDifferent ? (newValue as JsonValue) : knownGoodValue
|
||||
} else {
|
||||
return jsonValue.validate(newValue)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Validate an object has a particular shape.
|
||||
|
@ -581,15 +841,21 @@ export function model<T extends { readonly id: string }>(
|
|||
name: string,
|
||||
validator: Validatable<T>
|
||||
): Validator<T> {
|
||||
return new Validator((value) => {
|
||||
const prefix =
|
||||
value && typeof value === 'object' && 'id' in value && typeof value.id === 'string'
|
||||
? `${name}(id = ${value.id})`
|
||||
: name
|
||||
|
||||
return prefixError(prefix, () => validator.validate(value))
|
||||
return new Validator(
|
||||
(value) => {
|
||||
return prefixError(name, () => validator.validate(value))
|
||||
},
|
||||
(prevValue, newValue) => {
|
||||
return prefixError(name, () => {
|
||||
if (validator.validateUsingKnownGoodVersion) {
|
||||
return validator.validateUsingKnownGoodVersion(prevValue, newValue)
|
||||
} else {
|
||||
return validator.validate(newValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function setEnum<T>(values: ReadonlySet<T>): Validator<T> {
|
||||
|
@ -604,18 +870,37 @@ export function setEnum<T>(values: ReadonlySet<T>): Validator<T> {
|
|||
|
||||
/** @public */
|
||||
export function optional<T>(validator: Validatable<T>): Validator<T | undefined> {
|
||||
return new Validator((value) => {
|
||||
return new Validator(
|
||||
(value) => {
|
||||
if (value === undefined) return undefined
|
||||
return validator.validate(value)
|
||||
})
|
||||
},
|
||||
(knownGoodValue, newValue) => {
|
||||
if (knownGoodValue === undefined && newValue === undefined) return undefined
|
||||
if (newValue === undefined) return undefined
|
||||
if (validator.validateUsingKnownGoodVersion && knownGoodValue !== undefined) {
|
||||
return validator.validateUsingKnownGoodVersion(knownGoodValue as T, newValue)
|
||||
}
|
||||
return validator.validate(newValue)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function nullable<T>(validator: Validatable<T>): Validator<T | null> {
|
||||
return new Validator((value) => {
|
||||
return new Validator(
|
||||
(value) => {
|
||||
if (value === null) return null
|
||||
return validator.validate(value)
|
||||
})
|
||||
},
|
||||
(knownGoodValue, newValue) => {
|
||||
if (newValue === null) return null
|
||||
if (validator.validateUsingKnownGoodVersion && knownGoodValue !== null) {
|
||||
return validator.validateUsingKnownGoodVersion(knownGoodValue as T, newValue)
|
||||
}
|
||||
return validator.validate(newValue)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
|
379
packages/validate/src/test/validation.fuzz.test.ts
Normal file
379
packages/validate/src/test/validation.fuzz.test.ts
Normal file
|
@ -0,0 +1,379 @@
|
|||
import { mapObjectMapValues } from '@tldraw/utils'
|
||||
import isEqual from 'lodash.isequal'
|
||||
import { T, Validator } from '..'
|
||||
|
||||
class RandomSource {
|
||||
private seed: number
|
||||
|
||||
constructor(seed: number) {
|
||||
this.seed = seed
|
||||
}
|
||||
|
||||
nextFloat(): number {
|
||||
this.seed = (this.seed * 9301 + 49297) % 233280
|
||||
return this.seed / 233280
|
||||
}
|
||||
|
||||
nextInt(max: number): number {
|
||||
return Math.floor(this.nextFloat() * max)
|
||||
}
|
||||
|
||||
nextIntInRange(min: number, max: number): number {
|
||||
return this.nextInt(max - min) + min
|
||||
}
|
||||
|
||||
nextId(): string {
|
||||
return this.nextInt(Number.MAX_SAFE_INTEGER).toString(36)
|
||||
}
|
||||
|
||||
selectOne<T>(arr: readonly T[]): T {
|
||||
return arr[this.nextInt(arr.length)]
|
||||
}
|
||||
|
||||
choice(probability: number): boolean {
|
||||
return this.nextFloat() < probability
|
||||
}
|
||||
|
||||
executeOne<Result>(
|
||||
_choices: Record<string, (() => Result) | { weight?: number; do(): Result }>
|
||||
): Result {
|
||||
const choices = Object.values(_choices).map((choice) => {
|
||||
if (typeof choice === 'function') {
|
||||
return { weight: 1, do: choice }
|
||||
}
|
||||
return choice
|
||||
})
|
||||
const totalWeight = Object.values(choices).reduce(
|
||||
(total, choice) => total + (choice.weight ?? 1),
|
||||
0
|
||||
)
|
||||
const randomWeight = this.nextInt(totalWeight)
|
||||
let weight = 0
|
||||
for (const choice of Object.values(choices)) {
|
||||
weight += choice.weight ?? 1
|
||||
if (randomWeight < weight) {
|
||||
return choice.do()
|
||||
}
|
||||
}
|
||||
throw new Error('unreachable')
|
||||
}
|
||||
|
||||
nextPropertyName(): string {
|
||||
return this.selectOne(['foo', 'bar', 'baz', 'qux', 'mux', 'bah'])
|
||||
}
|
||||
|
||||
nextJsonValue(): any {
|
||||
return this.executeOne<any>({
|
||||
string: { weight: 1, do: () => this.nextId() },
|
||||
number: { weight: 1, do: () => this.nextFloat() },
|
||||
integer: { weight: 1, do: () => this.nextInt(100) },
|
||||
boolean: { weight: 1, do: () => this.choice(0.5) },
|
||||
null: { weight: 1, do: () => null },
|
||||
array: {
|
||||
weight: 1,
|
||||
do: () => {
|
||||
const numItems = this.nextInt(4)
|
||||
const result = []
|
||||
for (let i = 0; i < numItems; i++) {
|
||||
result.push(this.nextJsonValue())
|
||||
}
|
||||
return result
|
||||
},
|
||||
},
|
||||
object: {
|
||||
weight: 1,
|
||||
do: () => {
|
||||
const numItems = this.nextInt(4)
|
||||
const result = {} as any
|
||||
for (let i = 0; i < numItems; i++) {
|
||||
result[this.nextPropertyName()] = this.nextJsonValue()
|
||||
}
|
||||
return result
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
nextTestType(depth: number): TestType {
|
||||
if (depth >= 3) {
|
||||
return this.selectOne(Object.values(builtinTypes))
|
||||
}
|
||||
return this.executeOne<TestType>({
|
||||
primitive: () => this.selectOne(Object.values(builtinTypes)),
|
||||
array: () => generateArrayType(this, depth),
|
||||
object: () => generateObjectType(this, {}, depth),
|
||||
union: () => generateUnionType(this, depth),
|
||||
dict: () => generateDictType(this, depth),
|
||||
model: () => {
|
||||
const objType = generateObjectType(this, {}, depth)
|
||||
const name = this.nextPropertyName()
|
||||
return {
|
||||
...objType,
|
||||
validator: T.model(name, objType.validator),
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type TestType = {
|
||||
validator: T.Validator<any>
|
||||
generateValid: (source: RandomSource) => any
|
||||
generateInvalid: (source: RandomSource) => any
|
||||
}
|
||||
const builtinTypes = {
|
||||
string: {
|
||||
validator: T.string,
|
||||
generateValid: (source) => source.selectOne(['a', 'b', 'c', 'd']),
|
||||
generateInvalid: (source) => source.selectOne([5, /regexp/, {}]),
|
||||
},
|
||||
number: {
|
||||
validator: T.number,
|
||||
generateValid: (source) => source.nextInt(5),
|
||||
generateInvalid: (source) => source.selectOne(['a', /num/]),
|
||||
},
|
||||
integer: {
|
||||
validator: T.integer,
|
||||
generateValid: (source) => source.nextInt(5),
|
||||
generateInvalid: (source) => source.selectOne([0.2, '3', 5n, /int/]),
|
||||
},
|
||||
json: {
|
||||
validator: T.jsonValue,
|
||||
generateValid: (source) => source.nextJsonValue(),
|
||||
generateInvalid: (source) => source.selectOne([/regexp/, 343n, { key: /regexp/ }]),
|
||||
},
|
||||
} as const satisfies Record<string, TestType>
|
||||
|
||||
function generateObjectType(
|
||||
source: RandomSource,
|
||||
injectProperties: Record<string, TestType>,
|
||||
depth: number
|
||||
): TestType {
|
||||
const numProperties = source.nextIntInRange(1, 5)
|
||||
const propertyTypes: Record<string, TestType> = {
|
||||
...injectProperties,
|
||||
}
|
||||
const optionalTypes = new Set<string>()
|
||||
const nullableTypes = new Set<string>()
|
||||
for (let i = 0; i < numProperties; i++) {
|
||||
const type = source.nextTestType(depth + 1)
|
||||
const name = source.nextPropertyName()
|
||||
if (source.choice(0.2)) {
|
||||
optionalTypes.add(name)
|
||||
}
|
||||
if (source.choice(0.2)) {
|
||||
nullableTypes.add(name)
|
||||
}
|
||||
let validator = type.validator
|
||||
if (nullableTypes.has(name)) {
|
||||
validator = validator.nullable()
|
||||
}
|
||||
if (optionalTypes.has(name)) {
|
||||
validator = validator.optional()
|
||||
}
|
||||
propertyTypes[name] = { ...type, validator }
|
||||
}
|
||||
|
||||
const generateValid = (source: RandomSource) => {
|
||||
const result = {} as any
|
||||
for (const [name, type] of Object.entries(propertyTypes)) {
|
||||
if (optionalTypes.has(name) && source.choice(0.2)) {
|
||||
continue
|
||||
} else if (nullableTypes.has(name) && source.choice(0.2)) {
|
||||
result[name] = null
|
||||
continue
|
||||
}
|
||||
result[name] = type.generateValid(source)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
return {
|
||||
validator: T.object(mapObjectMapValues(propertyTypes, (_, { validator }) => validator)),
|
||||
generateValid,
|
||||
generateInvalid: (source) => {
|
||||
return source.executeOne<any>({
|
||||
otherType: () =>
|
||||
source.executeOne<any>({
|
||||
string: () => source.selectOne(['a', 'b', 'c', 'd']),
|
||||
number: () => source.nextInt(5),
|
||||
array: () => [source.nextId(), source.nextFloat()],
|
||||
bool: () => true,
|
||||
}),
|
||||
missingProperty: () => {
|
||||
const val = generateValid(source)
|
||||
const keyToDelete = source.selectOne(
|
||||
Object.keys(val).filter((key) => !optionalTypes.has(key))
|
||||
)
|
||||
if (!keyToDelete) {
|
||||
// no non-optional properties, do a invalid property test instead
|
||||
val[keyToDelete] =
|
||||
propertyTypes[source.selectOne(Object.keys(propertyTypes))].generateInvalid(source)
|
||||
return val
|
||||
}
|
||||
delete val[keyToDelete]
|
||||
return val
|
||||
},
|
||||
extraProperty: () => {
|
||||
const val = generateValid(source)
|
||||
val[source.nextPropertyName() + '_'] = source.nextJsonValue()
|
||||
return val
|
||||
},
|
||||
invalidProperty: () => {
|
||||
const val = generateValid(source)
|
||||
const keyToChange = source.selectOne(Object.keys(propertyTypes))
|
||||
val[keyToChange] = propertyTypes[keyToChange].generateInvalid(source)
|
||||
return val
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function generateDictType(source: RandomSource, depth: number): TestType {
|
||||
const keyType = builtinTypes.string
|
||||
const keySet = ['a', 'b', 'c', 'd', 'e', 'f'] as const
|
||||
const valueType = source.nextTestType(depth + 1)
|
||||
|
||||
const validator = T.dict(keyType.validator, valueType.validator)
|
||||
|
||||
const generateValid = (source: RandomSource) => {
|
||||
const result = {} as any
|
||||
const numItems = source.nextInt(4)
|
||||
for (let i = 0; i < numItems; i++) {
|
||||
result[source.selectOne(keySet)] = valueType.generateValid(source)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
return {
|
||||
validator,
|
||||
generateValid,
|
||||
generateInvalid: (source) => {
|
||||
const result = generateValid(source)
|
||||
const key = source.selectOne(Object.keys(result)) ?? source.nextPropertyName()
|
||||
result[key] = valueType.generateInvalid(source)
|
||||
return result
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createLiteralType(value: string): TestType {
|
||||
return {
|
||||
validator: T.literal(value),
|
||||
generateValid: () => value,
|
||||
generateInvalid: (source) => source.selectOne(['_invalid_' + value, 2324, {}]),
|
||||
}
|
||||
}
|
||||
|
||||
function generateUnionType(source: RandomSource, depth: number): TestType {
|
||||
const key = source.selectOne(['type', 'name', 'kind'])
|
||||
const numMembers = source.nextIntInRange(1, 4)
|
||||
const members: TestType[] = []
|
||||
const unionMap: Record<string, Validator<any>> = {}
|
||||
for (let i = 0; i < numMembers; i++) {
|
||||
const id = source.nextId()
|
||||
const keyType = createLiteralType(id)
|
||||
const type = generateObjectType(source, { [key]: keyType }, depth + 1)
|
||||
members.push(type)
|
||||
unionMap[id] = type.validator
|
||||
}
|
||||
const validator = T.union(key, unionMap)
|
||||
|
||||
return {
|
||||
validator,
|
||||
generateValid: (source) => {
|
||||
const member = source.selectOne(members)
|
||||
return member.generateValid(source)
|
||||
},
|
||||
generateInvalid(source) {
|
||||
return source.executeOne<any>({
|
||||
otherType: () => source.selectOne(['_invalid_', 2324, {}]),
|
||||
badMember: {
|
||||
weight: 4,
|
||||
do() {
|
||||
const member = source.selectOne(members)
|
||||
return member.generateInvalid(source)
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function generateArrayType(source: RandomSource, depth: number): TestType {
|
||||
const valueType = source.nextTestType(depth + 1)
|
||||
const validator = T.arrayOf(valueType.validator)
|
||||
const generateValid = (source: RandomSource) => {
|
||||
const result = [] as any[]
|
||||
const numItems = source.nextInt(4)
|
||||
for (let i = 0; i < numItems; i++) {
|
||||
result.push(valueType.generateValid(source))
|
||||
}
|
||||
return result
|
||||
}
|
||||
return {
|
||||
validator,
|
||||
generateValid,
|
||||
generateInvalid: (source) => {
|
||||
return source.executeOne<any>({
|
||||
otherType: () =>
|
||||
source.executeOne<any>({
|
||||
string: () => source.nextId(),
|
||||
number: () => source.nextInt(100),
|
||||
object: () => ({ key: source.nextId() }),
|
||||
}),
|
||||
invalidItem: () => {
|
||||
const val = generateValid(source)
|
||||
if (val.length === 0) {
|
||||
return [valueType.generateInvalid(source)]
|
||||
}
|
||||
const indexToChange = source.nextInt(val.length)
|
||||
val[indexToChange] = valueType.generateInvalid(source)
|
||||
return val
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function runTest(seed: number) {
|
||||
test(`fuzz test with seed ${seed}`, () => {
|
||||
const source = new RandomSource(seed)
|
||||
const type = source.nextTestType(0)
|
||||
const oldValid = type.generateValid(source)
|
||||
const newValid = source.choice(0.5) ? type.generateValid(source) : oldValid
|
||||
const didChange = !isEqual(oldValid, newValid)
|
||||
const invalid = type.generateInvalid(source)
|
||||
|
||||
expect(type.validator.validate(oldValid)).toBe(oldValid)
|
||||
expect(type.validator.validate(newValid)).toBe(newValid)
|
||||
expect(() => {
|
||||
type.validator.validate(invalid)
|
||||
}).toThrow()
|
||||
|
||||
expect(() => type.validator.validateUsingKnownGoodVersion(oldValid, newValid)).not.toThrow()
|
||||
expect(() => type.validator.validateUsingKnownGoodVersion(oldValid, invalid)).toThrow()
|
||||
|
||||
if (didChange) {
|
||||
expect(type.validator.validateUsingKnownGoodVersion(oldValid, newValid)).toBe(newValid)
|
||||
} else {
|
||||
expect(type.validator.validateUsingKnownGoodVersion(oldValid, newValid)).toBe(oldValid)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const NUM_TESTS = 1000
|
||||
const source = new RandomSource(Math.random())
|
||||
|
||||
// 54480484
|
||||
const onlySeed: null | number = null
|
||||
|
||||
if (onlySeed) {
|
||||
runTest(onlySeed)
|
||||
} else {
|
||||
for (let i = 0; i < NUM_TESTS; i++) {
|
||||
const seed = source.nextInt(100000000)
|
||||
runTest(seed)
|
||||
}
|
||||
}
|
|
@ -59,7 +59,7 @@ describe('validations', () => {
|
|||
x: 132,
|
||||
y: NaN,
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"At shape().y: Expected a number, got NaN"`)
|
||||
).toThrowErrorMatchingInlineSnapshot(`"At shape.y: Expected a number, got NaN"`)
|
||||
|
||||
expect(() =>
|
||||
T.model(
|
||||
|
@ -70,7 +70,7 @@ describe('validations', () => {
|
|||
})
|
||||
).validate({ id: 'abc13', color: 'rubbish' })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"At shape().color: Expected "red" or "green" or "blue", got rubbish"`
|
||||
`"At shape.color: Expected "red" or "green" or "blue", got rubbish"`
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
@ -7559,6 +7559,7 @@ __metadata:
|
|||
dependencies:
|
||||
"@tldraw/utils": "workspace:*"
|
||||
lazyrepo: "npm:0.0.0-alpha.27"
|
||||
lodash.isequal: "npm:^4.5.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
|
|
Loading…
Reference in a new issue