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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,14 @@ pval.describe('value')
pval.fail('title', 'expected non-empty string, got string ("")')
```

`pval.or(...)` is useful for union-style runtime contracts:

```ts
head.validateProps({
value: pval.or(pval.isString, pval.refOf(pval.isString)),
})
```

### Dynamic bindings and refs

Single-prop dynamic bindings like `:title="titleRef"` flow into component props as refs.
Expand Down
14 changes: 14 additions & 0 deletions docs-site/src/content/docs/api/pval.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,20 @@ head.validateProps({
})
```

It also works well with ref-aware unions:

```ts
head.validateProps({
value: pval.or(pval.isString, pval.refOf(pval.isString)),
})
```

Example failure:

```txt
Invalid prop "value": expected string or expected ref<string>, got ref<number>(1).
```

### `pval.oneOf(values)`

Ensures the value is one of the provided literals.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "regor",
"version": "1.5.0",
"version": "1.5.1",
"description": "A modern UI framework for web and desktop applications, inspired by Vue's concepts and powered by simplicity and flexibility.",
"author": "Ahmed Yasin Koculu",
"license": "MIT",
Expand Down
43 changes: 37 additions & 6 deletions src/app/propValidators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ const formatPreview = (value: unknown, depth = 0): string => {
* @returns A compact `got ...` description suitable for validator errors.
*/
export const describe = (value: unknown): string => {
if (isRef(value)) {
return `got ${describeValue(value)}(${formatPreview(value())})`
}
const type = describeValue(value)
const preview = formatPreview(value)
return `got ${type} (${preview})`
Expand All @@ -203,15 +206,36 @@ const getErrorDetail = (error: unknown): string => {
return String(error)
}

const formatOrDetail = (errors: unknown[], value: unknown): string => {
const trimReceivedSuffix = (detail: string, value: unknown): string => {
const received = `, ${describe(value)}.`
return detail.endsWith(received) ? detail.slice(0, -received.length) : detail
}

const getOrReason = (name: string, error: unknown, value: unknown): string => {
if (error instanceof PropValidationError) {
if (error.propPath === name) {
return trimReceivedSuffix(error.detail, value)
}
if (error.propPath === `${name}.value` && isRef(value)) {
const innerReason = trimReceivedSuffix(error.detail, value())
if (innerReason.startsWith('expected ')) {
return `expected ref<${innerReason.slice('expected '.length)}>`
}
return `expected ref where ${innerReason}`
}
}
return getErrorDetail(error)
}

const formatOrDetail = (
name: string,
errors: unknown[],
value: unknown,
): string => {
const reasons: string[] = []

for (const error of errors) {
const detail = getErrorDetail(error)
const reason = detail.endsWith(received)
? detail.slice(0, -received.length)
: detail
const reason = getOrReason(name, error, value)
if (!reasons.includes(reason)) reasons.push(reason)
}

Expand Down Expand Up @@ -309,6 +333,13 @@ export const nullable = <TValue>(
* value: pval.or(pval.isString, pval.isNumber),
* })
* ```
*
* Example with refs:
* ```ts
* head.validateProps({
* value: pval.or(pval.isString, pval.refOf(pval.isString)),
* })
* ```
*/
export const or = <TValidators extends ValidatorTuple>(
...validators: TValidators
Expand All @@ -323,7 +354,7 @@ export const or = <TValidators extends ValidatorTuple>(
errors.push(error)
}
}
fail(name, formatOrDetail(errors, value))
fail(name, formatOrDetail(name, errors, value))
}
}

Expand Down
106 changes: 106 additions & 0 deletions tests/app/component-head.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,112 @@ test('component head validator utilities support or validation', () => {
)
})

test('component head validator utilities support or validation with ref branches', () => {
const host = document.createElement('div')
const start = document.createComment('s')
const end = document.createComment('e')
const head = new ComponentHead(
{
value: ref(1),
},
host,
[],
start,
end,
'throw',
)

expect(() =>
head.validateProps({
value: pval.or(pval.isString, pval.refOf(pval.isString)),
}),
).toThrow(
'Invalid prop "value" on <div>: expected string or expected ref<string>, got ref<number>(1).',
)
})

test('component head validator utilities support or validation with alternate shapes', () => {
const host = document.createElement('div')
const start = document.createComment('s')
const end = document.createComment('e')

const okHead = new ComponentHead(
{
value: {
slug: 'post-1',
},
},
host,
[],
start,
end,
'throw',
)

expect(() =>
okHead.validateProps({
value: pval.or(
pval.shape({
slug: pval.isString,
}),
pval.shape({
id: pval.isNumber,
}),
),
}),
).not.toThrow()
})

test('component head validator utilities support nested or validation inside shapes', () => {
const host = document.createElement('div')
const start = document.createComment('s')
const end = document.createComment('e')

const okHead = new ComponentHead(
{
meta: {
owner: ref('ada'),
},
},
host,
[],
start,
end,
'throw',
)

expect(() =>
okHead.validateProps({
meta: pval.shape({
owner: pval.or(pval.isString, pval.refOf(pval.isString)),
}),
}),
).not.toThrow()

const badHead = new ComponentHead(
{
meta: {
owner: ref(12),
},
},
host,
[],
start,
end,
'throw',
)

expect(() =>
badHead.validateProps({
meta: pval.shape({
owner: pval.or(pval.isString, pval.refOf(pval.isString)),
}),
}),
).toThrow(
'Invalid prop "meta.owner" on <div>: expected string or expected ref<string>, got ref<number>(12).',
)
})

test('component head validateProps accepts custom user validators', () => {
const host = document.createElement('div')
const start = document.createComment('s')
Expand Down
2 changes: 1 addition & 1 deletion tests/app/prop-validation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ test('component props validation throws before invalid component mount completes
},
),
).toThrow(
'Invalid prop "count" on <validatedcard>: expected number, got ref<string> (ref("oops")).',
'Invalid prop "count" on <validatedcard>: expected number, got ref<string>("oops").',
)
expect(root.querySelector('.summary')).toBeNull()
})
Expand Down
Loading