1+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
12/* eslint-disable @typescript-eslint/no-unsafe-return */
23
4+ import type { StandardSchemaV1 } from '@standard-schema/spec' ;
5+ import assert from 'assert' ;
36import type { BaseObjectLikeTypeImpl , BaseTypeImpl } from './base-type' ;
47import type { BasicType , LiteralValue , NumberTypeConfig , OneOrMore , Properties , StringTypeConfig , Type , Visitor } from './interfaces' ;
58import type { ArrayType , InterfaceType , IntersectionType , KeyofType , LiteralType , RecordType , UnionType , unknownRecord } from './types' ;
69import { an , basicType , printValue } from './utils' ;
710import { ValidationError } from './validation-error' ;
811
12+ /** Test case for a type. */
913export interface TypeTestCase {
14+ /** The expected name of the type */
1015 name : string ;
16+ /** The type to test. Can be a single type or an array of types. */
1117 type : Type < any > | Type < any > [ ] ;
1218 basicType ?: BasicType | 'mixed' ;
19+ /** Values that the type should accept as being valid. Note that the parser is not used for these values. */
1320 validValues ?: unknown [ ] ;
14- invalidValues ?: [ value : unknown , message : string | string [ ] | RegExp ] [ ] ;
21+ /**
22+ * Values that the type should not accept as being valid. Again, no parser is used for these values. Note that this input is also used
23+ * for invalidConversions unless provided explicitly. Therefore it is also possible to provide the third parameter (`issues`) here. Look
24+ * at invalidConversions for more details.
25+ */
26+ invalidValues ?: [ value : unknown , message : string | string [ ] | RegExp , issues ?: StandardSchemaV1 . Issue [ ] ] [ ] ;
27+ /** Values that type should accept as being valid after applying any parsers. */
1528 validConversions ?: [ input : unknown , value : unknown ] [ ] ;
16- invalidConversions ?: [ input : unknown , message : string | string [ ] | RegExp ] [ ] ;
29+ /**
30+ * Values that the type should not accept as being valid after applying any parsers. These cases are also applied to the standard schema
31+ * validation because that is linked to our validation "in construct mode". The third parameter can be given to override our default
32+ * expectations of the standard schema error messages. In a lot of cases we can determine this automatically, but in some cases we
33+ * cannot.
34+ */
35+ invalidConversions ?: [ input : unknown , message : string | string [ ] | RegExp , issues ?: StandardSchemaV1 . Issue [ ] ] [ ] ;
1736}
1837
1938/**
@@ -35,7 +54,9 @@ export function testTypeImpl({
3554 validValues,
3655 invalidValues,
3756 validConversions,
38- invalidConversions,
57+ // Also test the same conditions using the `construct` method, instead of only using the `check` method. This also ensures we take the
58+ // standard schema validation into account.
59+ invalidConversions = invalidValues ,
3960} : TypeTestCase ) : void {
4061 describe ( `test: ${ name } ` , ( ) => {
4162 Array . isArray ( types ) ? describe . each ( types ) ( 'implementation %#' , theTests ) : theTests ( types ) ;
@@ -87,11 +108,13 @@ export function testTypeImpl({
87108 expect ( type . apply ( undefined , [ input ] ) ) . toEqual ( output ) ;
88109 expect ( type . bind ( undefined , input ) ( ) ) . toEqual ( output ) ;
89110 expect ( type . call ( undefined , input ) ) . toEqual ( output ) ;
111+ expect ( standardValidate ( type , input ) ) . toEqual ( { value : output } ) ;
90112 } ) ;
91113
92114 invalidConversions &&
93- test . each ( invalidConversions ) ( 'will not convert: %p' , ( value , message ) => {
115+ test . each ( invalidConversions ) ( 'will not convert: %p' , ( value , message , issues = defaultIssues ( message ) ) => {
94116 expect ( ( ) => type ( value ) ) . toThrowWithMessage ( ValidationErrorForTest , Array . isArray ( message ) ? message . join ( '\n' ) : message ) ;
117+ expect ( standardValidate ( type , value ) ) . toEqual ( { issues } ) ;
95118 } ) ;
96119 }
97120}
@@ -209,3 +232,40 @@ class CreateExampleVisitor implements Visitor<unknown> {
209232function hasExample < T > ( obj : BaseTypeImpl < T > ) : obj is BaseTypeImpl < T > & { example : T } {
210233 return 'example' in obj ;
211234}
235+
236+ /**
237+ * Helper function around StandardSchema validation interface to incorporate it in the existing conversion tests.
238+ *
239+ * Note that Skunkteam Types has a distinction between checking if an input conforms to a schema (Type) as-is (`.is()`, `.check()`) vs
240+ * validating if an input can be parsed and converted into the schema (`.construct()`). This makes it non-trivial to fully incorporate
241+ * the StandardSchema interface into the existing test-suite.
242+ */
243+ function standardValidate < T > ( schema : StandardSchemaV1 < T > , input : unknown ) : StandardSchemaV1 . Result < T > {
244+ const result = schema [ '~standard' ] . validate ( input ) ;
245+ if ( result instanceof Promise ) throw new TypeError ( 'No asynchronous type validation in Skunkteam Types' ) ;
246+ return result ;
247+ }
248+
249+ function defaultIssues ( input : string | RegExp | string [ ] ) : StandardSchemaV1 . Issue [ ] {
250+ const message = Array . isArray ( input ) ? input . join ( '\n' ) : typeof input === 'string' ? input : expect . stringMatching ( input ) ;
251+ // Perform some string parsing on the error to guess what the standard schema issue should look like. This is just to prevent having to
252+ // configure a lot of expectations, but does not cover every possibility. Especially multiple errors will have to be stated explicitly.
253+ //
254+ // Things we do here:
255+ // - Remove the common header that says "error in {optional context} [TheTypeName]:", because that should not be part of the issues
256+ // list.
257+ // - Try to guess the path if any.
258+ const hasPath = typeof message === 'string' && / ^ e r r o r i n [ ^ [ ] * \[ .* ?\] (?: a t [ ^ < ] * < (?< path > [ ^ > ] + ) > ) ? : (?< message > [ ^ ] * ) $ / . exec ( message ) ;
259+ if ( hasPath ) {
260+ assert ( hasPath . groups ) ;
261+ return [
262+ {
263+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- TypeScript is wrong here
264+ path : hasPath . groups . path ?. split ( '.' ) . map ( key => ( / ^ \[ \d + \] $ / . test ( key ) ? + key . slice ( 1 , - 1 ) : key ) ) ,
265+ message : hasPath . groups . message ,
266+ } ,
267+ ] ;
268+ }
269+ // if (typeof message === 'string') message = message.replace(/^error in .*\[.*\]: /, '');
270+ return [ { message } ] ;
271+ }
0 commit comments