Skip to content

Commit

Permalink
fix: validation messages now refer to the used type (#97)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavadeli authored Oct 9, 2024
1 parent fa5cc5e commit 857d64e
Show file tree
Hide file tree
Showing 7 changed files with 43 additions and 48 deletions.
2 changes: 1 addition & 1 deletion etc/types.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export abstract class BaseTypeImpl<ResultType, TypeConfig = unknown> implements
// (undocumented)
protected createAutoCastAllType(): this;
protected createResult(input: unknown, result: unknown, validatorResult: ValidationResult): Result<ResultType>;
protected readonly customValidators: ReadonlyArray<(<T extends ResultType>(this: void, input: T, options: ValidationOptions) => Result<T>)>;
protected readonly customValidators: ReadonlyArray<Validator<unknown>>;
readonly enumerableLiteralDomain?: Iterable<LiteralValue>;
extendWith<const E>(factory: (type: this) => E): this & E;
get is(): TypeguardFor<ResultType>;
Expand Down
6 changes: 5 additions & 1 deletion markdown/types.basetypeimpl.customvalidators.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,9 @@ Additional custom validation added using [withValidation](./types.basetypeimpl.w
**Signature:**

```typescript
protected readonly customValidators: ReadonlyArray<(<T extends ResultType>(this: void, input: T, options: ValidationOptions) => Result<T>)>;
protected readonly customValidators: ReadonlyArray<Validator<unknown>>;
```

## Remarks

It says `Validator<unknown>` here, but it should only contain closures with `Validator<ResultType>` parameters. However, that would mean that this type is no longer assignable to `Type<unknown>` which is technically correct, but very inconvenient. We want to be able to write functions that ask for a type that validates anything, we don't care what. If we are not able to use the type `Type<unknown>` in those cases, then we are left with `Type<any>` which leads to `any`<!-- -->-contamination of our consumer code.
22 changes: 11 additions & 11 deletions markdown/types.basetypeimpl.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@ All type-implementations must extend this base class. Use [createType()](./types
## Properties
| Property | Modifiers | Type | Description |
| --------------------------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [autoCast](./types.basetypeimpl.autocast.md) | <code>readonly</code> | this | The same type, but with an auto-casting default parser installed. |
| [autoCastAll](./types.basetypeimpl.autocastall.md) | <code>readonly</code> | this | Create a recursive autocasting version of the current type. |
| [basicType](./types.basetypeimpl.basictype.md) | <p><code>abstract</code></p><p><code>readonly</code></p> | [BasicType](./types.basictype.md) \| 'mixed' | The kind of values this type validates. |
| [check](./types.basetypeimpl.check.md) | <code>readonly</code> | (this: void, input: unknown) =&gt; ResultType | Asserts that a value conforms to this Type and returns the input as is, if it does. |
| [customValidators](./types.basetypeimpl.customvalidators.md) | <p><code>protected</code></p><p><code>readonly</code></p> | ReadonlyArray&lt;(&lt;T extends ResultType&gt;(this: void, input: T, options: [ValidationOptions](./types.validationoptions.md)<!-- -->) =&gt; [Result](./types.result.md)<!-- -->&lt;T&gt;)&gt; | Additional custom validation added using [withValidation](./types.basetypeimpl.withvalidation.md) or [withConstraint](./types.basetypeimpl.withconstraint.md)<!-- -->. |
| [enumerableLiteralDomain?](./types.basetypeimpl.enumerableliteraldomain.md) | <code>readonly</code> | Iterable&lt;[LiteralValue](./types.literalvalue.md)<!-- -->&gt; | _(Optional)_ The set of valid literals if enumerable. |
| [is](./types.basetypeimpl.is.md) | <code>readonly</code> | [TypeguardFor](./types.typeguardfor.md)<!-- -->&lt;ResultType&gt; | A type guard for this Type. |
| [name](./types.basetypeimpl.name.md) | <p><code>abstract</code></p><p><code>readonly</code></p> | string | The name of the Type. |
| [typeConfig](./types.basetypeimpl.typeconfig.md) | <p><code>abstract</code></p><p><code>readonly</code></p> | TypeConfig | Extra information that is made available by this Type for runtime analysis. |
| Property | Modifiers | Type | Description |
| --------------------------------------------------------------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [autoCast](./types.basetypeimpl.autocast.md) | <code>readonly</code> | this | The same type, but with an auto-casting default parser installed. |
| [autoCastAll](./types.basetypeimpl.autocastall.md) | <code>readonly</code> | this | Create a recursive autocasting version of the current type. |
| [basicType](./types.basetypeimpl.basictype.md) | <p><code>abstract</code></p><p><code>readonly</code></p> | [BasicType](./types.basictype.md) \| 'mixed' | The kind of values this type validates. |
| [check](./types.basetypeimpl.check.md) | <code>readonly</code> | (this: void, input: unknown) =&gt; ResultType | Asserts that a value conforms to this Type and returns the input as is, if it does. |
| [customValidators](./types.basetypeimpl.customvalidators.md) | <p><code>protected</code></p><p><code>readonly</code></p> | ReadonlyArray&lt;[Validator](./types.validator.md)<!-- -->&lt;unknown&gt;&gt; | Additional custom validation added using [withValidation](./types.basetypeimpl.withvalidation.md) or [withConstraint](./types.basetypeimpl.withconstraint.md)<!-- -->. |
| [enumerableLiteralDomain?](./types.basetypeimpl.enumerableliteraldomain.md) | <code>readonly</code> | Iterable&lt;[LiteralValue](./types.literalvalue.md)<!-- -->&gt; | _(Optional)_ The set of valid literals if enumerable. |
| [is](./types.basetypeimpl.is.md) | <code>readonly</code> | [TypeguardFor](./types.typeguardfor.md)<!-- -->&lt;ResultType&gt; | A type guard for this Type. |
| [name](./types.basetypeimpl.name.md) | <p><code>abstract</code></p><p><code>readonly</code></p> | string | The name of the Type. |
| [typeConfig](./types.basetypeimpl.typeconfig.md) | <p><code>abstract</code></p><p><code>readonly</code></p> | TypeConfig | Extra information that is made available by this Type for runtime analysis. |
## Methods
Expand Down
42 changes: 17 additions & 25 deletions src/base-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,14 @@ export abstract class BaseTypeImpl<ResultType, TypeConfig = unknown> implements
/**
* Additional custom validation added using {@link BaseTypeImpl.withValidation | withValidation} or
* {@link BaseTypeImpl.withConstraint | withConstraint}.
*
* @remarks
* It says `Validator<unknown>` here, but it should only contain closures with `Validator<ResultType>` parameters. However, that would
* mean that this type is no longer assignable to `Type<unknown>` which is technically correct, but very inconvenient. We want to be
* able to write functions that ask for a type that validates anything, we don't care what. If we are not able to use the type
* `Type<unknown>` in those cases, then we are left with `Type<any>` which leads to `any`-contamination of our consumer code.
*/
// It says `input: unknown` here, but it should only contain closures with `input: ResultType` parameters. However, that would mean that
// this type is no longer assignable to `Type<unknown>` which is technically correct, but very inconvenient. We want to be able to write
// functions that ask for a type that validates anything, we don't care what. If we are not able to use the type `Type<unknown>` in
// those cases, then we are left with `Type<any>` which leads to `any`-contamination of our consumer code.
protected readonly customValidators: ReadonlyArray<
<T extends ResultType>(this: void, input: T, options: ValidationOptions) => Result<T>
> = [];
protected readonly customValidators: ReadonlyArray<Validator<unknown>> = [];

/**
* Optional pre-processing parser.
Expand Down Expand Up @@ -273,7 +273,9 @@ export abstract class BaseTypeImpl<ResultType, TypeConfig = unknown> implements
let result = this.typeValidator(value, options);
for (const customValidator of this.customValidators) {
if (!result.ok) break;
result = customValidator(result.value, options);
const resultValue = result.value;
const tryResult = ValidationError.try({ type: this, input }, () => customValidator(resultValue, options));
result = tryResult.ok ? this.createResult(resultValue, resultValue, tryResult.value) : tryResult;
}
if (this.typeParser && options.mode === 'construct') {
result = addParserInputToResult(result, input);
Expand Down Expand Up @@ -391,16 +393,11 @@ export abstract class BaseTypeImpl<ResultType, TypeConfig = unknown> implements
* @param validation - the additional validation to restrict the current type
*/
withValidation(validation: Validator<ResultType>): this {
const typeValidator = (input: ResultType, options: ValidationOptions): Result<ResultType> => {
const tryResult = ValidationError.try<ValidationResult>(
{ type, input },
// if no name is given, then default to the message "additional validation failed"
() => validation(input, options) || 'additional validation failed',
);
return tryResult.ok ? type.createResult(input, input, tryResult.value) : tryResult;
};
const type = createType(this, { customValidators: { configurable: true, value: [...this.customValidators, typeValidator] } });
return type;
// default to the message "additional validation failed", this differs from `withConstraint` where we don't fall back to a default
// message. This results in subtly different error messages. Using `withValidation` the error message will be something like:
// "[BaseType]: additional validation failed", while `withConstraint` will result in "expected a [NewTypeName], got: ...".
const validator: Validator<ResultType> = (input, options) => validation(input, options) || 'additional validation failed';
return createType(this, { customValidators: { configurable: true, value: [...this.customValidators, validator] } });
}

/**
Expand All @@ -416,15 +413,10 @@ export abstract class BaseTypeImpl<ResultType, TypeConfig = unknown> implements
name: BrandName,
constraint: Validator<ResultType>,
): Type<Branded<ResultType, BrandName>, TypeConfig> {
const typeValidator = (input: ResultType, options: ValidationOptions): Result<Branded<ResultType, BrandName>> => {
const tryResult = ValidationError.try({ type, input }, () => constraint(input, options));
return tryResult.ok ? type.createResult(input, input, tryResult.value) : tryResult;
};
const type = createType(branded<ResultType, BrandName, TypeConfig>(this), {
return createType(branded<ResultType, BrandName, TypeConfig>(this), {
name: { configurable: true, value: name },
customValidators: { configurable: true, value: [...this.customValidators, typeValidator] },
customValidators: { configurable: true, value: [...this.customValidators, constraint] },
});
return type;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,7 @@ testTypeImpl({
invalidConversions: [
[100, 'error in parser precondition of [ChainedParserParser]: outer parser failed, got: 100'],
[10, 'error in parser precondition of [Inner]: inner parser failed, got: 110, parsed from: 10'],
[5, 'error in [number]: inner validation failed, got: 115, parsed from: 5'],
[5, 'error in [ChainedParserParser]: inner validation failed, got: 115, parsed from: 5'],
],
});

Expand Down
5 changes: 2 additions & 3 deletions src/types/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
TypeImpl,
TypeOfProperties,
ValidationOptions,
Validator,
Visitor,
Writable,
} from '../interfaces';
Expand Down Expand Up @@ -228,9 +229,7 @@ export class InterfaceType<Props extends Properties, ResultType>
private _mergeWith<OtherProps extends Properties, OtherType>(
{ name = this.isDefaultName ? undefined : this.name, omitParsers, omitValidations }: Partial<InterfaceMergeOptions>,
otherPropsInfo: PropertiesInfo<OtherProps>,
otherCustomValidators: ReadonlyArray<
<T extends ResultType & OtherType>(this: void, input: T, options: ValidationOptions) => Result<T>
>,
otherCustomValidators: ReadonlyArray<Validator<unknown>>,
otherHasCustomParser: boolean,
method: string,
): MergeType<Props, ResultType, OtherProps, OtherType> {
Expand Down
12 changes: 6 additions & 6 deletions src/types/union.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ testTypeImpl({
[
'error in [StrangeNumberUnion.autoCastAll]: failed every element in union:',
'(got: 3)',
' • expected a [LessThanMinus10]',
' • expected a [LessThanMinus10.autoCast]',
' • expected the literal 0',
' • errors in [number]:',
' • errors in [number.autoCast]:',
' ‣ should be more than 10',
' ‣ not even close',
],
Expand All @@ -62,9 +62,9 @@ testTypeImpl({
[
'error in [StrangeNumberUnion.autoCastAll]: failed every element in union:',
'(got: 7)',
' • expected a [LessThanMinus10]',
' • expected a [LessThanMinus10.autoCast]',
' • expected the literal 0',
' • error in [number]: should be more than 10',
' • error in [number.autoCast]: should be more than 10',
],
],
],
Expand All @@ -79,9 +79,9 @@ testTypeImpl({
[
'error in [StrangeNumberUnion.autoCastAll]: failed every element in union:',
'(got: "-5")',
' • expected a [LessThanMinus10]',
' • expected a [LessThanMinus10.autoCast]',
' • expected the literal 0',
' • errors in [number]:',
' • errors in [number.autoCast]:',
' ‣ should be more than 10',
' ‣ not even close',
],
Expand Down

0 comments on commit 857d64e

Please sign in to comment.