Skip to content
Open
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
33 changes: 33 additions & 0 deletions doc/v3/owl/reference/types_validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ class UserCard extends Component {
The validators can also be used standalone via `validateType` and `assertType`
to validate any value at runtime, not just props.

Validating never observes: the value is walked untracked and raw, so a call
made inside a `computed` or an `effect` subscribes to nothing, even when the
value is reactive.

## `validateType`

Checks a value against a validator and returns a list of validation issues.
Expand Down Expand Up @@ -178,6 +182,35 @@ t.record(t.number()); // all values must be numbers
// rejects: { a: 1, b: "two" } with t.record(t.number())
```

### `t.set(valueType?)`

Validates that the value is a `Set`. When `valueType` is provided, every
element is validated against it.

```js
t.set(); // any set
t.set(t.number()); // set of numbers

// validates: new Set([1, 2]) with t.set(t.number())
// rejects: new Set([1, "two"]) with t.set(t.number())
```

### `t.map(keyType?, valueType?)`

Validates that the value is a `Map`. When `keyType` is provided, every key is
validated against it, and likewise for `valueType` and the values. A failing
entry is reported by its iteration index (`0 > key`, `0 > value`), since a map
key can be any value.

```js
t.map(); // any map
t.map(t.string()); // string keys, any value
t.map(t.string(), t.number()); // string keys, number values

// validates: new Map([["a", 1]]) with t.map(t.string(), t.number())
// rejects: new Map([["a", "one"]]) with t.map(t.string(), t.number())
```

### `t.tuple(types)`

Validates that the value is an array with a fixed length, where each element
Expand Down
48 changes: 48 additions & 0 deletions packages/owl-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,52 @@ function recordType(valueType?: any): any {
});
}

function setType(): Type<Set<any>>;
function setType<V>(): Type<Set<V>>;
function setType<V>(valueType: V): Type<Set<StripBrands<V>>>;
function setType(valueType?: any): any {
return makeType(function validateSet(context: ValidationContext) {
if (!(context.value instanceof Set)) {
context.addIssue({ message: "value is not a set" });
return;
}
if (!valueType) {
return;
}
let index = 0;
for (const value of context.value) {
context.withEntry(index++, value).validate(valueType);
}
});
}

function mapType(): Type<Map<any, any>>;
function mapType<K, V>(): Type<Map<K, V>>;
function mapType<K>(keyType: K): Type<Map<StripBrands<K>, any>>;
function mapType<K, V>(keyType: K, valueType: V): Type<Map<StripBrands<K>, StripBrands<V>>>;
function mapType(keyType?: any, valueType?: any): any {
return makeType(function validateMap(context: ValidationContext) {
if (!(context.value instanceof Map)) {
context.addIssue({ message: "value is not a map" });
return;
}
if (!keyType && !valueType) {
return;
}
// A map key can be any value, so an entry is located by its iteration index.
let index = 0;
for (const [key, value] of context.value) {
if (keyType) {
context.withEntry([index, "key"], key).validate(keyType);
}
if (valueType) {
context.withEntry([index, "value"], value).validate(valueType);
}
index++;
}
});
}

function tuple<const T extends unknown[]>(types: T): Type<StripBrandsAll<T>> {
const validate = makeType(function validateTuple(context: ValidationContext) {
if (!Array.isArray(context.value)) {
Expand Down Expand Up @@ -584,13 +630,15 @@ export const types = {
function: functionType,
instanceOf: instanceType,
literal: literalType,
map: mapType,
number: numberType,
object: objectType,
or: union,
promise: promiseType,
record: recordType,
ref,
selection: literalSelection,
set: setType,
signal: reactiveValueType,
strictObject: strictObjectType,
string: stringType,
Expand Down
13 changes: 10 additions & 3 deletions packages/owl-core/src/validation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { untrack } from "./computations";
import { OwlError } from "./owl_error";
import { toRaw } from "./proxy";

export interface ValidationIssue {
message: string;
Expand All @@ -15,6 +17,7 @@ export interface ValidationContext {
path: PropertyKey[];
validate(type: any): void;
value: any;
withEntry(key: PropertyKey | PropertyKey[], value: any): ValidationContext;
withIssues(issues: ValidationIssue[]): ValidationContext;
withKey(key: PropertyKey): ValidationContext;
}
Expand Down Expand Up @@ -65,7 +68,8 @@ function createContext(
return {
issueDepth: 0,
path,
value,
// Walk the raw value: a proxy read builds an atom the target then keeps.
value: toRaw(value),
get isValid() {
return !issues.length;
},
Expand All @@ -85,17 +89,20 @@ function createContext(
parent.issueDepth = this.issueDepth + depthOffset;
}
},
withEntry(key, value) {
return createContext(issues, value, this.path.concat(key), this);
},
withIssues(issues) {
return createContext(issues, this.value, this.path, this, 0);
},
withKey(key) {
return createContext(issues, this.value[key], this.path.concat(key), this);
return this.withEntry(key, this.value[key]);
},
};
}

export function validateType(value: any, validation: any): ValidationIssue[] {
const issues: ValidationIssue[] = [];
validation(createContext(issues, value, []));
untrack(() => validation(createContext(issues, value, [])));
return issues;
}
Loading